Skip to main content

debin

A binary parser for Python. You describe a file format as a dataclass with type annotations, and debin reads a buffer into it.

from typing import List
from debin import *

@debin(magic="HDR ")
class Header:
    version: uint32
    count: uint32
    values: List[uint32] = field(metadata={"count": "count"})

with open("file.bin", "rb") as f:
    buffer = f.read()

header = Header().read_le(buffer)
print(header)
# Header(version=1, count=3, values=[10, 20, 30])

Install

Requires Python 3.10 or newer. No third party dependencies.

uv

uv add git+https://github.com/maxcabd/debin.git

Or clone it and install locally:

git clone https://github.com/maxcabd/debin.git
cd debin
uv pip install -e .

pip

pip install git+https://github.com/maxcabd/debin.git

Or clone it and install locally:

git clone https://github.com/maxcabd/debin.git
cd debin
pip install -e .

Quick start

A struct is a dataclass decorated with @debin. Fields are read in declaration order.

from debin import *

@debin
class Point:
    x: int32
    y: int32

buffer = bytearray((1).to_bytes(4, "little") + (2).to_bytes(4, "little"))
p = Point().read_le(buffer)
print(p)  # Point(x=1, y=2)

Call .read_le(buffer) or .read_be(buffer) to pick the byte order for that read. .read(buffer) uses whatever endian the struct was declared with (@debin(endian="big")), or little endian if none was given.

Nested structs, lists, and strings work the same way:

from typing import List
from debin import *

@debin
class Entry:
    id: uint16
    name: nullstr

@debin(magic="TBL ")
class Table:
    entry_count: uint32
    entries: List[Entry] = field(metadata={"count": "entry_count"})

Types

Type Size Notes
bool 1 byte
uint8, int8 1 byte
uint16, int16 2 bytes
uint32, int32 4 bytes
uint64, int64 8 bytes
float16, float32, float64 2, 4, 8 bytes
nullstr variable ASCII string, reads until a 0x00 byte
List[T] variable any of the above, an enum, or another struct

An enum works as a field type if it inherits IntEnum or IntFlag and is decorated with @debin(repr=<some integer type>), which is the type actually read from the buffer:

from enum import IntFlag
from debin import *

@debin(repr=uint8)
class Flags(IntFlag):
    READ = 0x1
    WRITE = 0x2
    EXEC = 0x4

Directives

Directives are passed as field(metadata={...}) on a field. They control how that field gets read, beyond just its type.

magic

Struct level, not a field directive. Checks the first bytes of the struct against a fixed value and raises MagicError if they don't match.

@debin(magic="PNG ")
class Header:
    ...

magic also works on a single field, for a tag that shows up partway through a struct rather than at the very start. A magic field is fully consumed by the check, it isn't a prefix glued onto a separately typed value:

@debin
class Entry:
    tag: bytes = field(metadata={"magic": b"ENT1"})
    value: uint32

endian

Struct level (@debin(endian="big")) or field level (field(metadata={"endian": "big"})). A field level override wins over whatever endian the surrounding struct is being read with.

@debin
class Packet:
    length: uint16              # follows the struct's own endian
    checksum: uint16 = field(metadata={"endian": "big"})  # always big endian

count

How many elements to read into a List[T] field. Can be a fixed number, the name of another field, or an expression built with this (see below).

@debin
class Blob:
    length: uint32
    data: List[uint8] = field(metadata={"count": "length"})

if

Only parse this field when the condition is true. When it's false the field is set to None and nothing is read from the buffer. Conditions are written with this.

@debin
class Chunk:
    has_extra: uint8
    extra: uint32 = field(metadata={"if": this.has_extra != 0})

assert / pre_assert

Check a condition and raise ValidationError if it's false, instead of silently continuing on a file that doesn't match what you expected. assert runs after the field is parsed, so the condition can reference its own value. pre_assert runs before, so it can only reference earlier fields.

@debin
class Header:
    version: uint8 = field(metadata={"assert": this.version <= 3})

@debin
class Entry:
    kind: uint8
    value: uint32 = field(metadata={"pre_assert": this.kind == 1})

calc

A field that isn't read from the buffer at all. Its value is computed from other fields once they've already been parsed.

@debin
class Name:
    first: nullstr
    last: nullstr
    full: nullstr = field(metadata={"calc": this.first + " " + this.last})

map

Read the field normally, then run its value through a function before storing it.

@debin
class Version:
    raw: uint16 = field(metadata={"map": lambda v: (v >> 8, v & 0xFF)})

ignore

Skip this field during normal parsing. Its value is None unless something else sets it, such as map on a different field or calc.

@debin
class Entry:
    raw_bytes: List[uint8] = field(metadata={"count": 8})
    text: str = field(metadata={
        "ignore": True,
        "map": lambda self: bytes(self.raw_bytes).rstrip(b"\x00").decode(),
    })

pad_before / pad_after

Skip a fixed number of bytes before or after reading the field.

