GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing msgspec.dev — Structs Structs¶Structs are the preferred way of defining structured data types in Structs are defined by subclassing from >>> import msgspec >>> class User(msgspec.Struct): ... """A struct describing a user""" ... name : str ... email : str | None = None ... groups : set[str] = set()
Struct types automatically generate a few methods based on the provided type annotations:
>>> alice = User("alice", groups={"admin", "engineering"}) >>> alice User(name='alice', email=None, groups={'admin', 'engineering'}) >>> bob = User("bob", email="[email protected]") >>> bob User(name='bob', email='[email protected]', groups=set()) >>> alice.name "alice" >>> bob.groups set() >>> alice == bob False >>> alice == User("alice", groups={"admin", "engineering"}) True Note that it is forbidden to override The struct fields are available via the >>> class Point(msgspec.Struct): ... """A point in 2D space""" ... x : float ... y : float ... ... def to_dict(self): ... return {f: getattr(self, f) for f in self.__struct_fields__} ... >>> p = Point(1.0, 2.0) >>> p.to_dict() {"x": 1.0, "y": 2.0} Default Values¶Struct fields may be given default values, which are used if no value is
provided to >>> from msgspec import Struct, field >>> import uuid >>> class Example(Struct): ... a: int = 1 ... b: uuid.UUID = field(default_factory=uuid.uuid4) ... c: list[int] = [] >>> Example() Example(a=1, b=UUID('f63219d5-e9ca-4ae8-afd0-cba30e84222d'), c=[]) >>> Example(a=2) Example(a=2, b=UUID('319a6c0f-2841-4439-8bc8-2c1daf7d77a2'), c=[]) >>> Example().c is Example().c # new list instance used each time False Default values may be one of 3 kinds:
Post-Init Processing¶If a struct type defines a In addition to in
In these cases any >>> import msgspec >>> class Interval(msgspec.Struct): ... low: float ... high: float ... ... def __post_init__(self): ... if self.low > self.high: ... raise ValueError("`low` may not be greater than `high`") >>> Interval(1, 2) # valid interval Interval(low=1, high=2) >>> Interval(2, 1) # invalid interval Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 6, in __post_init__ ValueError: `low` may not be greater than `high` >>> msgspec.json.decode(b'{"low": 2, "high": 1}', type=Interval) # invalid interval from JSON Traceback (most recent call last): File "<stdin>", line 6, in __post_init__ ValueError: `low` may not be greater than `high` The above exception was the direct cause of the following exception: Traceback (most recent call last): File "<stdin>", line 1, in <module> msgspec.ValidationError: `low` may not be greater than `high` Field Ordering¶When defining a new struct type, fields are stored in the order they’re defined
(keyword-only fields excluded, more on this later). This is nice for
readability since the generated class Example(msgspec.Struct): a: str b: int = 0 The generated def __init__(self, a: str, b: int = 0): One consequence of this is that you can’t put fields without defaults after fields with defaults, since the Python VM doesn’t allow keyword arguments before positional arguments. The following struct definition will error: >>> class Invalid(msgspec.Struct): ... a: str = "" ... b: int # oop, no default! Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Required field 'b' cannot follow optional fields. Either reorder the struct fields, or set `kw_only=True` in the struct definition. Thankfully the error message includes some solutions:
Keyword-only parameters have no such restriction; required and optional parameters can be mixed in any order. >>> class Example(msgspec.Struct, kw_only=True): ... a: str = "" ... b: int # this is fine with kw_only=True >>> Example(a="example", b=123) Example(a='example', b=123) Note that the >>> class Base(msgspec.Struct, kw_only=True): ... a: str = "" ... b: int >>> class Subclass(Base): ... c: float ... d: bytes = b"" The generated def __init__(self, c: float, d: bytes = b"", *, a: str, b: int = 0): The field ordering rules for Class Variables¶Like >>> import msgspec >>> from typing import ClassVar >>> class Example(msgspec.Struct): ... x: int ... a_class_variable: ClassVar[int] = 2 >>> Example.a_class_variable 2 >>> Example(1) # only `x` is counted as a field Example(x=1) Note that if using PEP 563 “postponed evaluation of annotations” (e.g.
Importing Type Validation¶Unlike some other libraries (e.g. pydantic), the type annotations on a
>>> import msgspec >>> class Point(msgspec.Struct): ... x: float ... y: float >>> # Improper types in *your* code aren't checked at runtime ... Point(x=1, y="oops") Point(x=1, y='oops') >>> # Improper types when decoding *are* checked at runtime ... msgspec.json.decode(b'{"x": 1.0, "y": "oops"}', type=Point) Traceback (most recent call last): File "<stdin>", line 1, in <module> msgspec.ValidationError: Expected `float`, got `str` - at `$.y` This is intentional. Static type checkers like mypy/pyright work well with
The input(s) to your programs however cannot be checked statically, as they
aren’t known until runtime. As such, Pattern Matching¶If using Python 3.10+, # NOTE: this example requires Python 3.10+ >>> import msgspec >>> class Point(msgspec.Struct): ... x: float ... y: float >>> def where_is(point): ... match point: ... case Point(0, 0): ... print("Origin") ... case Point(0, y): ... print(f"Y={y}") ... case Point(x, 0): ... print(f"X={x}") ... case Point(): ... print("Somewhere else") ... case _: ... print("Not a point") >>> where_is(Point(0, 6)) "Y=6" Equality and Order¶By default struct types define an >>> class Point(msgspec.Struct, order=True): ... x: float ... y: float >>> Point(1, 2) == Point(1, 2) True >>> Point(1, 2) < Point(3, 4) True In rare instances you may opt to disable generation of the >>> class Point(msgspec.Struct, eq=False): ... x: float ... y: float >>> p = Point(1, 2) >>> p == Point(1, 2) False >>> p == p # identity comparison only True Frozen Instances¶A struct type can optionally be marked as “frozen” by specifying
>>> class Point(msgspec.Struct, frozen=True): ... """This struct is immutable & hashable""" ... x: float ... y: float ... >>> p = Point(1.0, 2.0) >>> {p: 1} # frozen structs are hashable, and can be keys in dicts {Point(1.0, 2.0): 1} >>> p.x = 2.0 # frozen structs cannot be modified after creation Traceback (most recent call last): ... AttributeError: immutable type: 'Point' Tagged Unions¶By default a serialized struct only contains information on the values present in the struct instance - no information is serialized noting which struct type corresponds to the message. Instead, the user is expected to know the type the message corresponds to, and pass that information appropriately to the decoder. >>> import msgspec >>> class Get(msgspec.Struct): ... key: str >>> msg = msgspec.json.encode(Get("my key")) >>> msg # No type information present in the message b'{"key":"my key"}' >>> msgspec.json.decode(msg, type=Get) Get(key='my key') In most cases this works well - schemas are often simple and each value may only correspond to at most one Struct type. However, sometimes you may have a message (or a field in a message) that may contain one of a number of different structured types. In this case we need some way to determine the type of the message from the message itself!
The quickest way to enable tagged unions is to set >>> import msgspec >>> from typing import Union >>> # Pass in ``tag=True`` to tag the structs using the default configuration ... class Get(msgspec.Struct, tag=True): ... key: str >>> class Put(msgspec.Struct, tag=True): ... key: str ... val: str >>> msg = msgspec.json.encode(Get("my key")) >>> msg # "type" is the tag field, "Get" is the tag b'{"type":"Get","key":"my key"}' >>> # Create a decoder for decoding either Get or Put ... dec = msgspec.json.Decoder(Get | Put) >>> # The tag value is used to determine the message type ... dec.decode(b'{"type": "Put", "key": "my key", "val": "my val"}') Put(key='my key', val='my val') >>> dec.decode(b'{"type": "Get", "key": "my key"}') Get(key='my key') >>> # A tagged union can also contain non-struct types. ... msgspec.json.decode( ... b'123', ... type=Get | Put | int, ... ) 123 If you want to change this behavior to use a different tag field and/or value,
you can further configure things through the
If you like subclassing, both >>> import msgspec >>> from typing import Union >>> # Create a base class for tagged structs, where: ... # - the tag field is "op" ... # - the tag is the class name lowercased ... class TaggedBase(msgspec.Struct, tag_field="op", tag=str.lower): ... pass >>> # Use the base class to pass on the configuration ... class Get(TaggedBase): ... key: str >>> class Put(TaggedBase): ... key: str ... val: str >>> msg = msgspec.json.encode(Get("my key")) >>> msg # "op" is the tag field, "get" is the tag b'{"op":"get","key":"my key"}' >>> # Create a decoder for decoding either Get or Put ... dec = msgspec.json.Decoder(Get | Put) >>> # The tag value is used to determine the message type ... dec.decode(b'{"op": "put", "key": "my key", "val": "my val"}') Put(key='my key', val='my val') >>> dec.decode(b'{"op": "get", "key": "my key"}') Get(key='my key') Omitting Default Values¶By default, >>> import msgspec >>> class User(msgspec.Struct): ... name : str ... email : str | None = None ... groups : set[str] = set() >>> alice = User("alice") >>> alice # email & groups are using the default values User(name='alice', email=None, groups=set()) >>> msgspec.json.encode(alice) # default values are present in encoded message b'{"name":"alice","email":null,"groups":[]}' If the default values are known on the decoding end (making serializing them
redundant), it may be beneficial and desired to omit default values from the
encoded message. This can be done by configuring >>> import msgspec >>> class User(msgspec.Struct, omit_defaults=True): ... name : str ... email : str | None = None ... groups : set[str] = set() >>> alice = User("alice") >>> msgspec.json.encode(alice) # default values are omitted b'{"name":"alice"}' >>> bob = User("bob", email="[email protected]") >>> msgspec.json.encode(bob) b'{"name":"bob","email":"[email protected]"}' Omitting defaults reduces the size of the encoded message, and often also improves encoding and decoding performance (since there’s less work to do).
Note that detection of default values is optimized for performance; in certain situations a default value may still be encoded. For the curious, the current detection logic is as follows: >>> def matches_default(value: Any, default: Any) -> bool: ... """Whether a value matches the default for a field""" ... if value is default: ... return True ... if type(value) != type(default): ... return False ... if type(value) in (list, set, dict) and (len(value) == len(default) == 0): ... return True ... return False This detection never calls a >>> class Basket(msgspec.Struct, omit_defaults=True): ... items: list[int] = msgspec.field(default_factory=list) The field annotation supplies the element type, so Forbidding Unknown Fields¶By default One downside is that typos may go unnoticed when decoding >>> class Example(msgspec.Struct): ... field_one: int ... field_two: bool = False >>> msgspec.json.decode( ... b'{"field_one": 1, "field_twoo": true}', # oops, a typo ... type=Example ... ) Example(field_one=1, field_two=False) In this example, the misspelled To prevent typos like this, you can configure >>> class Example(msgspec.Struct, forbid_unknown_fields=True): ... field_one: int ... field_two: bool = False >>> msgspec.json.decode( ... b'{"field_one": 1, "field_twoo": true}', # oops, a typo ... type=Example ... ) Traceback (most recent call last): File "<stdin>", line 1, in <module> msgspec.ValidationError: Object contains unknown field `field_twoo` Renaming Fields¶Sometimes you want the field name used in the encoded message to differ from
the name used by your Python code. Perhaps you want a
On the field definition If you’re only renaming a few fields, you might find configuring the new names
as part of the field definition to be the simplest option. To do this you can
use the >>> import msgspec >>> class Example(msgspec.Struct): ... x: int ... y: int ... z: int = msgspec.field(name="field_z") # renamed to "field_z" >>> # Python code uses the original field names ... ex = Example(x=1, y=2, z=3) >>> # Encoded messages use the renamed field names ... msgspec.json.encode(ex) b'{"x":1,"y":2,"field_z":3}' >>> # Decoding also uses the renamed field names ... msgspec.json.decode(b'{"x": 1, "y": 2, "field_z": 3}', type=Example) Example(x=1, y=2, z=3) On the struct definition If you’re renaming lots of fields (especially if you’re renaming them with a
naming convention like
The renamed field names are used for encoding and decoding only, any python code will still refer to them using their original names. >>> import msgspec >>> class Example(msgspec.Struct, rename="camel"): ... """A struct with fields renamed using camelCase""" ... field_one: int ... field_two: str >>> # Python code uses the original field names ... ex = Example(1, field_two="two") >>> # Encoded messages use the renamed field names ... msgspec.json.encode(ex) b'{"fieldOne":1,"fieldTwo":"two"}' >>> # Decoding uses the renamed field names ... msgspec.json.decode(b'{"fieldOne": 3, "fieldTwo": "four"}', type=Example) Example(field_one=3, field_two='four') >>> # Decoding errors also use the renamed field names ... msgspec.json.decode(b'{"fieldOne": 5}', type=Example) Traceback (most recent call last): File "<stdin>", line 1, in <module> msgspec.ValidationError: Object missing required field `fieldTwo` If renaming to camelCase, you may run into issues if your field names contain
acronyms (e.g. # https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec # An explicit mapping from python name -> JSON field name v1podspec_names = { ... "service_account_name": "serviceAccountName", "set_hostname_as_fqdn": "setHostnameAsFQDN", ... } # Pass the mapping to `rename` to explicitly rename all fields class V1PodSpec(msgspec.Struct, rename=v1podspec_names): ... service_account_name: str = "" set_hostname_as_fqdn: bool = False ... Note that if both the >>> import msgspec >>> class Example(msgspec.Struct, rename="camel"): ... field_x: int ... field_y: int = msgspec.field(name="y") # set explicitly >>> msgspec.json.encode(Example(1, 2)) b'{"fieldX":1,"y":2}' Encoding/Decoding as Arrays¶By default Struct objects encode the same dicts, with both the keys and values present in the message. >>> import msgspec >>> class Point(msgspec.Struct): ... x: int ... y: int >>> msgspec.json.encode(Point(1, 2)) b'{"x":1,"y":2}' If you need higher performance (at the cost of more inscrutable message
encoding), you can set >>> class Point2(msgspec.Struct, array_like=True): ... x: int ... y: int >>> msgspec.json.encode(Point2(1, 2)) b'[1,2]' >>> msgspec.json.decode(b'[3,4]', type=Point2) Point2(x=3, y=4) Note that Tagged Unions also work with structs with
>>> import msgspec >>> from typing import Union >>> class Get(msgspec.Struct, tag=True, array_like=True): ... key: str >>> class Put(msgspec.Struct, tag=True, array_like=True): ... key: str ... val: str >>> msgspec.json.encode(Get("my key")) b'["Get","my key"]' >>> msgspec.json.decode( ... b'["Put", "my key", "my val"]', ... type=Get | Put, ... ) Put(key='my key', val='my val') Runtime Definition¶In some cases it can be useful to dynamically generate >>> import msgspec >>> Point = msgspec.defstruct("Point", [("x", float), ("y", float)]) >>> p = Point(1.0, 2.0) >>> p Point(x=1.0, y=2.0) Advanced¶Metaclasses¶You can define project-wide In the following example, we flip the default value of >>> from msgspec import Struct, StructMeta >>> class KwOnlyStructMeta(StructMeta): ... def __new__(mcls, name, bases, namespace, **struct_config): ... struct_config.setdefault("kw_only", True) ... return super().__new__(mcls, name, bases, namespace, **struct_config) >>> class KwOnlyStruct(Struct, metaclass=KwOnlyStructMeta): ... >>> class Example(KwOnlyStruct): ... a: str = "" ... b: int >>> Example() Traceback (most recent call last): File "<python-input-3>", line 1, in <module> Example() ~~~~~~~^^ TypeError: Missing required argument 'b' >>> Example(b=123) Example(a='', b=123) You can also mix >>> from abc import ABCMeta, abstractmethod >>> from msgspec import Struct, StructMeta >>> class EventMeta(StructMeta, ABCMeta): ... >>> class Event(Struct, metaclass=EventMeta): ... id: int ... @abstractmethod ... def kind(self) -> str: ... ... >>> Event(id=1) Traceback (most recent call last): File "<python-input-4>", line 1, in <module> Event(id=1) ~~~~~^^^^^^ TypeError: Can't instantiate abstract class Event without an implementation for abstract method 'kind' >>> class UserCreated(Event): ... username: str ... def kind(self) -> str: ... return "user_created" ... >>> UserCreated(id=1, username="alice") UserCreated(id=1, username='alice') Here Important
Disabling Garbage Collection¶Warning This is an advanced optimization, and only recommended for users who fully understand the implications of disabling the GC. Python uses reference counting to detect when memory can be freed, with a periodic cyclic garbage collector pass to detect and free cyclic references. Garbage collection (GC) is triggered by the number of uncollected GC-enabled (objects that contain other objects) objects passing a certain threshold. This design means that garbage collection passes often run during code that creates a lot of objects (for example, deserializing a large message). By default, >>> import msgspec >>> from typing import Any >>> import gc >>> class Example(msgspec.Struct): ... x: Any ... y: Any >>> ex1 = Example(1, "two") >>> # ex1 is untracked, since it only references untracked objects ... gc.is_tracked(ex1) False >>> ex2 = Example([1, 2, 3], (4, 5, 6)) >>> # ex2 is tracked, since it references tracked objects ... gc.is_tracked(ex2) True If you are certain that your struct types can never participate in a
reference cycle, you may find a performance boost from setting
Struct types with
|
| You are visitor 01402503 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |