We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Modelling Data with Dataclasses step 14 of 28
Check a Class Before You Add slots=True
Before you add slots=True to a group of dataclasses, you want a list of
the classes that might break. The article Save Memory with slots=True named
the two changes that cause trouble. Objects lose __dict__, and the
decorator returns a new class. Write a checker that reads a class and names
the risks it finds.
solve("Clean", [3, 12], []) # []
solve("WithCachedProperty", [3, 12], []) # ["cached-property"]
solve("UsesZeroArgSuper", [3, 12], []) # ["zero-arg-super"]
solve("UsesZeroArgSuper", [3, 14], []) # []
Terms you need
- A descriptor is an object stored on a class. It runs code when you
read or set the attribute of the same name on an instance.
propertyandcached_propertyare descriptors. A descriptor can also define__set_name__(owner, name), which Python calls when it creates the class. - Zero-argument
super()is a call tosuper()with no arguments inside a method. Python finds the class through a hidden variable named__class__, which holds the class you wrote the method in. On Python 3.12,slots=Trueleaves__class__pointing at the old class, so the call raisesTypeError. Python 3.14 fixed this. Recent 3.13 patch releases have the fix too, but older ones do not. func.__code__.co_freevarslists the names a function reads from an enclosing scope. A method that callssuper()has"__class__"there.func.__code__.co_nameslists the global and attribute names the code uses.self.__dict__puts"__dict__"there.
What to write
Fill in slots_safety_check:
def slots_safety_check(
cls: "type[DataclassInstance]",
target_python: tuple[int, int],
migrating: frozenset[str],
) -> list[str]:
- Return the reasons sorted alphabetically, each one once.
- Only read the class. Do not change it, create instances, call its
methods or apply
dataclass(slots=True). target_pythonis the Python version you plan to deploy on. Use it only in the comparison with(3, 14), and ignoresys.version_info.
Check each entry the class defines
Loop over vars(cls).items(), the names and values the class defines
itself. Skip inherited entries. For each value:
- Add
"cached-property"if the value is afunctools.cached_property. It stores its result in__dict__. - Otherwise, add
"set-name-descriptor"ifhasattr(type(value), "__set_name__")is true. Python calls__set_name__with the original class, beforeslots=Truereplaces it, so the descriptor remembers the wrong class. A value never gets both reasons.
Then call the supplied _as_function(value). It returns the function
inside a method, a classmethod or a staticmethod, and None for
anything else, such as a property or a cached_property. When it returns
a function func, add every reason below that matches:
"zero-arg-super"if"__class__"is infunc.__code__.co_freevarsandtarget_python < (3, 14)."instance-dict-access"if"__dict__"is infunc.__code__.co_names."init-subclass-kwargs"if the entry's name is__init_subclass__andinspect.signature(func)has more than one parameter. Countclsand parameters with defaults. When a subclass getsslots=True, the decorator builds its new class without the keywords from itsclassline. A required parameter then raisesTypeError, and one with a default gets the default.
Do not look inside nested functions, property getters or other callable objects.
Check the base classes
A class cannot inherit from two unrelated classes that both have slots.
Python raises TypeError: multiple bases have instance lay-out conflict.
The rule here only approximates Python's. Loop over cls.__mro__[1:], every class
that cls inherits from, and skip object. Count an ancestor when either
of these is true:
- Its
__name__is inmigrating, the classes about to getslots=True. bool(getattr(base, "__slots__", ()))is true, so it already has slots.
If more than one ancestor counts, add "multiple-slotted-bases". cls
itself is not in cls.__mro__[1:], so here only the two mixins count:
solve("Combined", [3, 14], ["LeftMixin", "RightMixin", "Combined"])
# ["multiple-slotted-bases"]
What solve does
Keep solve and the fixture classes as they are. solve looks up
target in REGISTRY, turns the [major, minor] list into a tuple and
turns migrating into a frozenset. Every input names a class in
REGISTRY.
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.
The zero-argument super() fix is
CPython issue 90562.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.