A stock entity changes over time while retaining its identity. A priced order line can instead represent a fact: a SKU, a quantity and a captured unit price. Two independently created lines with the same three fields should compare equal. This is value semantics. The existing record lesson introduced validated frozen configuration; here the design work is equality, hashing, replacement and the identity used to combine priced lines.
@dataclass(frozen=True) generates initialization, representation and field-based
equality, and normally generates a hash for this configuration. Each field in
our value contains an immutable string or integer. Frozen assignment protection
alone would not make a list-valued field deeply immutable, and an unhashable
field could still prevent hashing the whole record.
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Line:
sku: str
units: int
unit_cents: int
def __post_init__(self):
if self.units <= 0 or self.unit_cents < 0:
raise ValueError("invalid line")
@property
def subtotal(self):
return self.units * self.unit_cents
first = Line("tea", 2, 125)
same = Line("tea", 2, 125)
changed = replace(first, units=3)
print(first == same, first is same)
print(len({first, same}), first.subtotal)
print(first.units, changed.units)This example assumes the shown field types; the build requires exact runtime
validation in addition to annotations. Generated __init__ invokes
__post_init__, making it the right place to reject invalid direct construction.
dataclasses.replace creates another instance through initialization, so the
same validation applies to replacement.
Replacement is different from mutation
with_units(5) means the new value has five units, even if the old value had
two. It does not mean add five. Return a new record and retain the original
unchanged. If the replacement is invalid, neither value needs a rollback:
construction raises while the original record remains valid.
Equal objects must have equal hashes, although unequal objects can share a hash.
For ordinary set and dictionary use, equality-relevant state must remain stable.
This is why the lesson combines frozen records with immutable fields rather
than making a mutable StockItem hash depend on its changing quantity. Identity
comparison with is asks a different question from field equality with ==.
Aggregation has its own identity rule
A line’s full equality includes units, but merging uses the pair (sku, unit_cents). Quantities are what we add. The same SKU at two different historical
prices must remain separate or the merge would silently reprice some units.
Compare the sum of subtotals before and after aggregation as a conservation test.
Sorting by SKU and then price makes output deterministic even if inputs arrived in a different order. Integer cents keep the arithmetic exact, including free items and very large counts. Empty input returns an empty tuple, while an individual line still requires a positive quantity.
Predict equality, identity and set behavior in the checks. Fix the subtotal and overwrite bugs, fill price-aware aggregation, then complete OrderLine and its replacement method. The build intentionally asks for domain value behavior rather than another configuration parser or a hierarchy of passive records.