Parsing Binary Formats with Construct

construct is a Python library that lets you describe binary file layouts as data structures instead of writing manual struct.unpack calls. It is especially useful for reverse engineering and parsing proprietary formats like game archives, packet captures, and firmware headers.

Core idea

Instead of reading byte-by-byte and unpacking each field, you define a container that describes the layout. construct then reads bytes from the start and maps them onto named fields.

from construct import Struct, Int32ul, Bytes, Padding, Array, this

Header = Struct(
    "magic" / Bytes(4),
    "entry_count" / Int32ul,
    Padding(8),
    "entries" / Array(this.entry_count, Struct(
        "entry_id" / Int32ul,
        "offset" / Int32ul,
    )),
)

Common field types

Type Meaning
Int32ul 4-byte unsigned integer, little-endian
Int32ub 4-byte unsigned integer, big-endian
Bytes(n) Raw n bytes, returned as a bytes object
Padding(n) Skip n bytes (often unknown/reserved fields)
Array(count, subcon) Repeat subcon exactly count times
this.field_name Reference another field read earlier

The ul in Int32ul means unsigned, little-endian. This is the same format as struct.unpack("<I", ...).

Parsing a file

with open("_GRAPHRES.BIN", "rb") as f:
    file_data = f.read()

parsed = Header.parse(file_data)

print(parsed.magic)        # b'LINK'
print(parsed.entry_count)  # 1030
print(parsed.entries[0])   # Container: entry_id=..., offset=...

The result is a Container object that behaves like a Python object with attributes.

Why use construct instead of struct?

  • Self-documenting: the format is described declaratively.
  • Easier to maintain: adding a field is a single line.
  • Better for nested formats: headers, arrays, and sub-structures are straightforward.
  • Endian swap is trivial: change Int32ul to Int32ub.
  • Validation: you can use Const to require exact magic bytes.

Limitations

  • It reads from the start of the buffer, not searching for patterns.
  • The whole structure is parsed into memory; very large files may need parse_stream or compiled parsers.
  • Custom logic like size-by-next-offset or padding trimming still happens in Python after parsing.

Example: validating the magic

Use Const to make parsing fail early if the file signature is wrong:

from construct import Const

Header = Struct(
    Const(b"LINK"),
    "entry_count" / Int32ul,
    Padding(8),
    "entries" / Array(this.entry_count, Struct(
        "entry_id" / Int32ul,
        "offset" / Int32ul,
    )),
)