Skip to content

← Data Modelling and Invariants step 14 of 25

Medium Primitives

field(): the seven knobs that change generated behaviour

repr=False on a credential field is the difference between a live token sitting in your log aggregator forever and not. Every logger.info("built %s", creds), every unhandled-exception frame dump, every pytest assertion diff calls __repr__. The default @dataclass repr prints every field, verbatim.

field() is the per-field escape hatch. Nine knobs:

knob effect
default the value, evaluated once at class-creation time
default_factory a zero-arg callable, invoked on every construction
init include as an __init__ parameter
repr include in the generated __repr__
compare include in __eq__ and in __lt__/__le__/… under order=True
hash include in __hash__; None (the default) means follow compare
metadata an arbitrary mapping, exposed read-only; ignored by the generator
kw_only make this one field keyword-only
doc per-field docstring (⚠ 3.14)

Two of those are sharper than they look.

compare=False also removes the field from __hash__. Because hash defaults to None, and None means “do whatever compare does”. So on a frozen dataclass, marking a field compare=False silently changes its hash contract as well as its equality contract. That is usually what you want (a cached derived value should be in neither), but it is worth knowing it is one decision, not two.

metadata is a MappingProxyType. Not a copied dict you can mutate — a genuinely read-only view, frozen at class creation. It is also the documented namespace third-party serializers key off: cattrs, dataclasses-json, and every in-house codec you will ever write puts its per-field hints there. Conventionally you namespace your keys so two libraries do not collide.

Why the repr is the right place to enforce this

Not __str__ — nothing calls __str__ in a traceback. Not a to_log() helper — nobody remembers to call it. __repr__ is the method that fires when your code is not being careful, which is exactly when you need the redaction.


Your task

Implement:

def redacted_repr(obj: DataclassInstance) -> str:

It renders like the builtin dataclass repr — ClassName(a=1, b='x') — with three differences:

  1. a field whose metadata contains {"sensitive": True} renders its value as the literal five characters '***' (an apostrophe, three stars, an apostrophe — i.e. what repr("***") would give);
  2. a field with repr=False is omitted entirely, not redacted;
  3. a field whose value is itself a dataclass instance is rendered by recursing, so nested secrets are redacted too.

Everything else uses repr(value) — including Mapping values, which render exactly as their own repr does.

solve(sample) looks an instance up in SAMPLES and returns its redacted repr.

Types. Field.metadata is typed Mapping[Any, Any], so f.metadata["sensitive"] is Any — and Any propagates silently into whatever you assign it to. Convert at the boundary (bool(f.metadata.get("sensitive", False))) so nothing untyped escapes. For the recursion you need to narrow object to a dataclass instance; is_dataclass(x) alone narrows to the union DataclassInstance | type[DataclassInstance], so the stdlib’s own idiom is is_dataclass(x) and not isinstance(x, type).