struct: Native Format Padding Silently Breaks unpack() (tested)
A struct format string with no byte-order character, or an explicit @, uses the platform's native size and alignment -- which means it can silently insert padding bytes between fields that don't exist in <, >, = or ! (standard, no-padding) formats. We measured what that padding actually does to the byte layout and to unpacking.
1. The same format string, with and without a byte-order prefix, isn't the same size
import struct
native = struct.pack('bi', 1, 2) # no prefix = native (same as '@')
standard = struct.pack('=bi', 1, 2) # explicit standard, no padding
print(len(native), native.hex())
print(len(standard), standard.hex())
print(struct.calcsize('bi'), struct.calcsize('=bi'))
8 0100000002000000
5 0102000000
8 5
Measured: packing a byte then an int with no prefix produces 8 bytes -- 3 invisible padding bytes get inserted after the byte so the int starts on a 4-byte-aligned offset, which is how C structs are laid out on this platform. The =bi version has no alignment rules and packs exactly 5 bytes: 1 for the byte, 4 for the int, nothing else. Nothing in the format string 'bi' itself hints that 3 extra bytes exist.
2. Unpacking native-packed bytes with a standard prefix fails on size, not on content
import struct
packed = struct.pack('bi', 1, 2) # 8 bytes, native padding included
try:
struct.unpack('=bi', packed)
except struct.error as e:
print("error:", e)
error: unpack requires a buffer of 5 bytes
Measured: the error says nothing about padding or alignment -- it just reports a buffer-size mismatch (8 bytes supplied, 5 expected), which reads like the format string is wrong rather than like a byte-order/padding mismatch between writer and reader. Debugging this by staring at the format string 'bi'/'=bi' alone won't reveal the real cause.
3. When sizes happen to line up anyway, unpack succeeds with a silent phantom field
import struct
packed = struct.pack('hi', 7, 99999) # native: short, 2 pad bytes, int = 8 bytes
print(len(packed), packed.hex())
print(struct.calcsize('
8 070000009f860100
8
(7, 0, 99999)
Measured: struct.calcsize('<hhi') happens to equal the native-padded size of 'hi', so unpack('<hhi', packed) raises no error at all -- it just returns three values instead of two. The middle value, 0, is the native alignment padding being silently reinterpreted as a real short field. Any code expecting exactly the two values it originally packed gets a plausible-looking but wrong three-value result instead.
Frequently asked questions
Does struct.pack() add padding bytes by default?
Yes, when the format string has no byte-order prefix (or uses '@' explicitly) -- we measured 'bi' producing 8 bytes instead of the 5 you'd get from the field sizes alone, because 3 bytes of native alignment padding get inserted before the int.
Why does struct.unpack() fail with a buffer-size error that doesn't mention padding?
Because struct.error only reports the byte-count mismatch between what calcsize() expects and what you supplied -- we measured the exact message ('unpack requires a buffer of 5 bytes') giving no hint that native alignment padding, not a wrong format string, is the real cause.
Can struct.unpack() succeed but return the wrong values because of native padding?
Yes -- we measured packing 'hi' (short, int) natively into 8 padded bytes, then unpacking those same bytes with '<hhi' (3 unpadded fields totalling the same 8 bytes): it succeeded and returned (7, 0, 99999), silently turning the 2 padding bytes into a bogus extra field.
How do you avoid struct's native padding footgun?
Always start the format string with an explicit '<', '>', '=' or '!' for any data you serialize, store, or send across a process/network boundary -- we measured those four prefixes never inserting padding, unlike no-prefix or '@', which should be reserved for in-process interop with C structs on the same machine.
Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in struct module on x86-64 Linux: packed values with the native format (no prefix, same as '@') and unpacked the same bytes with explicit byte-order prefixes ('<', '=') to compare sizes and results. PlainHeaders is our extension; Python's struct module is built in.