@Data#
The all-in-one decorator: one line gives a class a constructor, __repr__,
__eq__, and a getter/setter per field (plus __hash__ when frozen).
The problem it solves#
A class that just holds a few fields still needs a constructor, a readable
repr, value-based equality, and get_x/set_x accessors.
Writing those by hand is repetitive and drifts out of sync whenever a field
is added or renamed. @Data derives all of them from the class’s
annotations, so there is nothing to keep in sync.
Usage#
from inito import Data
@Data
class User:
name: str
email: str
age: int = 0
user = User("Ada", "[email protected]", age=30)
print(user) # User(name='Ada', email='[email protected]', age=30)
print(user == User("Ada", "[email protected]", 30)) # True
print(user.get_name()) # Ada
user.set_age(31)
What it generates#
Member |
Behaviour |
|---|---|
|
every field, in declaration order (a required field after a defaulted one is rejected, as in |
|
|
|
value equality; different classes compare |
|
only when frozen — a mutable |
|
one per field (unless |
|
one per field (unless |
Fields are the class’s annotated attributes, accumulated across the MRO
(base-class fields first). ClassVar-annotated attributes are ignored.
Options#
@Data can be used bare (@Data) or configured (@Data(...)):
Option |
Default |
Effect |
|---|---|---|
|
|
make instances immutable (assignment/deletion raise |
|
|
set |
|
|
set |
|
|
accessor style: |
|
|
set |
@Data(accessors="attr") # Pythonic: no get_x/set_x, just user.name
accessors="attr" is the Pythonic choice for new code — the attribute is the
accessor. The mypy plugin honors it, so get_/set_ disappear from the typed
surface too.
@Data(slots=True) # smaller instances, no accidental attributes
slots=True recreates the class with __slots__ (Python fixes slots at class
creation, so they can’t be added in place). Defaults, accessors,
__post_init__, super(), and weakref all keep working, and the mypy plugin
models the slots. When stacking with @Builder, put @Builder on the outside
(@Builder above @Data(slots=True)) so it targets the rebuilt class. For the
full memory win every base class should use slots too.
@Data(frozen=True) # immutable value object, no setters
@Data(include_setters=False) # read-only accessors, but still mutable via obj.x = ...
@Data(include_getters=False) # no getters
frozen=True and @Value both give a genuinely immutable class; @Value
is the more descriptive choice when immutability is the point (see
@Value).
Notes & gotchas#
Mutable defaults are rejected. A shared mutable literal (
tags: list = []) would be one object across every instance — the classic Python footgun — so InitO raises at decoration time. Usefield(default_factory=...)for a fresh object per instance:from inito import Data, field @Data class Config: tags: list = field(default_factory=list) # a fresh list per instance
A mutable
@Datais unhashable. Likedataclasses(eq=True, frozen=False), a non-frozen@Datasets__hash__toNone, so a mutated instance can’t silently break its ownset/dictmembership. Use@Data(frozen=True)or @Value for a hashable value type. (A@Datastacked on a frozen dataclass stays hashable.)Your own methods are untouched.
@Dataattaches only the members you did not write: a hand-written__repr__,__eq__, or__init__in the class body is left exactly as-is. (Methods synthesized by a stacked@dataclassare still taken over.)include_setters=Falseis not immutability — it only omits theset_xhelpers;obj.x = 5still works. Usefrozen=True(or@Value) to actually forbid mutation.
See also#
@Value —
@Datawithout setters, always immutable.Accessors, @ToString, @EqualsAndHashCode — the atomic pieces
@Databundles.