Binary Representation in Python

Working with binary data the Pythonic way

Why Python Is Great for Binary Experiments

In my experience, Python is the most comfortable language for exploring binary concepts, whether you are a beginner learning how bits work or an experienced developer prototyping a binary protocol. Python's arbitrary-precision integers, built-in conversion functions, and powerful standard library make binary manipulation almost effortless. I have used Python to build binary file parsers, network packet analyzers, and educational tools that help people understand binary representation. The best part is that you can start experimenting in a REPL session without writing a single line of boilerplate.

bin(), format(), and int() with Base 2

Python gives you three excellent ways to convert between integers and their binary string representations. The bin() function is the simplest but includes the 0b prefix:

bin(42)       # '0b101010'
format(42, 'b') # '101010'
format(42, '016b') # '0000000000101010'
int('101010', 2) # 42

I use format() most often because of its flexible formatting options. The width specifier with zero-padding is invaluable when I need fixed-width binary output for debugging or display purposes. For the reverse direction, int(str, base=2) converts any binary string back to an integer. One thing I love about Python is that it handles negative numbers naturally: bin(-42) returns '-0b101010', making the two's complement representation obvious.

Bitwise Operators on Arbitrary-Precision Integers

Python's integers are arbitrary-precision, which means bitwise operations work correctly on numbers of any size. This is a huge advantage over languages like C or Java where you have to worry about fixed-width overflow:

x = 0b1100
y = 0b1010
x & y  # 0b1000 (8)
x | y  # 0b1110 (14)
x ^ y  # 0b0110 (6)
x << 2 # 0b110000 (48)
~x     # -13 (infinite two's complement)

The ~ operator in Python deserves special attention. Unlike fixed-width languages where ~0b1100 would give you 0b...0011 within a 4-bit window, Python treats integers as having infinite leading zeros. So ~0b1100 is -13 in Python, which is ...11110011 in infinite two's complement. When you need a fixed-width NOT, I use the pattern x ^ mask where mask has all bits set for your desired width.

bytes and bytearray Types

For working with raw binary data, Python's bytes and bytearray types are my daily drivers. bytes is immutable and ideal for fixed binary records, while bytearray is mutable and perfect for building binary data incrementally:

data = bytes([0x48, 0x65, 0x6C, 0x6C, 0x6F])
data[0]      # 72 (integer access)
buf = bytearray(16) # 16 zeroed bytes
buf[4:8] = b'\x00\x01\x02\x03'
buf.hex()   # '00000000000102030000000000000000'

I frequently use bytes.hex() and bytes.fromhex() for debugging and logging binary data. The memoryview object provides zero-copy slices of bytes objects, which I have used extensively when parsing large binary files without duplicating memory. The struct module pairs beautifully with bytes for packing and unpacking structured binary records.

struct.pack and struct.unpack

When I need to convert Python values to and from binary representations, the struct module is indispensable. It handles byte order, alignment, and type conversion in a single, compact format string:

import struct
packed = struct.pack('<IhB', 42, -1, 255)
# Little-endian: uint32, int16, uint8
unpacked = struct.unpack('<IhB', packed)
# Returns (42, -1, 255)

I have used struct to parse binary file headers (BMP, PNG, WAV), read and write serialized configuration data, and communicate with hardware devices over serial ports. The format string syntax is concise once you learn the byte order prefixes (< for little-endian, > for big-endian, ! for network byte order) and type codes. My advice is to always specify the byte order explicitly rather than relying on the default native order, which varies by platform and can lead to hard-to-find bugs.

Binary File I/O in Python

Reading and writing binary files in Python is straightforward with the 'rb' and 'wb' modes. I always use binary mode explicitly to prevent Python from doing any newline translation or encoding detection:

with open('data.bin', 'rb') as f:
  header = f.read(8) # read exactly 8 bytes
  payload = f.read() # read the rest

with open('output.bin', 'wb') as f:
  f.write(b'\x00\x01\x02\x03')

For large binary files, I use the f.read(chunk_size) pattern in a loop to avoid loading the entire file into memory. The seek() and tell() methods let me navigate through structured binary files with headers, indexes, and data sections. Combined with struct.unpack(), this setup handles virtually any binary file format I have encountered.

Bitarray Library for Advanced Use

For projects that need heavy bit-level manipulation beyond what Python's built-in types offer, I turn to the bitarray library. It provides a compact, efficient array of bits with optimized operations:

from bitarray import bitarray
ba = bitarray('11001010')
ba[0] = 1
ba.count(1) # number of set bits
ba & bitarray('11110000') # bitwise AND

In my experience, bitarray is the go-to choice for implementing compression algorithms, error-correcting codes, and bitmap indices. It supports slicing, packing and unpacking from bytes, and most bitwise operations. For projects where I need to process millions of bits efficiently, it outperforms manual bit manipulation with integers by a wide margin.

Best Practices for Binary Data in Python

Here are the patterns I rely on after years of working with binary data in Python:

  • Always use 'rb' and 'wb'. Never open binary files in text mode or you will get corrupted data on Windows due to newline translation.
  • Use struct.pack_into() for pre-allocated buffers. It avoids creating intermediate bytes objects, which matters for performance-critical code.
  • Leverage memoryview for zero-copy parsing. When you need to slice a large byte buffer without copying, memoryview.cast() is your friend.
  • Be explicit about integer width. Python's infinite integers are great, but when you need fixed-width behavior (e.g., wrapping at 32 bits), mask explicitly with & 0xFFFFFFFF.
  • Know the int.bit_length() method. It tells you how many bits are needed to represent a number, which is useful for calculating buffer sizes and compression ratios.
  • Use int.to_bytes() and int.from_bytes(). These methods from Python 3.2+ provide clean, native conversions between integers and byte sequences without the struct module.

Try It Yourself

Open the Binary Code Decoder in a new tab and enter some binary patterns to see the results instantly. All conversions happen in your browser — no data is sent to any server.