Auto Decorators¶
@auto_freeze, @auto_eq, and @auto_hash are lightweight decorators that add immutability, structural equality, and hashing to plain Python classes — no inheritance from a base class required.
@auto_freeze¶
Enforces immutability after __init__ completes. After construction, any attempt to assign a frozen attribute raises CantModifyImmutableAttributeError.
Usage¶
@auto_freeze— Freezes the entire instance@auto_freeze()— Equivalent, explicit parens form@auto_freeze(attrs=["_id"])— Selectively freeze specific attributes only
The decorator handles __init__ nesting (inheritance chains) and skips abstract classes.
from forging_blocks.foundation import auto_freeze
@auto_freeze
class Money:
__slots__ = ("_amount", "_currency")
def __init__(self, amount: int, currency: str) -> None:
self._amount = amount
self._currency = currency.upper()
m = Money(100, "usd")
m._amount = 200 # raises CantModifyImmutableAttributeError
Selective freezing:
from forging_blocks.foundation import auto_freeze
@auto_freeze(attrs=["_id"])
class User:
__slots__ = ("_id", "_name")
def __init__(self, user_id: str, name: str) -> None:
self._id = user_id
self._name = name
u = User("usr-1", "Alice")
u._name = "Bob" # ok — not frozen
u._id = "usr-2" # raises CantModifyImmutableAttributeError
auto_freeze injects a __setattr__ override and a freeze flag. It detects existing custom __setattr__ implementations and avoids double-wrapping.
@auto_eq¶
Generates __eq__ for class instances based on their fields. Does not generate __hash__ — combine with @auto_hash when hashability is required.
Usage¶
@auto_eq— Equality on all fields@auto_eq()— Equivalent, explicit parens form@auto_eq(fields=["x", "y"])— Equality on specific fields only
Comparison is type-strict: type(self) is type(other) must be true, so subclasses are not equal to their parent.
Generated members¶
__eq__— structural equality on the resolved fields__auto_eq_fields__— tuple of field names used in equality, available for introspection
Examples¶
from forging_blocks.foundation import auto_eq
@auto_eq
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
assert p1 == p2
assert p1 != p3
With explicit field selection:
from forging_blocks.foundation import auto_eq
@auto_eq(fields=["x"])
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
# y is ignored — only x matters
assert Point(1, 2) == Point(1, 999)
assert Point(1, 2) != Point(2, 999)
@auto_hash¶
Generates __hash__ for class instances based on their fields. Mutable values (lists, dicts) are automatically converted to hashable equivalents (tuples, frozensets). Does not generate __eq__ — combine with @auto_eq for explicit equality.
Usage¶
@auto_hash— Hash on all fields@auto_hash()— Equivalent, explicit parens form@auto_hash(fields=["x", "y"])— Hash on specific fields only
Equal objects produce equal hashes — suitable for sets and dict keys. Raises NonHashableValueError at hash time if a field value cannot be converted to a hashable type.
Generated members¶
__hash__— hash computed from the resolved field values, with mutable-to-hashable conversion__auto_hash_fields__— tuple of field names used in hashing, available for introspection
Examples¶
from forging_blocks.foundation import auto_hash
@auto_hash
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
assert hash(p1) == hash(p2)
assert hash(p1) != hash(p3)
With mutable field conversion:
from forging_blocks.foundation import auto_hash
@auto_hash
class Tagged:
__slots__ = ("name", "tags")
def __init__(self, name: str, tags: list[str]) -> None:
self.name = name
self.tags = tags
a = Tagged("item", ["a", "b"])
b = Tagged("item", ["a", "b"])
assert hash(a) == hash(b) # list converted to tuple automatically
Selective fields:
from forging_blocks.foundation import auto_hash
@auto_hash(fields=["id"])
class Entity:
__slots__ = ("id", "name")
def __init__(self, id: str, name: str) -> None:
self.id = id
self.name = name
e1 = Entity("1", "Alice")
e2 = Entity("1", "Bob")
assert hash(e1) == hash(e2) # only id matters
Combining Decorators¶
The three auto decorators compose with each other and with @dataclass. Stacking order matters:
| Position | Decorator | Role |
|---|---|---|
| Top (outermost) | @auto_hash |
Generates __hash__ |
@auto_eq |
Generates __eq__ |
|
@auto_freeze |
Enforces immutability | |
| Bottom (innermost) | @dataclass |
Generates __init__, __repr__, etc. |
Note
@auto_freeze must be applied before @dataclass but after @auto_eq / @auto_hash so that equality and hashing test the frozen fields.
auto_eq + auto_hash (POPO)¶
Combining @auto_hash and @auto_eq on a plain Python class gives both structural equality and hashability:
from forging_blocks.foundation import auto_hash, auto_eq
@auto_hash
@auto_eq
class Point:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
assert p1 == p2 # structural equality
assert hash(p1) == hash(p2) # consistent hashing
assert p1 in {p2} # usable in sets
auto_freeze with Other Decorators (POPO)¶
Add @auto_freeze below @auto_eq / @auto_hash to make the class immutable after construction:
from forging_blocks.foundation import auto_hash, auto_eq, auto_freeze
@auto_hash
@auto_eq
@auto_freeze
class Point:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p = Point(1.0, 2.0)
p.x = 5.0 # raises CantModifyImmutableAttributeError
With @dataclass¶
When combining with @dataclass, place the auto decorators above it (outermost first). @dataclass generates __eq__ by default, so @auto_eq is optional here.
@auto_hash only¶
from dataclasses import dataclass
from forging_blocks.foundation import auto_hash
@auto_hash
@dataclass
class User:
id: str
name: str
u1 = User("1", "Alice")
u2 = User("1", "Alice")
d = {u1: "data"}
assert d[u2] == "data"
Full stack with @dataclass¶
from dataclasses import dataclass
from forging_blocks.foundation import auto_hash, auto_eq
@auto_hash
@auto_eq
@dataclass
class Point:
x: float
y: float
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
assert p1 == p2
assert hash(p1) == hash(p2)
Full stack with @auto_freeze¶
from dataclasses import dataclass
from forging_blocks.foundation import auto_hash, auto_eq, auto_freeze
@auto_hash
@auto_eq
@auto_freeze
@dataclass
class Money:
amount: int
currency: str
m = Money(100, "usd")
m.amount = 200 # raises CantModifyImmutableAttributeError
Decorator Stacking Order¶
The decorator order determines how each layer sees the class:
@auto_hash(top) — receives the class after@auto_eqand@auto_freeze/@dataclasshave applied. It generates__hash__based on the fields visible at that point.@auto_eq— receives the class after@auto_freeze/@dataclasshave applied. It generates__eq__.@auto_freeze— receives the class after@dataclass(if present). It wraps__setattr__to prevent mutations.@dataclass(bottom) — generates__init__,__repr__, and optionally__eq__.
Why @auto_freeze goes below @auto_eq / @auto_hash
Placing @auto_freeze below @auto_eq / @auto_hash ensures the equality and hash methods are generated before @auto_freeze potentially alters attribute access patterns. The freeze applies after all structural methods are in place.