@debin
class Entry:
    id: uint8
    value: uint32 = field(metadata={"pad_before": 3})

pad_size_to

Pad this field up to a fixed total width, whatever it naturally consumed. Different from pad_after, which always adds a fixed number of bytes on top. Useful for fixed-width slots like a string that's always allotted N bytes regardless of how long it actually is.

@debin
class Entry:
    name: nullstr = field(metadata={"pad_size_to": 32})
    value: uint32

align_before / align_after

Advance the offset up to the next multiple of N before or after reading the field.

@debin
class Entry:
    id: uint8
    value: uint32 = field(metadata={"align_before": 4})

seek / seek_before

Jump to a specific position before reading the field, then parse it there. The position can be an absolute offset, the name of another field, a (SeekFrom, offset) pair, or a function of (buffer, offset).

@debin
class File:
    data_start: uint32
    payload: List[uint8] = field(metadata={"seek": "data_start", "count": 16})
    checksum: uint32 = field(metadata={"seek_before": (SeekFrom.END, -4)})

restore_pos

Parse this field, but leave the offset exactly where it was before, so the next field starts as if this one had never been read. Handy for looking ahead at a value before deciding what to do with it.

@debin
class Header:
    a: uint8
    peek_next: uint8 = field(metadata={"restore_pos": True})
    b: uint8  # reads the same byte as peek_next

context

Pass values into a nested struct's field that isn't itself present in the buffer at that point, only available from the struct doing the nesting. The receiving field is ignored, and gets set from context before any of that struct's own fields are read.

@debin
class Chunk:
    version: uint16 = field(metadata={"ignore": True})
    fov: float32

@debin
class File:
    version: uint16
    chunk: Chunk = field(metadata={"context": {"version": this.version}})

parse_with

Read a List[T] field with a custom function instead of a fixed count. debin ships three ready to use ones:

  • until_eof reads structs until the buffer runs out
  • until_with(predicate) reads structs up to and including the one where predicate returns true
  • until_exclusive(predicate) reads structs up to but not including the one where predicate returns true
from debin.helpers import until_eof

@debin
class Container:
    header: Header
    chunks: List[Chunk] = field(metadata={"parse_with": until_eof})

try

Parse the field, and if that raises any exception, fall back to a default value instead of failing the whole read. Real files have edge cases and garbage data; this lets you keep going instead of hard-crashing on one bad field. Nothing is guaranteed about the offset afterward beyond "unchanged" - downstream fields may end up misaligned if this actually triggers.

extra: uint32 = field(metadata={"try": 0})

err_context

Wrap any exception raised while parsing this field with an extra message, so failures inside deeply nested structs are easier to track down.

extra: uint32 = field(metadata={"err_context": "reading Header.extra"})

dbg

Print the offset and value of a field as it's read. Useful while figuring out a format.

value: uint32 = field(metadata={"dbg": True})
# [offset 0x04] value = 12345

Writing expressions with this

if, count, and calc accept expressions built from this, which stands for the struct being parsed. Attribute access, comparisons, and arithmetic all work directly:

texture_data: List[uint8] = field(metadata={"count": this.header.pitch})

dx10_header: DX10Header = field(metadata={
    "if": this.header.pixel_format.four_cc.apply(bytes) == b"DX10"
})

Plain function calls on a this expression need .apply(...) instead of wrapping it directly, since bytes(this.four_cc) would call bytes() immediately rather than deferring it:

this.four_cc.apply(bytes) == b"DX10"     # correct
bytes(this.four_cc) == b"DX10"           # wrong, evaluates too early

More examples

The examples/ folder has full, runnable formats: BMP, DDS, WAV, TCP packets, and a real game asset container (xfbin) that shows nested structs, lists of structs, and parse_with together.

Metadata

Release files for debin 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for debin 0.1.0
File Size Uploaded Provenance Actions
debin-0.1.0.tar.gz 23.5 kB Without provenance Details for debin-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for debin 0.1.0
File Interpreter ABI Platform Actions
debin-0.1.0-py3-none-any.whl Python 3 none any Details for debin-0.1.0-py3-none-any.whl

Total release size: 48.0 kB

Release files / debin-0.1.0.tar.gz

Download URL debin-0.1.0.tar.gz
Size 23.5 kB
Tags Source
SHA-256 checksum
How to use checksums
eb0e8733c0f9ca34b91dab767514fc2e8dd6e1723f7f4517f7e5ca8204bc128b
BLAKE2b-256 checksum
How to use checksums
0d3f6426ad36bcbd1f8a0a6863979eb2185b40ee334441070e78881e234cfb82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.4

Release files / debin-0.1.0-py3-none-any.whl

Download URL debin-0.1.0-py3-none-any.whl
Size 24.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
73d1287ed4d25f858a485f9cc0c13a02203ae096fde3a6b7342e2c95609a4ceb
BLAKE2b-256 checksum
How to use checksums
758bb10d765fce805b1f8013d77801fa0bb849e1728cffcdafa4b3c9d1a764a0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.4

Security for debin 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page