jangada.serialization.SerializableProperty#

class jangada.serialization.SerializableProperty(postinitializer: Callable[[object], T] | None = None, default: T | Callable[[object], T] | None = None, parser: Callable[[object, Any], T] | None = None, observers: set[Callable[[object, T, T], None]] | None = None, writeonce: bool = False, copiable: bool = True, doc: str | None = None)#

Bases: object

A descriptor for properties that support defaults, parsing, observation, and post-initialization hooks.

SerializableProperty provides a rich descriptor implementation designed for scientific computing applications where properties need validation, change tracking, lazy initialization, and eventual serialization to disk (e.g., HDF5).

Parameters:
postinitializerPostinitializer | None, optional

Function called once after the property is first set or accessed. Receives the instance as its only argument. Useful for lazy setup of related state or expensive initialization. Default is None.

defaultT | DefaultCallable | None, optional

Default value for the property. Can be: - A static value (used directly) - A callable receiving the instance and returning the default - None (property defaults to None) Setting the property to None resets it to this default. Default is None.

parserParser | None, optional

Function to parse/validate values before storage. Receives the instance and raw value, returns the parsed value. Applied to both explicitly set values and defaults. Exceptions raised by the parser propagate to the caller. Default is None.

observersset[Observer] | None, optional

Set of functions called after the property value changes. Each observer receives (instance, old_value, new_value). Observers are called after parsing and storage, but before post-initialization. Default is None.

writeoncebool, optional

If True, the property can only be set once (including initialization). Subsequent sets raise AttributeError. Useful for immutable configuration. Default is False.

copiablebool, optional

Flag indicating whether this property should be included when copying or serializing instances. Used by serialization systems to determine which properties to persist. Default is True.

docstr | None, optional

Documentation string for the property. Default is None.

Attributes:
namestr

Name of the property (set by __set_name__).

ownertype

Class that owns this descriptor (set by __set_name__).

private_namestr

Internal attribute name used to store the value on instances.

writeoncebool

Whether this property can only be set once.

copiablebool

Whether this property should be included in serialization.

See also

property

Python’s built-in property descriptor

dataclasses.field

Similar concept for dataclasses

Notes

  • Properties are stored in instance.__dict__ with mangled names to avoid conflicts with user attributes.

  • Setting a property to None resets it to its default value.

  • Observers are called on first access (with old_value=None) when the property is initialized with its default.

  • Post-initializers run after the first set completes, allowing them to safely access the property value.

  • Parsers are applied to default values as well as explicitly set values.

  • All decorator methods (.default(), .parser(), etc.) return new descriptor instances rather than mutating the existing one.

  • Properties cannot be deleted (raises AttributeError).

Examples

Basic usage with static default:

>>> class Experiment:
...     temperature = SerializableProperty(default=293.15)
...
>>> exp = Experiment()
>>> exp.temperature
293.15
>>> exp.temperature = 373.15
>>> exp.temperature
373.15

Using a callable default for mutable objects:

>>> class DataContainer:
...     data = SerializableProperty(default=lambda self: [])
...
>>> c1 = DataContainer()
>>> c2 = DataContainer()
>>> c1.data.append(1)
>>> c2.data.append(2)
>>> c1.data
[1]
>>> c2.data
[2]

Using a parser for validation:

>>> class PositiveValue:
...     value = SerializableProperty(
...         default=1.0,
...         parser=lambda self, v: max(0.0, float(v))
...     )
...
>>> obj = PositiveValue()
>>> obj.value = -5
>>> obj.value
0.0

Using decorators for cleaner syntax:

>>> class System:
...     data = SerializableProperty()
...
...     @data.default
...     def data(self):
...         return {"initialized": True}
...
...     @data.parser
...     def data(self, value):
...         if not isinstance(value, dict):
...             raise TypeError("data must be a dict")
...         return value
...
...     @data.add_observer
...     def data(self, old, new):
...         print(f"Data changed from {old} to {new}")

Using post-initializer for lazy setup:

>>> class LazyLoader:
...     data = SerializableProperty(default=None)
...
...     @data.postinitializer
...     def data(self):
...         print("Loading expensive data...")
...         if self.data is None:
...             self.data = list(range(1000))
...
>>> loader = LazyLoader()
>>> # No output yet - not accessed
>>> _ = loader.data
Loading expensive data...
>>> len(loader.data)
1000

Write-once property for configuration:

>>> class Config:
...     api_key = SerializableProperty(writeonce=True)
...
>>> cfg = Config()
>>> cfg.api_key = "secret123"
>>> cfg.api_key = "different"  # Raises AttributeError
Traceback (most recent call last):
    ...
AttributeError: api_key is a write-once property and has already been set