Python’s struct module packs and unpacks C-style binary data. import struct. pack: struct.pack(">HI", 1, 256) → bytes. unpack: struct.unpack(">HI", data) → tuple. pack_into: struct.pack_into(fmt, buf, offset, *values) — write into bytearray. unpack_from: struct.unpack_from(fmt, buf, offset=0) — read from buffer. calcsize: struct.calcsize(">HI") → 6. Struct: s = struct.Struct(">HI"); s.pack(1, 256); s.unpack(data) — compiled, reusable. iter_unpack: for vals in struct.iter_unpack(">H", data): — stream records. Byte order: > big-endian (network); < little-endian; = native; ! network (= big). Format chars: B uint8, b int8, H uint16, h int16, I uint32, i int32, Q uint64, q int64, f float32, d float64, ? bool, c char (1 byte), s string bytes (e.g. 4s), x padding byte, p pascal string. struct.error: raised on size mismatch or bad format. peek via unpack_from with offset. Checksum: pack CRC after data: struct.pack(">I", zlib.crc32(data) & 0xFFFFFFFF). Claude Code generates binary protocol parsers, file format readers, network frame decoders, and BLE/serial packet builders.
CLAUDE.md for struct
## struct Stack
- Stdlib: import struct
- Pack: struct.pack(">BHI", uint8, uint16, uint32) — > = big-endian
- Unpack: struct.unpack(">BHI", data) — returns tuple
- Reuse: s = struct.Struct(fmt); s.pack(*values); s.unpack(data)
- Size: struct.calcsize(fmt) — bytes needed
- Stream: for (val,) in struct.iter_unpack(">H", data):
- Offset: struct.unpack_from(fmt, buf, offset=n)
struct Binary Protocol Pipeline
# app/binutil.py — pack, unpack, Struct, protocols, file formats, checksums
from __future__ import annotations
import io
import struct
import zlib
from dataclasses import dataclass
from typing import Any, BinaryIO, Iterator
# ─────────────────────────────────────────────────────────────────────────────
# 1. Low-level pack/unpack helpers
# ─────────────────────────────────────────────────────────────────────────────
def pack_uint8(value: int) -> bytes:
"""Pack a single unsigned byte."""
return struct.pack("B", value)
def pack_uint16_be(value: int) -> bytes:
"""Pack an unsigned 16-bit integer, big-endian."""
return struct.pack(">H", value)
def pack_uint32_be(value: int) -> bytes:
"""Pack an unsigned 32-bit integer, big-endian."""
return struct.pack(">I", value)
def pack_float32_be(value: float) -> bytes:
"""Pack a 32-bit float, big-endian."""
return struct.pack(">f", value)
def unpack_uint16_be(data: bytes, offset: int = 0) -> int:
"""Unpack a big-endian uint16 from data at offset."""
(val,) = struct.unpack_from(">H", data, offset)
return val
def unpack_uint32_be(data: bytes, offset: int = 0) -> int:
"""Unpack a big-endian uint32 from data at offset."""
(val,) = struct.unpack_from(">I", data, offset)
return val
def read_cstring(data: bytes, offset: int = 0) -> tuple[str, int]:
"""
Read a null-terminated C string from data at offset.
Returns (string, new_offset).
Example:
s, end = read_cstring(b"hello\x00world\x00", 0) # ("hello", 6)
"""
end = data.index(b"\x00", offset)
return data[offset:end].decode("utf-8", errors="replace"), end + 1
# ─────────────────────────────────────────────────────────────────────────────
# 2. Reusable Struct objects
# ─────────────────────────────────────────────────────────────────────────────
# Common network/binary primitives
UINT8_S = struct.Struct("B")
UINT16_S = struct.Struct(">H")
UINT32_S = struct.Struct(">I")
UINT64_S = struct.Struct(">Q")
FLOAT32_S = struct.Struct(">f")
FLOAT64_S = struct.Struct(">d")
def iter_uint16_be(data: bytes) -> Iterator[int]:
"""
Iterate over big-endian uint16 values packed densely in data.
Example:
values = list(iter_uint16_be(b"\\x00\\x01\\x00\\x02\\x00\\x03")) # [1, 2, 3]
"""
for (val,) in struct.iter_unpack(">H", data):
yield val
def iter_uint32_be(data: bytes) -> Iterator[int]:
"""Iterate over big-endian uint32 dense array."""
for (val,) in struct.iter_unpack(">I", data):
yield val
def pack_array(values: list[int | float], fmt_char: str = "H", order: str = ">") -> bytes:
"""
Pack a list of same-type values as a dense binary array.
Example:
packet = pack_array([100, 200, 300], fmt_char="H", order=">")
"""
n = len(values)
return struct.pack(f"{order}{n}{fmt_char}", *values)
def unpack_array(data: bytes, fmt_char: str = "H", order: str = ">") -> list:
"""
Unpack a dense binary array of same-type values.
Example:
values = unpack_array(data, fmt_char="H", order=">")
"""
size = struct.calcsize(fmt_char)
n = len(data) // size
return list(struct.unpack(f"{order}{n}{fmt_char}", data[:n * size]))
# ─────────────────────────────────────────────────────────────────────────────
# 3. Framed binary protocol
# ─────────────────────────────────────────────────────────────────────────────
# Frame layout: [magic: 4s][version: B][msg_type: B][length: I][payload: Ns][crc32: I]
FRAME_HEADER = struct.Struct(">4sBBI")
FRAME_CRC = struct.Struct(">I")
FRAME_MAGIC = b"MYPR" # my protocol
@dataclass
class Frame:
"""
A binary protocol frame.
Example:
frame = Frame(version=1, msg_type=2, payload=b'{"event":"click"}')
wire = Frame.encode(frame)
received = Frame.decode(wire)
"""
version: int
msg_type: int
payload: bytes
@staticmethod
def encode(frame: "Frame") -> bytes:
"""
Serialize frame to wire bytes.
Example:
wire = Frame.encode(Frame(version=1, msg_type=0x01, payload=b"ping"))
"""
hdr = FRAME_HEADER.pack(FRAME_MAGIC, frame.version, frame.msg_type, len(frame.payload))
body = hdr + frame.payload
crc = FRAME_CRC.pack(zlib.crc32(body) & 0xFFFFFFFF)
return body + crc
@staticmethod
def decode(data: bytes) -> "Frame":
"""
Deserialize a frame from bytes; raises ValueError on CRC mismatch.
Example:
frame = Frame.decode(wire_bytes)
"""
hdr_size = FRAME_HEADER.size
magic, version, msg_type, length = FRAME_HEADER.unpack_from(data, 0)
if magic != FRAME_MAGIC:
raise ValueError(f"Bad magic: {magic!r}")
payload = data[hdr_size: hdr_size + length]
crc_off = hdr_size + length
(received_crc,) = FRAME_CRC.unpack_from(data, crc_off)
computed_crc = zlib.crc32(data[:crc_off]) & 0xFFFFFFFF
if received_crc != computed_crc:
raise ValueError(f"CRC mismatch: {received_crc:#010x} != {computed_crc:#010x}")
return Frame(version=version, msg_type=msg_type, payload=payload)
@staticmethod
def read_from(stream: BinaryIO) -> "Frame":
"""
Read one frame from a binary stream.
Example:
with open("capture.bin", "rb") as f:
while True:
try: frame = Frame.read_from(f)
except EOFError: break
"""
hdr_bytes = stream.read(FRAME_HEADER.size)
if len(hdr_bytes) < FRAME_HEADER.size:
raise EOFError("End of stream")
magic, version, msg_type, length = FRAME_HEADER.unpack(hdr_bytes)
payload = stream.read(length)
crc_bytes = stream.read(FRAME_CRC.size)
(received_crc,) = FRAME_CRC.unpack(crc_bytes)
body = hdr_bytes + payload
computed = zlib.crc32(body) & 0xFFFFFFFF
if received_crc != computed:
raise ValueError("CRC mismatch")
return Frame(version=version, msg_type=msg_type, payload=payload)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Simple binary file format reader
# ─────────────────────────────────────────────────────────────────────────────
# WAV-lite: simplified RIFF-like PCM reader (fixed format, mono, 16-bit)
WAV_HEADER = struct.Struct("<4sI4s4sIHHIIHH4sI") # RIFF/WAVE/fmt /data headers
WAV_EXPECTED = b"RIFF", b"WAVE"
@dataclass
class PCMInfo:
num_channels: int
sample_rate: int
bits_per_sample: int
num_frames: int
duration_s: float
def parse_wav_header(data: bytes) -> PCMInfo:
"""
Parse a minimal WAV header (PCM, any channel count).
Raises ValueError on invalid format.
Example:
with open("audio.wav", "rb") as f:
info = parse_wav_header(f.read(44))
"""
if len(data) < WAV_HEADER.size:
raise ValueError("Too short for WAV header")
fields = WAV_HEADER.unpack_from(data, 0)
riff_id, _, wave_id, fmt_id, _, audio_fmt, channels, rate, _, _, bps, data_id, data_size = fields
if riff_id != b"RIFF" or wave_id != b"WAVE":
raise ValueError(f"Not a RIFF/WAVE: {riff_id!r}/{wave_id!r}")
if audio_fmt != 1:
raise ValueError(f"Only PCM (format 1) supported, got {audio_fmt}")
frame_size = channels * (bps // 8)
num_frames = data_size // frame_size if frame_size else 0
return PCMInfo(
num_channels=channels,
sample_rate=rate,
bits_per_sample=bps,
num_frames=num_frames,
duration_s=num_frames / rate if rate else 0.0,
)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== struct demo ===")
print("\n--- pack / unpack round-trip ---")
fmt = ">BHIf"
size = struct.calcsize(fmt)
vals = (7, 1000, 99999, 3.14)
packed = struct.pack(fmt, *vals)
print(f" values: {vals} → {len(packed)} bytes: {packed.hex()}")
unpacked = struct.unpack(fmt, packed)
print(f" unpacked: {unpacked}")
print(f" float eq: {abs(unpacked[3] - 3.14) < 0.001}")
print("\n--- pack_array / unpack_array ---")
sensor_data = [100, 200, 150, 300, 250]
wire = pack_array(sensor_data, fmt_char="H")
print(f" packed {len(sensor_data)} uint16: {wire.hex()}")
back = unpack_array(wire, fmt_char="H")
print(f" unpacked: {back}")
print("\n--- iter_uint16_be ---")
counts = list(iter_uint16_be(wire))
print(f" iter_uint16_be: {counts}")
print("\n--- Frame encode / decode ---")
frame = Frame(version=1, msg_type=0x01, payload=b'{"event":"ping"}')
wire_frame = Frame.encode(frame)
print(f" encoded: {len(wire_frame)} bytes, hex[:20]={wire_frame[:20].hex()}")
decoded = Frame.decode(wire_frame)
print(f" decoded: version={decoded.version} type={decoded.msg_type} payload={decoded.payload!r}")
print("\n--- Frame CRC tamper detection ---")
tampered = bytearray(wire_frame)
tampered[10] ^= 0xFF
try:
Frame.decode(bytes(tampered))
except ValueError as e:
print(f" caught: {e}")
print("\n--- Frame.read_from stream ---")
buf = io.BytesIO()
for msg_type, payload in [(1, b"ping"), (2, b"data"), (3, b"bye")]:
buf.write(Frame.encode(Frame(version=1, msg_type=msg_type, payload=payload)))
buf.seek(0)
while True:
try:
f = Frame.read_from(buf)
print(f" read frame type={f.msg_type} payload={f.payload!r}")
except EOFError:
break
print("\n--- read_cstring ---")
raw = b"hello\x00world\x00!"
s1, off = read_cstring(raw, 0)
s2, _ = read_cstring(raw, off)
print(f" strings: {s1!r}, {s2!r}")
print("\n--- WAV header parse ---")
# Synthesize a minimal valid 44-byte WAV header (mono, 44100 Hz, 16-bit, 100 frames)
data_size = 100 * 1 * 2 # frames * channels * bytes_per_sample
wav_hdr = struct.pack(
"<4sI4s4sIHHIIHH4sI",
b"RIFF", 36 + data_size, b"WAVE",
b"fmt ", 16, 1, 1, 44100, 44100*2, 2, 16,
b"data", data_size,
)
info = parse_wav_header(wav_hdr)
print(f" {info}")
print("\n=== done ===")
For the construct alternative — construct (PyPI, claude-code-construct) provides a declarative DSL for describing binary formats using Python objects: Struct("header" / Bytes(4), "length" / Int32ub) — it auto-generates parsers and builders and handles nested structures, arrays, conditional fields, and checksums; stdlib struct requires manual format strings and offsetting — use construct for complex multi-layered binary protocols (TLS records, ELF sections, custom filesystems) where documenting the format as code is valuable, stdlib struct for simple fixed-layout records, performance-critical inner loops, and zero-dependency environments. For the ctypes alternative — ctypes.Structure maps C struct layouts including bitfields, pointers, and platform-aligned types, and interoperates directly with C shared libraries via ctypes.CDLL; stdlib struct is pure Python with no C interop — use ctypes.Structure when calling native C libraries, interfacing with OS APIs (ioctl, Windows WINAPI), or needing C bitfield semantics, stdlib struct for portable binary file I/O and network protocols where C interop is not required. The Claude Skills 360 bundle includes struct skill sets covering pack_uint8/16/32/float helpers, iter_uint16_be/iter_uint32_be streaming unpacking, pack_array()/unpack_array() dense array helpers, Frame dataclass with encode()/decode()/read_from() binary protocol implementation with CRC32 validation, and parse_wav_header() example file format reader. Start with the free tier to try binary protocol parsing and struct pipeline code generation.