We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Data Modelling and Invariants step 21 of 25
__post_init__ and InitVar: parameters that are consumed, not stored
The way a psycopg connection ends up inside a JSON response body is this: someone
needed a database handle inside a domain object, stored it as a real field, and
six months later a different someone wrote json.dumps(asdict(order)). InitVar
is the mechanism that stops it — a constructor parameter that is consumed,
not kept.
__post_init__
The generated __init__ ends with a call to self.__post_init__(...) if and
only if the class defines one. Facts worth having in muscle memory:
-
It is not called at all when
init=False, because there is no generated__init__to call it from. Putting your invariant check there and then addinginit=Falsesilently disables the check. -
A dataclass
__init__never callssuper().__init__(). If you inherit from a non-dataclass that needs initialising,__post_init__is where you do it, by hand. -
On a frozen dataclass,
self.x = ...inside__post_init__raisesFrozenInstanceErrorlike anywhere else. The sanctioned escape isobject.__setattr__(self, "x", value)— which is exactly what the generated frozen__init__does for every field.
InitVar[T]
rounding: InitVar[str] = "half_even" produces a field entry whose _field_type
is _FIELD_INITVAR. Consequences:
-
it becomes an
__init__parameter, in declaration order, with its default; -
it is forwarded positionally to
__post_init__, in declaration order, so the parameter names in__post_init__do not have to match — but the order does; -
it is not stored on the instance, and does not appear in
fields(),__repr__,__eq__,asdict()orreplace().
That last line is the whole point. The value shapes construction and then disappears. A database session, a clock, a feature-flag snapshot, a rounding policy — all belong here, not in a field.
The derived-field pattern
minor_units: int = field(init=False, default=0)
init=False keeps it out of the constructor; __post_init__ computes it once
and writes it with object.__setattr__. It is in fields(), repr, eq
and asdict(), because it is a real part of the value. (replace() will
recompute it — see the replace() lesson.)
Your task
Complete the frozen Money:
-
amount: Decimalandcurrency: strare ordinary fields; -
minor_units: intis derived —init=False, computed in__post_init__; -
rounding: InitVar[str]defaults to"half_even"and is never stored.
__post_init__ must:
-
raise
ValueError(f"unknown ISO-4217 currency: {currency!r}")if the currency is not inMINOR_UNIT_EXPONENT(note the!r— the message is asserted); -
raise
ValueError(f"unknown rounding mode: {rounding!r}")for an unknown mode; -
otherwise set
minor_unitsto the amount expressed in the currency’s smallest unit, quantized with the requested rounding. USD has exponent 2 (so10.00→1000), JPY has exponent 0 (1234→1234), KWD has 3. UseDecimal(1).scaleb(-exponent)as the quantize step andint(quantized.scaleb(exponent))to get the integer.
Then implement solve(amount, currency, rounding) -> dict[str, object] returning:
-
"error"— the exception message, or""on success (with the other keys empty/false); -
"field_names"—[f.name for f in fields(money)]; -
"as_dict"—asdict(money)with every value passed throughstr(); -
"eq_vs_half_even"— whether thisMoneyequals the same amount and currency built with"half_even".
That last key is the sharp edge. rounding is not a field, so equality never
looks at it — but minor_units is, and rounding changes it. 1.005 USD
rounds to 100 under half-even and 101 under half-up, so the two objects are
not equal even though the thing that made them differ is invisible in the
repr. An InitVar is excluded from equality; its effects are not.
Types. mypy models __post_init__ as overriding a method on a synthetic
supertype, so if you get an InitVar parameter type wrong the error is reported
against “supertype dataclass“ — confusing the first time, unambiguous once
you have seen it.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.