bz2 Module Complexity¶
The bz2 module compresses and decompresses data in the bzip2 format, through one-shot
functions, streaming compressor and decompressor objects, and a file interface. libbzip2 works a
block at a time: the compressor sorts each block of input on its own, and the decompressor has to
take in a whole compressed block before it can return any of it.
m is uncompressed bytes and n is compressed bytes. B is the block size a stream is written
with, compresslevel × 100 kB, so at most 900 kB. c is the bytes passed to one streaming call,
r the larger of the bytes a read asks for and the bytes it returns, k the uncompressed bytes a
seek passes over, s the streams concatenated in one input, and h the unconsumed input a
decompressor holds from earlier calls.
Sorting or decoding one block costs at most a fixed amount at a given level, so a B term marks
where that work lands: the call that completes a block pays for it. The Space column counts the
buffers Python sees. libbzip2's own working memory is set by the block size and does not grow with
the input.
Complexity Reference¶
One-Shot Functions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bz2.compress(data, compresslevel=9) |
O(m) | O(n) | Each block is sorted on its own, so time is linear in m at any level; memory follows the output, not the input |
bz2.decompress(data) |
O(n + m) | O(m) | One stream. m can be far larger than n |
bz2.decompress(data) over s concatenated streams |
O(s·n + m) | O(n + m) | Each stream after the first copies the rest of the input again; a BZ2File over the same bytes recopies at most one read buffer per stream |
BZ2Compressor¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bz2.BZ2Compressor(compresslevel=9) |
O(1) | O(1) | compresslevel sets B, and with it the working memory |
BZ2Compressor.compress(data) |
O(c + B) | O(c + B) | Returns b'' until a block fills; the call that fills one sorts it and returns its compressed bytes |
BZ2Compressor.flush() |
O(B) | O(B) | Compresses the last, partial block and ends the stream; the compressor cannot be used again |
BZ2Decompressor¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bz2.BZ2Decompressor() |
O(1) | O(1) | |
BZ2Decompressor.decompress(data, max_length=-1) |
O(h + c + r + B) | O(h + c + r) | r ≤ max_length when it is set; nothing comes out until a whole compressed block is in. Input left unconsumed is copied and held for the next call. One stream only: a call after eof raises EOFError |
BZ2Decompressor.eof, BZ2Decompressor.needs_input |
O(1) | O(1) | needs_input is False while max_length held back output |
BZ2Decompressor.unused_data |
O(1) | O(1) | Bytes after the end of the stream, copied when it is reached; b'' before |
BZ2File¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bz2.open(filename, mode='rb', compresslevel=9, encoding=None, errors=None, newline=None) |
O(1) | O(1) | Reads and writes nothing yet; text modes wrap the BZ2File in io.TextIOWrapper |
bz2.BZ2File(filename, mode='r', *, compresslevel=9) |
O(1) | O(1) | Binary only; closes filename on close() only if it opened it |
BZ2File.read(size=-1), BZ2File.read1(size=-1), BZ2File.readinto(b) |
O(r + B) | O(r) | A read that reaches a new block decompresses the whole block first; read() with no size is O(m). Concatenated streams are read in turn |
BZ2File.readline(size=-1), BZ2File.readlines(size=-1), iterating a BZ2File |
O(r + B) | O(r) | |
BZ2File.peek(n=0) |
O(B) | O(1) | Returns what is buffered, decompressing more only when nothing is; the size argument is ignored |
BZ2File.seek(offset, whence=io.SEEK_SET) |
O(k + B) | O(1) | Emulated by decompressing: forward from the current position, or from the start of the file for a backward seek past the read buffer. The first SEEK_END decompresses to the end to learn the size, which is then kept |
BZ2File.tell() |
O(1) | O(1) | Uncompressed position |
BZ2File.write(data), BZ2File.writelines(seq) |
O(c + B) | O(c + B) | c counts every item of seq. Output reaches the file only as blocks fill |
BZ2File.flush() |
O(1) | O(1) | Writes nothing: the last block reaches the file only on close() |
BZ2File.close() |
O(B) | O(B) | In write mode compresses the last block and ends the stream |
BZ2File.mode, BZ2File.name, BZ2File.closed, BZ2File.fileno(), BZ2File.readable(), BZ2File.writable(), BZ2File.seekable() |
O(1) | O(1) | mode is 'rb' or 'wb'; name is the underlying file's. mode and name are Python 3.13+ |
Blocks Are the Unit of Work¶
A compressor holds input until a block fills, so a small compress() call returns nothing and
the work arrives in lumps of B. A decompressor cannot reverse a block's sort until all of it has
arrived, so the first byte out of a block costs the whole block.
import bz2
import random
rng = random.Random(0)
words = [rng.choice(["alpha", "beta", "gamma", "delta"]) for _ in range(20_000)]
data = " ".join(words).encode()[:90_000] # one block at level 1
compressor = bz2.BZ2Compressor(1) # O(1) - B = 100 kB
assert compressor.compress(data[:50_000]) == b'' # O(c) - the block is not full yet
compressed = compressor.compress(data[50_000:]) + compressor.flush() # O(B) per block
decompressor = bz2.BZ2Decompressor()
assert decompressor.decompress(compressed[:-100]) == b'' # the block is incomplete
assert decompressor.decompress(compressed[-100:]) == data # O(B) - now it decodes
assert decompressor.eof
One-Shot vs Streaming¶
compress() and decompress() hold the whole result. A BZ2Compressor fed in chunks, or a
BZ2Decompressor asked for at most max_length bytes at a time, holds one chunk's worth plus
whatever the decompressor has not yet consumed.
import bz2
import io
data = b"log line\n" * 100_000
one_shot = bz2.compress(data) # O(m) time, O(n) memory
assert bz2.decompress(one_shot) == data # O(n + m) time, O(m) memory
compressor = bz2.BZ2Compressor() # O(1)
sink = io.BytesIO()
for start in range(0, len(data), 64_000):
sink.write(compressor.compress(data[start:start + 64_000])) # O(c + B)
sink.write(compressor.flush()) # O(B)
assert bz2.decompress(sink.getvalue()) == data
# Decompress a bomb without holding its output
bomb = bz2.compress(b"\0" * 10_000_000)
decompressor = bz2.BZ2Decompressor()
chunk = decompressor.decompress(bomb, max_length=4096) # O(n + 4096 + B)
assert chunk == b"\0" * 4096
assert decompressor.needs_input is False # more output is waiting
total = len(chunk)
while not decompressor.eof:
total += len(decompressor.decompress(b"", max_length=1_000_000)) # O(h + r + B)
assert total == 10_000_000
Concatenated Streams¶
A bzip2 file may hold several streams one after another: appending to a file adds one, and so
does concatenating two files. decompress() reads them all, but hands each new decompressor the
rest of the input as a fresh copy, so it costs O(s·n). A BZ2File reads the same bytes a buffer
at a time and recopies at most one buffer per stream.
import bz2
import io
target = io.BytesIO()
with bz2.BZ2File(target, "wb") as f:
f.write(b"first ")
with bz2.BZ2File(target, "ab") as f: # appends a second stream
f.write(b"second")
assert bz2.decompress(target.getvalue()) == b"first second" # O(s·n + m)
target.seek(0)
with bz2.BZ2File(target) as f:
assert f.read() == b"first second" # O(n + m) plus one buffer per stream
# One stream per BZ2Decompressor; the rest is left in unused_data
decompressor = bz2.BZ2Decompressor()
assert decompressor.decompress(target.getvalue()) == b"first "
assert decompressor.eof
assert bz2.decompress(decompressor.unused_data) == b"second" # O(1) to read the attribute
try:
decompressor.decompress(b"more")
except EOFError as error:
assert 'already reached' in str(error)
else:
raise AssertionError('a finished decompressor accepted more input')
Reading and Writing Files¶
Reading and Seeking¶
Opening reads nothing. A read decompresses as much as it returns, rounded up to the blocks it
touches, and seek() is emulated by decompressing, so a backward seek past the read buffer starts
again from the beginning.
import bz2
import io
data = b"".join(b"record %06d\n" % i for i in range(50_000))
source = io.BytesIO(bz2.compress(data))
with bz2.open(source) as f: # O(1) - nothing read yet
assert f.readline() == b"record 000000\n" # O(r + B)
f.seek(350_000) # O(k + B) - decompresses forward
assert f.read(14) == b"record 025000\n"
f.seek(14) # O(k + B) - backward: rewinds to the start
assert f.tell() == 14 # O(1)
assert f.read(14) == b"record 000001\n"
# Text mode wraps the same file object
source.seek(0)
with bz2.open(source, "rt", encoding="ascii") as f:
lines = sum(1 for _ in f) # O(m) time, one line held at a time
assert lines == 50_000
Writing¶
write() passes output on only when a block fills, and flush() does not change that: the last
block reaches the file when the BZ2File is closed.
import bz2
import io
target = io.BytesIO()
f = bz2.BZ2File(target, "wb", compresslevel=1) # O(1)
assert f.write(b"small record\n") == 13 # O(c) - held in the block
f.flush() # O(1) - writes nothing
assert target.getvalue() == b""
f.close() # O(B) - compresses the last block and ends the stream
assert bz2.decompress(target.getvalue()) == b"small record\n"
assert not target.closed # a file object passed in stays open
Common Patterns¶
Compressing a File in Chunks¶
import bz2
import io
import shutil
source = io.BytesIO(b"payload " * 500_000)
target = io.BytesIO()
with bz2.BZ2File(target, "wb") as f:
shutil.copyfileobj(source, f, 1024 * 1024) # O(m) time, one chunk held
target.seek(0)
with bz2.BZ2File(target) as f:
size = sum(len(chunk) for chunk in iter(lambda: f.read(1024 * 1024), b"")) # O(m)
assert size == 4_000_000
Performance Best Practices¶
✅ Do:
- Read a large file in chunks or lines, so memory follows the chunk rather than m
- Pass
max_lengthtoBZ2Decompressor.decompress()for untrusted input, since m can be far larger than n - Read files of many concatenated streams through
BZ2File, which recopies one buffer per stream wheredecompress()recopies the rest of the input - Lower
compresslevelto shrink B, and with it libbzip2's working memory and the cost of the first byte out of each block
❌ Avoid:
- Seeking backward past the read buffer of a
BZ2File- it decompresses again from the start - Expecting
flush()to make written data readable - onlyclose()ends the stream - Reading a few bytes at a time for low latency - each new block costs the whole block first
Version Notes¶
- Python 3.13+: Added
BZ2File.modeandBZ2File.name - Python 3.14+: The module is also available as
compression.bz2
Related Modules¶
- compression -
compression.bz2re-exports this module - gzip - DEFLATE files with the same file interface
- lzma - XZ and LZMA files with the same file interface
- zlib - DEFLATE streams without a file format
- tarfile - reads and writes
.tar.bz2archives through this module