bitarray
bitarray is a C extension that stores Boolean sequences as packed bits, eight values per byte, while exposing familiar Python sequence operations. It supports slicing, mutation, bitwise operators, searches, buffer import and export, selectable bit endianness, file and byte conversion, Huffman coding, sparse compression, integer and hexadecimal helpers, and immutable hashable frozenbitarray values. It is aimed at dense flags and binary algorithms where a Python list of bool objects wastes too much memory.
bitarray is the practical default for dense, mutable bits in Python and its C implementation offers much more than compact storage. Do not install it merely to avoid a small list of booleans, and define serialization length and endianness before data reaches disk or a network.
Use it if
- You need millions of dense Boolean flags and want one bit per value instead of Python object references
- Your workload spends meaningful time in bitwise intersection, union, XOR, counting, shifting, or scanning set bits
- You need a Python object that can import or export a contiguous buffer, including memory-mapped storage
- You are implementing prefix codes, compact integer fields, Bloom-style filters, or dense membership masks
- Your data is a sparse set of large integers: a compressed bitmap such as PyRoaring can use less memory and provide set-oriented operations without allocating every intervening bit
- You need named, mixed-width binary fields with declarative pack and unpack formats: bitstruct or bitstring expresses protocols more directly than manual slices
- You cannot accept a native extension: the README says all functionality is implemented in C, so an unsupported platform or source install needs a compiler and Python development headers
- Your code cannot define bit endianness and logical length at every binary boundary: tobytes pads the final byte, frombytes always adds eight bits per byte, and endian changes byte interpretation
- You expect NumPy-style numeric vector operations or multidimensional broadcasting: bitarray is a one-dimensional homogeneous bit sequence, not a general array library
Setup reality
`pip install bitarray` is usually uneventful because the project publishes wheels for major platforms and supported Python versions, and version 3.10.1 has no runtime dependencies. It supports Python 3.7 and newer and includes free-threading support for GIL-disabled CPython 3.14+. The fallback is a native source build, so unusual architectures, minimal containers, and brand-new interpreters need a C compiler plus matching Python headers. Run `python -c 'import bitarray; bitarray.test()'` when validating a custom build; the self-test is a public API and the README's current run covers 640 tests. The main setup decision is data representation. Every bitarray has `big` or `little` bit endianness, defaulting to big, and that affects bytes, files, buffers, hexadecimal conversion, and integer conversion. It is separate from the machine's byte order. Persist the endian and exact bit length in your format. `tobytes()` zero-pads an incomplete last byte, while `frombytes()` restores all eight bits in each byte, so bytes alone cannot recover a non-byte-aligned length. Use `bitarray.util.serialize()` and `deserialize()` when you control both ends and want the library's stable representation to retain metadata. Version 3 changed `search()` and `decode()` to return iterators, so old code expecting lists must wrap them explicitly. Converting a large array with `tolist()` defeats the point: the reference warns that the list can require 32 or 64 times more memory. Buffer-backed instances may be read-only, and resizing is constrained while another object exports or owns the underlying buffer. Operations are fast, but bitwise operands still need compatible lengths, and shifts preserve length rather than growing the number.
Patterns
Create a dense flag array and update rangescreate-and-mutate-bits
from bitarray import bitarray
flags = bitarray(1_000_000)
flags.setall(0)
flags[100:200] = 1
flags[150] = 0
active = flags.count(1)An integer initializer creates that many zero bits in current bitarray. Slice assignment to a Boolean avoids allocating a temporary repeated array.
Intersect and compare dense maskscombine-bit-masks
from bitarray import bitarray
from bitarray.util import count_and, count_xor
a = bitarray('10110100')
b = bitarray('11100100')
intersection = a & b
common_count = count_and(a, b)
hamming_distance = count_xor(a, b)count_and and count_xor avoid allocating the intermediate bitarray produced by `(a & b)` or `(a ^ b)`.
Iterate indices whose bit is oneiterate-set-bits
from bitarray import bitarray
flags = bitarray('00100101')
for index in flags.search(1):
print(index)Since bitarray 3.0, search returns an iterator. Wrap it in list only when every index must be retained.
Convert fixed-width signed integersconvert-integers
from bitarray.util import ba2int, int2ba
bits = int2ba(-12, length=16, endian='big', signed=True)
value = ba2int(bits, signed=True)
assert value == -12Signed conversion uses two's complement and requires an explicit length. Values that do not fit raise OverflowError.
Round-trip hexadecimal dataconvert-hexadecimal
from bitarray.util import ba2hex, hex2ba
bits = hex2ba('dead beef', endian='big')
assert ba2hex(bits) == 'deadbeef'Hex output requires a bit length divisible by four. The selected bit endianness changes the relationship between bit indices and encoded bytes.
Serialize exact length and endiannessserialize-with-metadata
from bitarray import bitarray
from bitarray.util import deserialize, serialize
original = bitarray('10101', endian='little')
payload = serialize(original)
restored = deserialize(payload)
assert restored == original
assert restored.endian == 'little'Prefer serialize over bare tobytes when the bit length is not byte-aligned or the receiver must recover endianness.
Read and write a byte-oriented bit fieldexchange-raw-bytes
from bitarray import bitarray
bits = bitarray(endian='big')
bits.frombytes(b'\xa5\x10')
assert len(bits) == 16
wire_bytes = bits.tobytes()frombytes adds eight bits for every byte. tobytes zero-pads the final byte, so store the logical length separately for partial-byte data.
View an existing writable buffer as bitsimport-buffer-without-copy
from bitarray import bitarray
buffer = bytearray([0b10100000, 0b00000001])
bits = bitarray(buffer=buffer, endian='big')
bits[0] = 0
assert buffer[0] == 0b00100000The bitarray imports the buffer rather than copying it. Mutability follows the source buffer, and resizing a buffer-backed view is not generally available.
Use an immutable bit pattern as a dictionary keyuse-hashable-bits
from bitarray import frozenbitarray
routes = {
frozenbitarray('1010'): 'north',
frozenbitarray('0101'): 'south',
}
assert routes[frozenbitarray('1010')] == 'north'Regular bitarray objects are mutable and unhashable. frozenbitarray preserves bit behavior but rejects assignment.
Build and use a Huffman codeencode-huffman-data
from collections import Counter
from bitarray import bitarray, decodetree
from bitarray.util import huffman_code
text = 'banana bandana'
code = huffman_code(Counter(text))
encoded = bitarray()
encoded.encode(code, text)
tree = decodetree(code)
decoded = ''.join(encoded.decode(tree))
assert decoded == textdecode returns an iterator. Reuse a decodetree when decoding many payloads with the same code to avoid rebuilding its internal tree.
Compress a sparse bitarraycompress-sparse-bits
from bitarray import bitarray
from bitarray.util import sc_decode, sc_encode
bits = bitarray(1_000_000)
bits.setall(0)
bits[[7, 1000, 900_000]] = 1
compressed = sc_encode(bits)
restored = sc_decode(compressed)
assert restored == bitsSparse compression helps storage and transfer, but random access requires decoding back to a bitarray. PyRoaring may fit long-lived sparse integer sets better.
Pack one-byte flags into individual bitspack-byte-flags
from bitarray import bitarray
raw_flags = b'\x00\x01\x00\xff'
bits = bitarray()
bits.pack(raw_flags)
assert bits.to01() == '0101'
assert bits.unpack(zero=b'N', one=b'Y') == b'NYNY'pack maps each input byte to one bit, with zero becoming 0 and every nonzero byte becoming 1. It is different from frombytes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| bitstring | PyPI | You want readable slicing and parsing for binary protocols, files, and mixed-width integer fields |
| bitstruct | PyPI | You want struct-like format strings for packing and unpacking named-width bit fields |
| numpy | PyPI | You need multidimensional Boolean arrays, broadcasting, and numerical operations more than one-bit storage |
| pyroaring | PyPI | You store sparse integer sets and want compressed bitmap set operations and fast iteration |