Model inheritance

To give several messages the same framing, put the settings in a base class and declare fields in the concrete models:

from bytespec import Constructor, Flags, PayloadLength, ProtoModel, SchemaError
from bytespec.types import UInt8

class Compact(ProtoModel):
    __header__ = (Constructor(1), PayloadLength(1))

class Reading(Compact):
    __constructor__ = 7
    value: UInt8

reading = Reading(value=42)
assert Reading.decode(reading.encode()) == reading
print(reading.encode().hex(" "))
07 01 2a

__header__, __constructor__, __byte_order__, and configure_codecs() rules are inherited. Framing settings can also be overridden in a subclass with no fields of its own: each subclass gets a newly built schema.

Multiple base classes

Without an explicit __header__, custom headers from direct ProtoModel bases are considered. The default header does not compete with a custom one:

class DefaultBase(ProtoModel):
    pass

class Mixed(DefaultBase, Compact):
    value: UInt8

assert Mixed.__header__ is Compact.__header__
assert Mixed.decode(Mixed(value=1).encode()) == Mixed(value=1)

Diamond inheritance of the same header is also allowed:

class Left(Compact):
    pass

class Right(Compact):
    pass

class Diamond(Left, Right):
    value: UInt8

assert Diamond.__header__ is Compact.__header__
assert Diamond.decode(Diamond(value=2).encode()) == Diamond(value=2)

Two different custom headers require an explicit choice:

class Other(ProtoModel):
    __header__ = (PayloadLength(2),)

try:
    class Conflict(Compact, Other):
        value: UInt8
except SchemaError as error:
    print(error)

class Resolved(Compact, Other):
    __header__ = Compact.__header__
    value: UInt8

assert Resolved.decode(Resolved(value=3).encode()) == Resolved(value=3)
Conflict: conflicting inherited headers; define __header__ explicitly

The comparison uses tuple identity, not element equivalence. Two independently created (Constructor(1), PayloadLength(1)) tuples conflict even if their wire format is identical. To reuse a header, inherit it or assign an existing tuple. The empty tuple () also counts as a custom header. This special resolution applies only to the header; other settings follow Python’s normal lookup order.

Fields are not merged

A subclass that declares its own fields builds a schema only from those fields:

class Parent(ProtoModel):
    __header__ = ()
    first: UInt8

class Child(Parent):
    second: UInt8

child = Child(second=2)
print(child.encode().hex(" "))
02

first is not included in the Child schema. For a structure containing parent data plus new data, use Lists and nested models or explicitly declare the complete set of fields in the child model.

Change framing while preserving fields

A subclass with no serializable fields of its own retains the inherited fields, their codecs, defaults, and factories. It gets a separate schema with its own __constructor__, __header__, and __byte_order__. You do not need to repeat fields just to change framing:

from bytespec import ByteOrder
from bytespec.types import UInt16

class ReadingBase(ProtoModel):
    __constructor__ = 2
    __header__ = (Constructor(1), PayloadLength(1))
    value: UInt16

class LittleReading(ReadingBase):
    __constructor__ = 3
    __header__ = (PayloadLength(1), Constructor(2))
    __byte_order__ = ByteOrder.LITTLE

reading = LittleReading(value=0x1234)
assert LittleReading.decode(reading.encode()) == reading
print(reading.encode().hex(" "))
print(ReadingBase(value=0x1234).encode().hex(" "))
02 03 00 34 12
02 02 12 34

In the first line, the length comes before the two-byte constructor, and the number is written in little-endian order. The parent’s format is unchanged. If the inherited fields include optional fields, the new header must also contain Flags wide enough to accommodate them; otherwise, declaring the subclass raises SchemaError.

With multiple inheritance and no fields of its own, a subclass uses the first available set of fields in the MRO, without merging base-class schemas. The custom header selection rule described above still applies.

Automatic __validate__ calls follow a separate rule: the parent’s method is not automatically called for a child instance. See Validating model values for how to reuse validation explicitly.