AI Skill Report Card

Packing Binary Data with Struct

A88·Aug 14, 2026·Source: Web
14 / 15
Python
import struct # Pack: 1 unsigned short, 1 signed int, 4-byte string data = struct.pack('>Hi4s', 512, -42, b'ABCD') # Unpack back h, i, s = struct.unpack('>Hi4s', data) # h=512, i=-42, s=b'ABCD' # Calculate size before allocating buffers size = struct.calcsize('>Hi4s') # 10
Recommendation
Add a brief example showing error handling for struct.error on malformed input
14 / 15

Progress:

  • Identify the binary layout (field order, types, sizes) — from spec/docs/C struct definition
  • Choose byte order/alignment prefix
  • Build the format string
  • Pack or unpack; verify with calcsize
  • Handle variable-length data (strings, arrays) explicitly
  • Test round-trip (pack → unpack → compare)

1. Choose byte order & alignment prefix

PrefixByte orderSizeAlignment
@ (default)nativenativenative (padded)
=nativestandardnone
<little-endianstandardnone
>big-endianstandardnone
!network (big-endian)standardnone

Default recommendation: use < or > explicitly (never rely on @) when the data is persisted to disk or sent over a network — native alignment causes portability bugs.

2. Map format characters

CharC typePython typeSize
xpad bytenone1
ccharbytes (len 1)1
b/Bsigned/unsigned charint1
?_Boolbool1
h/Hshortint2
i/Iintint4
l/Llongint4
q/Qlong longint8
n/Nssize_t/size_tintnative only (@)
ehalf floatfloat2
ffloatfloat4
ddoublefloat8
schar[]bytesN (prefix count, e.g. 4s)
ppascal stringbytesN
Pvoid*intnative only

Repeat count prefix applies per-field (e.g. 4h = 4 shorts), except s/p where the count is the field length, not a repeat.

3. Pack / unpack

Python
struct.pack(fmt, v1, v2, ...) # -> bytes struct.unpack(fmt, buffer) # -> tuple, buffer size must match calcsize(fmt) struct.unpack_from(fmt, buffer, offset=0) # unpack from a larger buffer without slicing struct.pack_into(fmt, buffer, offset, *values) # write into an existing mutable buffer

4. Reuse compiled formats for performance

Python
Header = struct.Struct('<4sHHI') # magic, ver_major, ver_minor, length data = Header.pack(b'MAGC', 1, 0, 128) magic, vmaj, vmin, length = Header.unpack(data) print(Header.size) # equivalent to calcsize

Use Struct objects instead of repeated struct.pack(fmt, ...) calls when the same format is used many times (e.g. parsing many records in a loop).

Recommendation
Include a note on Python version differences (e.g., 'e' half-float format availability)
18 / 20

Example 1: Reading a fixed-size binary file header Input: File header spec — 4-byte magic, uint32 version (little-endian), uint32 payload length. Output:

Python
HEADER_FMT = '<4sII' HEADER_SIZE = struct.calcsize(HEADER_FMT) # 12 with open('file.bin', 'rb') as f: magic, version, length = struct.unpack(HEADER_FMT, f.read(HEADER_SIZE)) payload = f.read(length)

Example 2: Parsing a network packet with mixed types Input: Packet = 1 byte type, 1 byte flags, 2-byte big-endian sequence number, 8-byte big-endian timestamp. Output:

Python
PKT_FMT = '!BBHQ' pkt_type, flags, seq, ts = struct.unpack(PKT_FMT, raw_bytes)

Example 3: Packing an array of records Input: 3 points, each (x: float, y: float) little-endian. Output:

Python
fmt = '<' + 'ff' * 3 data = struct.pack(fmt, 0.0, 0.0, 1.5, 2.5, -1.0, 3.0)

Example 4: Unpacking from a larger buffer at an offset Input: A 1024-byte buffer where a 16-byte record starts at offset 100. Output:

Python
record = struct.unpack_from('<4sIII', buffer, offset=100)
Recommendation
Could add a table row/example demonstrating nested/compound struct parsing for complex records
  • Always specify byte order (<, >, !) explicitly for file/network formats; only use native @/no-prefix when interoperating with the local C compiler's ABI.
  • Use Struct objects for repeated pack/unpack in hot loops — avoids re-parsing the format string.
  • Validate len(buffer) == struct.calcsize(fmt) before calling unpack, or use unpack_from/pack_into with buffers of arbitrary size.
  • For variable-length strings, unpack the fixed header first, read the length field, then read the variable payload separately — struct itself only handles fixed-size fields.
  • Strip trailing null padding from s fields manually: s.rstrip(b'\x00').
  • Use bytearray/memoryview with pack_into/unpack_from to avoid unnecessary copies when processing large buffers.
  • Relying on native alignment (@) for cross-platform/file data — introduces padding bytes that differ across platforms/compilers, breaking round-trips.
  • Confusing s count with repeat count4s is one 4-byte bytes field, not four 1-byte fields; 4h is four shorts.
  • Forgetting calcsize mismatches — passing a buffer of the wrong length to unpack raises struct.error; always compute or check size.
  • Using n/N/P with non-native byte order prefixes — these are only valid with @, not </>/!/=.
  • Ignoring endianness of multi-byte fields from network sources — most network protocols are big-endian (! or >); using host byte order silently corrupts values.
  • Not stripping padding from fixed-width string fields, leading to comparisons failing against expected values.
0
Grade AAI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
14/15
Examples
18/20
Completeness
19/20
Format
14/15
Conciseness
14/15