Upgrading to version 5#
This guide is for code being moved from bitstring 4.4.x to bitstring 5.x.
If your project is using bitstring and performance isn’t an issue then the best course of action may be to ignore version 5 and pin your bitstring dependency to <5.
The main change in version 5 is that bitstrings no longer store a stream
position. The bit data is represented by Bits or BitArray,
and sequential reading is handled by a separate Reader.
Minimum Python version#
bitstring 5 requires Python 3.10 or later. If your package metadata pins bitstring and Python versions, update both together, for example:
requires-python = ">=3.10"
dependencies = ["bitstring>=5"]
The bitarray dependency has also been removed. The core bit storage is now
provided by the required tibs dependency. If there are problems downloading, building or
installing tibs then please file a bug report with either project.
Replace stream classes with Reader#
The ConstBitStream and BitStream classes have been removed.
For immutable data, wrap a Bits object in a Reader:
# bitstring 4
s = ConstBitStream("0x160120f")
value = s.read("uint12")
# bitstring 5
r = Reader(Bits("0x160120f"))
value = r.read("u12")
For mutable data, wrap a BitArray. The wrapped object is available as
Reader.bits, and it is the original object rather than a copy:
# bitstring 4
s = BitStream("0x001122")
first = s.read("uint8")
s.append("0xff")
# bitstring 5
r = Reader(BitArray("0x001122"))
first = r.read("u8")
r.bits.append("0xff")
The reader position is independent of the wrapped bitstring. Mutating
r.bits does not automatically adjust Reader.pos.
Operations that used the stream’s current position should now pass
r.pos explicitly and then update it if needed:
# bitstring 4
s = BitStream("0x001122")
s.pos = 8
s.insert("0xff")
# bitstring 5
r = Reader(BitArray("0x001122"), pos=8)
inserted = Bits("0xff")
r.bits.insert(r.pos, inserted)
r.pos += len(inserted)
Reader.pos is deliberately lax. Assigning to pos
or bytepos stores the integer value
without checking it against the current length. A later read or search will
raise an error if the position cannot be used.
Update pack() usage#
pack now returns Bits. In version 4 it returned BitStream,
so code that immediately read from or mutated the result needs to be updated.
For reading, wrap the result in Reader:
# bitstring 4
s = pack("uint8, uint8", 1, 2)
first = s.read("uint8")
# bitstring 5
bits = pack("u8, u8", 1, 2)
r = Reader(bits)
first = r.read("u8")
For mutation, convert the result to BitArray:
# bitstring 4
s = pack("uint8", 1)
s.append("0xff")
# bitstring 5
s = pack("u8", 1).to_bitarray()
s.append("0xff")
Update find() and rfind() checks#
Bits.find, Bits.rfind, Reader.find and
Reader.rfind now return int | None. In version 4 they returned a
single-item tuple for success and an empty tuple for failure.
This means a match at bit position zero evaluates as False if tested
directly. Test explicitly against None:
# bitstring 4
found = s.find("0xff")
if found:
pos = found[0]
# bitstring 5
pos = s.find("0xff")
if pos is not None:
...
If you used stream searching to move the current position, use
Reader:
# bitstring 4
if s.find("0xff", start=s.pos):
print(s.pos)
# bitstring 5
if r.find("0xff", start=r.pos) is not None:
print(r.pos)
Remove reliance on range checking exceptions#
Methods taking start and end arguments no longer raise a
ValueError for out of range values. Instead the values are clamped to the
ends of the bitstring, in the same way as slice indices and the equivalent
str methods, with an end before the start giving an empty range.
This applies to find, rfind, findall, cut, split,
startswith, endswith, replace, reverse, rol, ror and
byteswap:
# bitstring 4
s = Bits("0x0123")
s.find("0x1", start=0, end=100) # Raised ValueError
# bitstring 5
s.find("0x1", start=0, end=100) # end is clamped to len(s), finds 4
Code that relied on catching these exceptions to detect out of range values
should check the positions against len(s) explicitly instead.
Swap insert() and overwrite() arguments#
BitArray.insert and BitArray.overwrite now take the bit
position first and the bitstring second, matching list.insert,
array.array.insert and Array.insert:
# bitstring 4
s.insert("0xff", 8)
s.overwrite("0xff", 8)
# bitstring 5
s.insert(8, "0xff")
s.overwrite(8, "0xff")
Calls using the old positional order raise a TypeError explaining the
change. Calls that already used keywords (s.insert(bs=..., pos=...)) work
unchanged.
Use explicit construction helpers#
Some constructor forms that relied on the type of the first positional argument have been removed or should be avoided. Use the explicit factory methods instead.
For zero-filled bitstrings, replace integer construction with
Bits.from_zeros or BitArray.from_zeros. This applies to the
length-only keyword form as well:
# bitstring 4
a = Bits(100)
b = BitArray(100)
c = BitArray(length=100)
# bitstring 5
a = Bits.from_zeros(100)
b = BitArray.from_zeros(100)
c = BitArray.from_zeros(100)
Prefer Bits.from_string or BitArray.from_string over the old
fromstring spelling. The old spelling still works as a compatibility
alias.:
# bitstring 4
s = Bits.fromstring("uint16=1000")
t = BitArray.fromstring("0xff")
# bitstring 5
s = Bits.from_string("u16=1000")
t = BitArray.from_string("0xff")
Lists and tuples containing only 0, 1, True and False can still
be used as the first positional argument. Use the other factory methods for
construction from values that are no longer accepted:
# bitstring 4
f = open("data.bin", "rb")
b = Bits(f)
f.close()
c = Bits(io.BytesIO(b"\x01\x02"))
d = Bits(array_obj)
# bitstring 5
with open("data.bin", "rb") as f:
b = Bits.from_file(f)
c = Bits.from_bytes(b"\x01\x02")
d = Bits.from_bytes(array_obj.tobytes())
The bytes= keyword constructor and constructor-level offset= keyword
have also been removed. Use Bits.from_bytes or
BitArray.from_bytes instead:
# bitstring 4
s = Bits(bytes=b"\x0b\x1c\x2f", offset=4, length=12)
# bitstring 5
s = Bits.from_bytes(b"\x0b\x1c\x2f", offset=4, length=12)
The filename= keyword constructor has been removed. Use
Bits.from_file or BitArray.from_file instead:
# bitstring 4
s = Bits(filename="data.bin", offset=8, length=32)
# bitstring 5
s = Bits.from_file("data.bin", offset=8, length=32)
Use explicit Array construction#
The Array constructor now only accepts an iterable of values (such as
a list, another Array or an array.array). The integer item count, raw
binary data and file object initialiser forms have been removed, as they were
ambiguous with iterables of values - for example Array('u8', b'\x01\x02')
meant something different to Array('u8', [1, 2]) even though bytes is
an iterable of integers. Use the explicit alternatives instead:
# bitstring 4
a = Array("u8", 100)
b = Array("u8", b"some_bytes")
c = Array("u8", open("data.bin", "rb"))
# bitstring 5
a = Array.from_zeros("u8", 100)
b = Array.from_bytes("u8", b"some_bytes")
c = Array.from_file("u8", "data.bin")
Array.from_file is now a constructor taking the dtype as its first
argument, like the other from_ methods. The bitstring 4 instance method of
the same name (and its fromfile() alias) that appended items to an existing
Array has been removed:
# bitstring 4
a = Array("u8")
a.fromfile(f, 10)
# bitstring 5
a = Array.from_file("u8", f, 10)
To append file data to an existing Array, extend from a newly read one,
for example a.extend(Array.from_file(a.dtype, f)).
Check file object usage in from_file()#
When Bits.from_file or BitArray.from_file is given a file
object it now reads from the object’s current file position, where previously
the position was ignored and the whole file was used. Passing an in-memory
stream such as io.BytesIO now raises a TypeError - use
Bits.from_bytes for those instead. Array.from_file also reads
file objects from their current position.
Replace bitarray compatibility#
If you used the external bitarray package, convert explicitly. For exact
bit lengths, Bits.from_bools is the most direct replacement. For
byte-oriented data, use Bits.from_bytes with an explicit length:
bits = Bits.from_bools(bitarray_obj)
bits = Bits.from_bytes(bitarray_obj.tobytes(), length=len(bitarray_obj))
The old tobitarray() method returned an object from the external
bitarray package and has been removed. Bits.to_bitarray returns a
bitstring BitArray instead. If you still need an external
bitarray object, create it explicitly using that package’s API, for example
from the bitstring as an iterable of booleans or from Bits.to_bytes.
Update names and dtype spellings#
Version 5 removes a few legacy aliases and makes the short dtype names the canonical spelling.
Version 4 spelling |
Version 5 spelling |
|---|---|
|
|
|
|
|
|
|
Prefer |
|
Prefer |
Native-endian names such as |
Use explicit big- or little-endian spellings such as |
Struct-style native prefixes |
Use |
The short numeric names u, i and f remain valid. The plain f
dtype is already big-endian, so both fbe and floatbe are compatibility
aliases for f.
Dtype stringification, Array representations and
pretty-print headers use the preferred names:
# bitstring 4
n = s.length
data = s.h
bits = s.unpack("b12, uint8")
value = r.read("floatle32")
# bitstring 5
n = len(s)
data = s.hex
bits = s.unpack("bin12, u8")
value = r.read("fle32")
Dtype.build and Dtype.parse have been renamed to
Dtype.pack and Dtype.unpack:
# bitstring 4
d = Dtype("uint8")
bits = d.build(42)
value = d.parse(bits)
# bitstring 5
d = Dtype("u8")
bits = d.pack(42)
value = d.unpack(bits)
Replace global options and modes#
The bitstring.options object and the old module-level option aliases have
been removed. Version 5 uses explicit dtype names, per-call arguments and the
NO_COLOR environment variable instead of mutable process-wide state.
Version 4 setting |
Version 5 replacement |
|---|---|
|
Removed. Indexing is always MSB0. |
|
Use an explicit MXFP dtype suffix such as |
|
Pass |
|
Code that enabled LSB0 mode needs to translate positions explicitly. For a
single bit position i in a bitstring s, the equivalent MSB0 position is
len(s) - 1 - i. Slice translations depend on the direction and bounds of
the original slice, so they should be reviewed case by case.
For MXFP packing, the overflow policy is now part of the dtype name. Use
e4m3mxfp_saturate or e5m2mxfp_saturate for saturating behaviour, and
e4m3mxfp_overflow or e5m2mxfp_overflow for overflow behaviour. The old
unsuffixed e4m3mxfp and e5m2mxfp names have been removed.
# bitstring 4
bitstring.options.mxfp_overflow = "overflow"
bits = Bits(e4m3mxfp=1e10)
bitstring.bytealigned = True
pos = bits.find("0xff")
bitstring.options.no_color = True
bits.pp()
# bitstring 5
bits = Bits(e4m3mxfp_overflow=1e10)
pos = bits.find("0xff", bytealigned=True)
bits.pp(color=False)
Prefer the new underscored method names#
Version 5 adds underscored spellings for several older method names. The old names remain as deprecated compatibility aliases and are expected to be removed in a future major version, so new code and documentation should use the underscored names.
Version 4 spelling |
Preferred version 5 spelling |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
Remove command-line usage#
The python -m bitstring command-line interface has been removed. Use a
small Python script or shell one-liner for any remaining uses.
Suggested upgrade order#
For a large codebase, the least surprising order is:
Update imports to remove
ConstBitStreamandBitStream.Introduce
Readerwherever code usespos,read,peekor stream-style searching.Update
packcall sites that relied on the oldBitStreamreturn value.Change
findandrfindchecks to useis not None.Replace
bytes=,filename=and other removed constructor forms with explicit factory methods.Replace direct
bitarraycompatibility with explicit conversion.Replace removed aliases, prefer current dtype names, and rename
Dtype.build/Dtype.parse.Replace removed global options and modes with explicit dtypes or per-call arguments.
Remove any remaining
python -m bitstringusage.Optionally update compatibility aliases such as
tobytesandreadlistto their preferred underscored names.