Reference¶
Versionable Class Parameters¶
Full signature:
@dataclass
class MyClass(
Versionable,
version=1,
hash="abc123",
name="MyClass",
old_names=["OldName"],
register=True,
skip_defaults=False,
unknown="ignore",
):
...
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
|
required |
Schema version. Increment on breaking field changes. |
|
|
recommended |
6-char hash of field names/types. Checked at import. |
|
|
class name |
Serialization name for output metadata and registry. |
|
|
|
Previous names; allows loading old files. |
|
|
|
Add class to global registry (for |
|
|
|
Omit fields equal to their class default from output. |
|
|
|
Unrecognised fields: |
Opting Out of Registration¶
By default, every Versionable subclass is added to a global registry keyed by its serialization name. This registry
powers loadDynamic(), which looks up the class from the __OBJECT__ metadata in a file.
Set register=False when a class shouldn’t participate in this registry:
# Abstract base — never serialized directly
@dataclass
class SensorBase(Versionable, version=1, register=False):
timestamp: datetime
# Test fixture — avoid name collisions with production classes
@dataclass
class Sample(Versionable, version=1, register=False):
value: int
Common use cases:
Test classes — test suites often define throwaway classes with generic names like
Sample. Withoutregister=False, these collide with each other across tests.Abstract base classes — intermediate classes that define shared fields but are never saved to disk.
Multiple versions in one process — if you need two definitions of the same schema (e.g. for migration testing), only one can be registered.
Duplicate name detection: if two registered classes share the same serialization name, versionable raises a
VersionableError at class definition time with a suggested fix:
VersionableError: Versionable name 'MyClass' is already registered to
mypackage.models.MyClass. Give one of the classes an explicit name to
disambiguate, e.g.: class MyClass(Versionable, ..., name="other.MyClass")
Nested Deserialization¶
When load() deserializes a Versionable field, list/dict/tuple/set element, or any value reachable from the root,
three things happen at every level:
Polymorphism resolution. The per-element envelope’s
objectname is looked up in the global registry. If found and the resolved class is a subclass of the declared field type, the resolved class is used —list[Animal]saved withDogandCatelements reconstructs as a list ofDogandCatinstances. If the envelope is missing or the name matches the declared type, the declared type is used (back-compat).Migration. If the file’s recorded version is less than the resolved class’s current version, the resolved class’s
Migratechain runs against the field data before deserialization. If newer,load()raisesVersionErroridentifying the nested type.Unknown-field handling. The resolved class’s
unknown="error"/"ignore"/"preserve"setting governs extra fields after migration.
See migrations.md for declarative and imperative migration syntax, including nested examples.
Field Serialization Rules¶
A field is included in serialization if it:
Has a type annotation
Does not have a leading underscore in its name
Is not a
ClassVar
@dataclass
class Example(Versionable, version=1, hash="..."):
included: int # serialized
_private: int = 0 # excluded — underscore prefix
class_var = "constant" # excluded — no annotation
CONSTANT: ClassVar[int] = 42 # excluded — ClassVar
metadata()¶
Inspect the schema metadata registered for a class:
from versionable import metadata
meta = metadata(SensorConfig)
meta.version # int — current version
meta.hash # str — 6-char hash
meta.name # str — serialization name
meta.fields # list[str] — serialized field names
Versionable.hash()¶
Compute the schema hash for a Versionable subclass. Use this to get the value to put in the hash= parameter:
from dataclasses import dataclass
from versionable import Versionable
@dataclass
class MyConfig(Versionable, version=1):
name: str
value: float
print(MyConfig.hash()) # e.g. "4b7866"
Then add the result to the class definition:
@dataclass
class MyConfig(Versionable, version=1, hash="4b7866"):
name: str
value: float
Reserved Keys¶
The following keys are used internally by versionable and must not be used as field names or as keys in user-provided dict values.
The __versionable__ envelope at the root of every saved file holds the schema metadata (no dunders inside — the
wrapper key is the namespace marker):
Key |
Purpose |
|---|---|
|
Serialization class name (stored in |
|
Schema version (stored in |
|
Schema hash (stored in |
|
Reserved for future versionable file format versioning |
User-data sentinels (live alongside user values; the __ver_*__ prefix marks them as package-owned):
Key |
Purpose |
|---|---|
|
Versionable metadata envelope (wrapper key — namespace marker) |
|
Marks a dict as a serialized numpy array (JSON/YAML/TOML) |
|
YAML/TOML wrapper for values with no native encoding |
⚠️ Warning: Using any of these as a field name or dict key may cause incorrect serialization or deserialization.
Compatibility with 0.1.x files¶
Files written by versionable 0.1.x used dunder forms inside the envelope (__OBJECT__, __VERSION__, __HASH__,
__FORMAT__) and bare sentinels (__ndarray__, __json__). Throughout the 0.2.x line, load() accepts both the old
and new keys, preferring the new ones. Saved files always use the new keys. The legacy read path will be removed in 1.0.