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.