Validating model values

A range’s start must not exceed its end. This rule relates two fields and cannot be expressed by choosing a numeric codec:

from bytespec import ProtoModel
from bytespec.types import UInt16

class Window(ProtoModel):
    __header__ = ()
    start: UInt16
    end: UInt16

    def __validate__(self) -> None:
        if self.start > self.end:
            raise ValueError("start must not exceed end")

window = Window(start=10, end=20)
assert Window.decode(window.encode()) == window

try:
    Window(start=20, end=10)
except ValueError as error:
    print(error)
start must not exceed end

The library validates the binary schema itself: whether types are known and indices and settings are valid. A schema error raises SchemaError. __validate__ checks application values. It is an ordinary instance method with no decorator; when it runs, all fields have already been assigned, including defaults and factory results. Its return value is ignored: raise an exception to reject the values.

When validation runs

The method runs at the end of ProtoModel.__init__, so it also runs during decode() and decode_from(). Decoding the bytes creates an ordinary instance. For example, valid numbers can violate the range rule:

try:
    Window.decode(bytes.fromhex("00 14 00 0a"))
except ValueError as error:
    print(error)
start must not exceed end

The validator’s exception propagates unchanged: here it is ValueError, not DecodeError or EncodeError. Choose an exception type the application can handle alongside decoding errors.

Attribute assignment and encode() do not run validation again:

window.start = 30
print(window.encode().hex(" "))
try:
    window.__validate__()
except ValueError as error:
    print(error)
00 1e 00 14
start must not exceed end

If you mutate the object, call the method explicitly before encoding, or create a new instance. Codecs still check numeric ranges and whether values can be represented during encode(). Models do not provide full runtime type checking or automatic value conversion.

Validation in a subclass

Automatic invocation looks for __validate__ only in the concrete class itself. An inherited method is available in Python but does not run automatically:

class UncheckedWindow(Window):
    pass

unchecked = UncheckedWindow(start=20, end=10)
assert unchecked.start > unchecked.end

To reuse it, define a method in the child class:

class CheckedWindow(Window):
    def __validate__(self) -> None:
        super().__validate__()

try:
    CheckedWindow(start=20, end=10)
except ValueError as error:
    print(error)
start must not exceed end

A nested model runs its own validation when it is created, before the outer model’s validator during decoding. Further rules for composition and subclass schemas are described in Model inheritance.

Next: Handling errors explains which exceptions to handle when exchanging messages.