CodeRigor

FREE LESSON

Value objects, equality & replacement

Design immutable priced-line values with generated equality/hash behavior, validated replacement and price-aware aggregation.

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.

Entity versus value
start / 2
An entity is observed as it changes. A priced line is replaced with a new value, preserving the earlier fact.
Entity versus value
An entity is observed as it changes. A priced line is replaced with a new value, preserving the earlier fact.
Python
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 ==.

Only equal pricing keys merge
start / 2
Two tea lines priced at 125 cents combine their units; a 150-cent tea line remains separate.
Only equal pricing keys merge
Two tea lines priced at 125 cents combine their units; a 150-cent tea line remains separate.

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.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Focus on how special methods integrate an object with Python operations. Dataclasses generate equality and hashing under documented options; use the official dataclasses reference for frozen records and replace.