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.