2.3.9. Templates#

Changed in version 2.9.0: Added support for Python’s Generic and TypeVar types.

Templates are blueprints for binary structures. They let you describe a layout once and specialize it with different Caterpillar field types later.

The preferred form uses Python generics. Legacy TemplateTypeVar templates are still supported and are covered at the end of this page.

If you’d like the implementation details, see Templates.

2.3.9.1. Generic Templates#

Define a template as a normal generic Python class and decorate it with @template. A plain type variable annotation is enough.

A generic scalar template#
from typing import Generic, TypeVar

from caterpillar.py import pack, template, uint8, uint16

T = TypeVar("T")


@template
class Box(Generic[T]):
    value: T


ByteBox = Box[uint8]
WordBox = Box[uint16]

assert pack(ByteBox(7)) == b"\x07"
assert pack(WordBox(0x0203)) == b"\x03\x02"

Box is not a struct by itself. Box[uint8] materializes a concrete struct class and caches it, so repeated uses of the same specialization return the same class.

2.3.9.2. Multiple Type Variables#

Templates can use more than one type variable. The order in Generic[...] defines the positional specialization order.

Multiple template parameters#
from typing import Generic, TypeVar

from caterpillar.py import BigEndian, pack, template, uint8, uint16

T = TypeVar("T")
U = TypeVar("U")


@template
class Pair(Generic[T, U]):
    left: T
    right: U


ByteWordPair = Pair[uint8, uint16]
obj = ByteWordPair(1, 0x0203)

assert pack(obj, order=BigEndian) == b"\x01\x02\x03"

2.3.9.3. Fields With Layout Metadata#

Use direct T annotations for simple scalar fields. When a field needs Caterpillar-specific layout metadata, keep the Python type in the first position of f[] and put the template field marker in the metadata position.

Repeated template field#
from typing import Generic, TypeVar

from caterpillar.py import f, field_of, pack, template, uint8

T = TypeVar("T")


@template
class Vector(Generic[T]):
    values: f[list[T], field_of(T)[2]]


ByteVector = Vector[uint8]

assert pack(ByteVector([3, 4])) == b"\x03\x04"

field_of(T) supports the same layout operators as normal fields, including sequence length, offset, switch mappings, byte order, bit width, and condition markers. For example, field_of(T)[2] becomes a two-element field after specialization.

2.3.9.4. Partial Templates#

A specialization that still contains unresolved type variables remains a template. This is useful for fixing only part of a layout.

Partial generic specialization#
from typing import Generic, TypeVar

from caterpillar.py import BigEndian, pack, template, uint8, uint16

T = TypeVar("T")
U = TypeVar("U")


@template
class Pair(Generic[T, U]):
    left: T
    right: U


BytePair = Pair[uint8, U]
ByteWordPair = BytePair[uint16]

assert pack(ByteWordPair(1, 0x0203), order=BigEndian) == b"\x01\x02\x03"

2.3.9.5. Subclassing Specializations#

Template specializations are normal struct classes and can be used as bases for other structs.

Extending a specialization#
from typing import Generic, TypeVar

from caterpillar.py import BigEndian, f, pack, struct, template, uint8, uint16

T = TypeVar("T")


@template
class Box(Generic[T]):
    value: T


@struct
class Packet(Box[uint8]):
    tail: f[int, uint16]


assert pack(Packet(1, 0x0203), order=BigEndian) == b"\x01\x02\x03"

2.3.9.6. Legacy Templates#

The older TemplateTypeVar style is still supported. Use it when maintaining existing code that already depends on derive().

Legacy template definition#
from caterpillar.py import TemplateTypeVar, derive, template, uint8, uint16

A = TemplateTypeVar("A")


@template(A, "B")
class FormatTemplate:
    foo: A
    bar: B


Format = derive(FormatTemplate, uint8, uint16)

derive() remains available as an explicit escape hatch for generic templates as well, but direct subscript syntax is the preferred form for new code.