Skip to content

← Modelling Data with Dataclasses step 3 of 28

Easy Primitives

Find Mutable Defaults That Instances Share

Python creates a field's default value once, when it defines the class. If that value is a list, every object that uses the default shares one list. The dataclass decorator refuses items: list[str] = [], but it misses other cases. The Bag class in the starter holds a list, and the decorator accepts bag: Bag = Bag(). A default factory causes the same sharing if it returns the same list every time.

Write an inspector that finds these fields.

solve("Leaky")
# ["bag", "rows"]

What to write

Fill in audit_defaults(cls). Loop over dataclasses.fields(cls) and return the names of the fields you flag, sorted alphabetically.

  • Inspect every field, including fields with init=False.
  • Do not create an instance of cls, and do not change its fields.
  • dataclasses.MISSING is the marker that a field's default and default_factory hold when you gave none. Test each one with is not MISSING. A default of None, 0 or "" is a real default.
  • Never flag a field that has neither a default nor a factory.

Rule 1: a plain default

Flag the field unless its default passes this check:

  • Accept a value when isinstance matches any of type(None), bool, int, float, complex, str, bytes or type. Use isinstance, not an exact type comparison.
  • Accept a tuple or frozenset when every element passes this same check. An empty one passes.
  • Reject every other value.

So (1, "x") passes, and (1, Bag()) fails because a Bag is none of the accepted types.

Rule 2: a default factory

  • Call the factory exactly twice, and flag the field if the two results are the same object.
  • Compare them with is, not ==, and do not apply rule 1 to them.
  • Keep both results alive while you compare them. id(f()) == id(f()) can be True for two new lists, because Python frees the first list and reuses its memory for the second.

list, dict and set build a new container on each call. _shared_factory and _shared_index return the same module-level object each time, so your inspector flags their fields.

Expected reports

Keep the classes and solve as they are. solve takes a name from REGISTRY and returns your report:

Target Result
Safe []
Leaky ["bag", "rows"]
Nested ["combo"]
Empty []
Cache ["index"]

Leaky.bag fails rule 1. Leaky.rows fails rule 2. Leaky.ok passes, because its factory returns a new list on each call. Cache.seeded has init=False and a list factory, so you inspect it and it passes.

Your code must pass mypy --strict. DataclassInstance is a type that exists only for the type checker and means "any dataclass instance". That is why the starter imports it under if TYPE_CHECKING: and writes it in quotes. Keep both as they are.