Bitfields¶
Bitfields allow you to pack multiple boolean flags or small integer values into a single byte, word, or dword. This is essential for space-efficient binary formats and C struct compatibility.
Why Use Bitfields?¶
Traditional approach with separate booleans:
class Flags(StructModel):
read: bool # 1 byte
write: bool # 1 byte
execute: bool # 1 byte
# Total: 3 bytes for 3 flags
With bitfields:
class Flags(BitFieldModel):
read: bool = Bit(0)
write: bool = Bit(1)
execute: bool = Bit(2)
struct_config = StructConfig(bit_width=8)
# Total: 1 byte for 3 flags (5 bits unused)
Benefits: - Space efficient - Multiple flags in single byte/word/dword - C compatible - Matches C bitfield layouts - Type safe - Pydantic validation for each field - Readable - Named fields instead of bit manipulation
Basic Usage¶
Creating a BitFieldModel¶
from pdc_struct import BitFieldModel, StructConfig
from pdc_struct.models.bit_field import Bit
class FilePermissions(BitFieldModel):
read: bool = Bit(0) # Bit 0
write: bool = Bit(1) # Bit 1
execute: bool = Bit(2) # Bit 2
struct_config = StructConfig(bit_width=8) # 8, 16, or 32
# Create instance
perms = FilePermissions(read=True, write=True, execute=False)
# Access fields
print(perms.read) # True
print(perms.write) # True
print(perms.execute) # False
# Get packed value
print(bin(perms.packed_value)) # 0b11 (bits 0 and 1 set)
Bit Width¶
BitFieldModels must specify a bit width: 8, 16, or 32 bits.
# 8-bit (1 byte) - most common
class Flags8(BitFieldModel):
flag0: bool = Bit(0)
flag1: bool = Bit(1)
struct_config = StructConfig(bit_width=8)
# 16-bit (2 bytes) - for more flags
class Flags16(BitFieldModel):
flag0: bool = Bit(0)
# ... up to Bit(15)
flag15: bool = Bit(15)
struct_config = StructConfig(bit_width=16)
# 32-bit (4 bytes) - maximum flags
class Flags32(BitFieldModel):
flag0: bool = Bit(0)
# ... up to Bit(31)
flag31: bool = Bit(31)
struct_config = StructConfig(bit_width=32)
Multi-Bit Fields¶
You can pack small integers into bit ranges using multiple bit positions:
from pdc_struct import BitFieldModel, StructConfig
from pdc_struct.models.bit_field import Bit
class Status(BitFieldModel):
# Boolean flags
enabled: bool = Bit(0)
error: bool = Bit(1)
# 3-bit integer (0-7) using bits 2, 3, 4
priority: int = Bit(2, 3, 4)
# 2-bit integer (0-3) using bits 5, 6
state: int = Bit(5, 6)
struct_config = StructConfig(bit_width=8)
# Create with integer values
status = Status(
enabled=True,
error=False,
priority=5, # 0-7 (3 bits)
state=2 # 0-3 (2 bits)
)
print(f"Priority: {status.priority}") # 5
print(f"State: {status.state}") # 2
print(bin(status.packed_value)) # 0b01010101
Multi-Bit Field Rules¶
- Contiguous bits - Bit positions must be adjacent (e.g., 2, 3, 4)
- Value range - For N bits, values must be 0 to 2^N - 1
- Validation - Out-of-range values raise ValueError
class Config(BitFieldModel):
# 4-bit field: values 0-15
level: int = Bit(0, 1, 2, 3)
struct_config = StructConfig(bit_width=8)
# Valid
cfg = Config(level=15) # OK
# Invalid - out of range
try:
cfg = Config(level=16) # Error: 16 > 15 (max for 4 bits)
except ValueError as e:
print(f"Validation error: {e}")
Using BitFields in StructModel¶
BitFields integrate seamlessly with StructModels:
from pdc_struct import StructModel, BitFieldModel, StructConfig, StructMode
from pdc_struct.models.bit_field import Bit
from pdc_struct.c_types import UInt16
class TCPFlags(BitFieldModel):
"""TCP header flags (8 bits)."""
fin: bool = Bit(0)
syn: bool = Bit(1)
rst: bool = Bit(2)
psh: bool = Bit(3)
ack: bool = Bit(4)
urg: bool = Bit(5)
struct_config = StructConfig(bit_width=8)
class TCPHeader(StructModel):
"""Simplified TCP header."""
source_port: UInt16
dest_port: UInt16
sequence: UInt16
flags: TCPFlags # BitField packed as single byte
struct_config = StructConfig(mode=StructMode.C_COMPATIBLE)
# Create packet
packet = TCPHeader(
source_port=8080,
dest_port=443,
sequence=12345,
flags=TCPFlags(syn=True, ack=False, fin=False)
)
# Pack to bytes
data = packet.to_bytes()
print(len(data)) # 7 bytes: 2 + 2 + 2 + 1
# Unpack preserves bitfield structure
restored = TCPHeader.from_bytes(data)
print(restored.flags.syn) # True
print(restored.flags.ack) # False
Working with Packed Values¶
Reading Packed Value¶
flags = FilePermissions(read=True, write=False, execute=True)
value = flags.packed_value
print(f"Packed: 0x{value:02x}") # 0x05 (bits 0 and 2 set)
print(f"Binary: {bin(value)}") # 0b101
Setting Packed Value¶
# Create from packed value
flags = FilePermissions(packed_value=0x07) # All 3 bits set
print(flags.read) # True
print(flags.write) # True
print(flags.execute) # True
# Modify via packed_value property
flags.packed_value = 0x02 # Only bit 1 set
print(flags.read) # False
print(flags.write) # True
print(flags.execute) # False
From Bytes¶
# Create from bytes (useful when unpacking from binary data)
raw_bytes = b'\x05' # Binary data
flags = FilePermissions(packed_value=raw_bytes)
print(flags.read) # True (bit 0)
print(flags.execute) # True (bit 2)
Bit Numbering¶
Bits are numbered from 0 (least significant) to N-1 (most significant):
8-bit example: 0b11010110
││││││││
Bit positions: 76543210
Bit 0 (LSB): 0
Bit 1: 1
Bit 2: 1
Bit 3: 0
...
Bit 7 (MSB): 1
class Example(BitFieldModel):
bit0: bool = Bit(0) # Least significant bit (rightmost)
bit7: bool = Bit(7) # Most significant bit (leftmost in 8-bit)
struct_config = StructConfig(bit_width=8)
ex = Example(bit0=True, bit7=True)
print(bin(ex.packed_value)) # 0b10000001 (bits 0 and 7 set)
Optional BitFields¶
BitFields work with Optional in both C_COMPATIBLE and DYNAMIC modes:
C_COMPATIBLE Mode¶
from typing import Optional
from pdc_struct import StructModel, BitFieldModel, StructConfig, StructMode
from pdc_struct.models.bit_field import Bit
from pdc_struct.c_types import UInt8
class Flags(BitFieldModel):
read: bool = Bit(0)
write: bool = Bit(1)
struct_config = StructConfig(bit_width=8)
class Packet(StructModel):
msg_type: UInt8
flags: Optional[Flags] = Flags() # Must have default in C_COMPATIBLE
struct_config = StructConfig(mode=StructMode.C_COMPATIBLE)
# With flags
p1 = Packet(msg_type=1, flags=Flags(read=True, write=False))
# Without flags (uses default)
p2 = Packet(msg_type=1) # flags=Flags(read=False, write=False)
# Both pack to same size
print(len(p1.to_bytes())) # 2 bytes
print(len(p2.to_bytes())) # 2 bytes
DYNAMIC Mode¶
class Packet(StructModel):
msg_type: UInt8
flags: Optional[Flags] = None # Truly optional
struct_config = StructConfig(mode=StructMode.DYNAMIC)
# With flags
p1 = Packet(msg_type=1, flags=Flags(read=True))
data1 = p1.to_bytes()
# Without flags
p2 = Packet(msg_type=1, flags=None)
data2 = p2.to_bytes()
# Different sizes!
print(len(data1)) # Larger (includes flags)
print(len(data2)) # Smaller (flags omitted)
# Roundtrip preserves None
restored = Packet.from_bytes(data2)
assert restored.flags is None
Common Patterns¶
File Permissions (Unix-style)¶
class UnixPermissions(BitFieldModel):
"""Unix file permissions (9 bits)."""
# Owner
owner_read: bool = Bit(0)
owner_write: bool = Bit(1)
owner_execute: bool = Bit(2)
# Group
group_read: bool = Bit(3)
group_write: bool = Bit(4)
group_execute: bool = Bit(5)
# Others
other_read: bool = Bit(6)
other_write: bool = Bit(7)
other_execute: bool = Bit(8)
struct_config = StructConfig(bit_width=16) # 9 bits needs 16-bit width
# chmod 755 = rwxr-xr-x
perms = UnixPermissions(
owner_read=True, owner_write=True, owner_execute=True,
group_read=True, group_write=False, group_execute=True,
other_read=True, other_write=False, other_execute=True
)
print(f"0o{perms.packed_value:o}") # 0o755
Device Control Register¶
class DeviceControl(BitFieldModel):
"""Hardware device control register (16 bits)."""
enable: bool = Bit(0)
reset: bool = Bit(1)
interrupt_enable: bool = Bit(2)
mode: int = Bit(3, 4, 5) # 3 bits: 0-7 for mode
baudrate: int = Bit(6, 7, 8, 9) # 4 bits: 0-15 for rate
parity: int = Bit(10, 11) # 2 bits: 0=none, 1=odd, 2=even
struct_config = StructConfig(bit_width=16)
control = DeviceControl(
enable=True,
reset=False,
interrupt_enable=True,
mode=3,
baudrate=9,
parity=2
)
Status Register with Reserved Bits¶
class Status(BitFieldModel):
"""Device status with reserved bits."""
ready: bool = Bit(0)
busy: bool = Bit(1)
error: bool = Bit(2)
# Bits 3-5 reserved (leave undefined)
overflow: bool = Bit(6)
underflow: bool = Bit(7)
struct_config = StructConfig(bit_width=8)
# Reserved bits remain 0
status = Status(ready=True, busy=False, error=False,
overflow=False, underflow=False)
Cloning BitFields¶
The clone() method creates a copy with selective updates:
flags = FilePermissions(read=True, write=False, execute=True)
# Clone with modifications
new_flags = flags.clone(write=True)
print(flags.write) # False (original unchanged)
print(new_flags.write) # True (clone modified)
print(new_flags.read) # True (copied from original)
Validation¶
BitFieldModels validate at creation:
class Config(BitFieldModel):
level: int = Bit(0, 1, 2) # 3 bits: 0-7
struct_config = StructConfig(bit_width=8)
# Valid
c1 = Config(level=5) # OK: 5 is in range 0-7
# Invalid - out of range
try:
c2 = Config(level=8) # Error: 8 > 7 (max for 3 bits)
except ValueError:
print("Out of range!")
# Invalid - wrong type
try:
c3 = Config(level=True) # Error: bool not allowed for int field
except ValueError:
print("Wrong type!")
Byte Order¶
BitFields respect the struct_config byte order when used in StructModels:
from pdc_struct import ByteOrder
class Flags(BitFieldModel):
flag0: bool = Bit(0)
flag1: bool = Bit(1)
struct_config = StructConfig(bit_width=16)
class Packet(StructModel):
flags: Flags
struct_config = StructConfig(
mode=StructMode.C_COMPATIBLE,
byte_order=ByteOrder.BIG_ENDIAN # Affects byte order of 16-bit flags
)
Best Practices¶
- Use descriptive names -
is_validnotflag3 - Document bit positions - Add comments for hardware registers
- Group related flags - One BitFieldModel per logical register
- Match hardware specs - Use same bit numbering as datasheets
- Use appropriate width - 8 bits for most cases, 16/32 when needed
- Validate ranges - Test boundary conditions for multi-bit fields
Limitations¶
- Bit width must be 8, 16, or 32 - No arbitrary widths
- Bits must be contiguous - Multi-bit fields can't have gaps
- No bit reuse - Each bit can only be assigned once
- No dynamic bit positions - Positions must be compile-time constants
Summary¶
BitFieldModels provide space-efficient binary encoding:
- Pack multiple flags into single bytes
- Define multi-bit integer fields for small value ranges
- Integrate seamlessly with StructModel
- Type-safe with Pydantic validation
- C-compatible for hardware/protocol work
For more information:
- Types - Other supported types
- Nested Structs - Composing complex structures
- Modes - C_COMPATIBLE vs DYNAMIC behavior