In
init_subclass.py, add a classYellow(Color)and thenGold(Yellow). PredictColor.registryafter each new class, then confirm.
Self-Registration
of Subclasses shows __init_subclass__() running
once for every new subclass. Trace what
__init_subclass__() adds and what it removes for
Yellow, then for Gold. Write your
predicted sets down before you run the listing.
# The shape of exercise_1.py
from typing import ClassVar
class Color:
registry: ClassVar[set[type[Color]]] = set()
def __init_subclass__(cls, **kwargs: object) -> None:
...
class Blue(Color):
pass
class Red(Color):
pass
class Green(Color):
pass
class PhthaloBlue(Blue):
pass
class CeruleanBlue(Blue):
pass
class Yellow(Color):
pass
class Gold(Yellow):
pass# exercise_1.py
from typing import ClassVar
class Color:
registry: ClassVar[set[type[Color]]] = set()
def __init_subclass__(cls, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
Color.registry.add(cls)
Color.registry -= set(cls.__bases__)
class Blue(Color):
pass
class Red(Color):
pass
class Green(Color):
pass
class PhthaloBlue(Blue):
pass
class CeruleanBlue(Blue):
pass
class Yellow(Color):
pass
print(sorted(c.__name__ for c in Color.registry))
#: ['CeruleanBlue', 'Green', 'PhthaloBlue', 'Red', 'Yellow']
class Gold(Yellow):
pass
print(sorted(c.__name__ for c in Color.registry))
#: ['CeruleanBlue', 'Gold', 'Green', 'PhthaloBlue', 'Red']Register a new leaf. Creating
Yellow adds it to the registry and removes its only
base, Color, which is not in the registry, so that
removal changes nothing. Yellow stays until a
subclass of its own arrives.
Drop a base that gains a child. Creating
Gold adds it and removes its base,
Yellow, the same pruning PhthaloBlue
and CeruleanBlue do to Blue earlier.
__init_subclass__() runs for every new subclass, so
each new generation adds itself and prunes its parent
automatically, with no edit to Color.
Field
descriptorIn
set_name.py, add a thirdField()attribute,z, toPoint, setp.z = 9, and confirmp.__dict__now also holds_z.
A
Descriptor That Learns Its Name shows
__set_name__() receiving the attribute name when
Python creates the class. __set_name__() runs once
per descriptor instance, so a third attribute needs a third
Field() and no new code. Compare
p.__dict__ with the attribute names on
Point.
# The shape of exercise_2.py
from typing import Any
class Field:
def __set_name__(self, owner: type, name: str) -> None:
...
def __get__(self, obj: Any,
owner: type | None = None) -> Any:
...
def __set__(self, obj: Any, value: Any) -> None:
...
class Point:
x = Field()
y = Field()
z = Field()# exercise_2.py
from typing import Any
class Field:
def __set_name__(self, owner: type, name: str) -> None:
self.name = name
self.storage = "_" + name
def __get__(self, obj: Any,
owner: type | None = None) -> Any:
if obj is None:
return self
return getattr(obj, self.storage)
def __set__(self, obj: Any, value: Any) -> None:
setattr(obj, self.storage, value)
class Point:
x = Field()
y = Field()
z = Field()
p = Point()
p.x = 3
p.y = 4
p.z = 9
print(p.x, p.y, p.z)
#: 3 4 9
print(p.__dict__)
#: {'_x': 3, '_y': 4, '_z': 9}z = Field() needs no change to the
Field class. __set_name__() runs once
per descriptor, at class-creation time. Python calls it
separately for each of x, y, and
z, passing each one its own attribute name, so
z’s Field instance learns the name
"z" and stores under "_z",
independently of the other two.
In
singleton.py, add a third classCSingleton(metaclass=Singleton)and confirmc1 = CSingleton(); c2 = CSingleton(); c1 is c2isTrue, whilec1 is a(comparing across the different singleton classes) isFalse.
Intercepting
Instance Creation shows a metaclass __call__()
that caches one instance per class. The cache is a dictionary
keyed by the class, so each class that uses the metaclass gets
its own entry. Add CSingleton with the same
metaclass= argument and compare identities with
is.
# The shape of exercise_3.py
from typing import Any, ClassVar
class Singleton(type):
_instances: ClassVar[dict[type, Any]] = {}
def __call__[T](
cls: type[T], *args: Any, **kwargs: Any) -> T:
...
class ASingleton(metaclass=Singleton):
pass
class CSingleton(metaclass=Singleton):
pass# exercise_3.py
from typing import Any, ClassVar
class Singleton(type):
_instances: ClassVar[dict[type, Any]] = {}
def __call__[T](
cls: type[T], *args: Any, **kwargs: Any) -> T:
if cls not in Singleton._instances:
Singleton._instances[cls] = type.__call__(
cls, *args, **kwargs)
return Singleton._instances[cls]
class ASingleton(metaclass=Singleton):
pass
class CSingleton(metaclass=Singleton):
pass
a = ASingleton()
c1 = CSingleton()
c2 = CSingleton()
print(c1 is c2)
#: True
print(c1 is a)
#: FalseType the result as the calling class. The
__call__[T] signature is the chapter’s, and this
solution keeps it rather than simplifying to
-> Any. It ties the return type to
cls, so CSingleton() type-checks as a
CSingleton, and the type checker still flags a
misspelled attribute on the result. Two details follow from that
annotation. cls: type[T] hides the fact that
cls is a Singleton, so the body writes
type.__call__(cls, ...), where ty
rejects a zero-argument super(). For the same
reason the body reads the cache through the class name,
Singleton._instances, rather than through
cls.
Keep one instance per class.
Singleton._instances is a dictionary keyed by the
class, so each class using the Singleton metaclass
gets its own independent slot: ASingleton’s single
instance, BSingleton’s single instance (omitted
here, but present in the book), and now
CSingleton’s. Calling CSingleton()
twice returns the same object both times.
ASingleton occupies its own key in that dictionary,
so its instance is a separate object.
Extend
final_runtime.pyso a class declares itself final with a keyword in its header,class B(A, final=True):, using the**kwargsthat__init_subclass__()receives. Confirm that a non-final sibling ofBstill subclasses freely.
Making
a Class Final shows __init_subclass__()
refusing a subclass at runtime. Python passes the keyword
arguments in a class header to __init_subclass__(),
so give that method a final parameter with a
default. Record each final class in a set on the base, and check
cls.__mro__ when a new subclass appears.
# The shape of exercise_4.py
from typing import ClassVar
from exceptions import expected
class A:
_final: ClassVar[set[type]] = set()
def __init_subclass__(cls, final: bool = False,
**kwargs: object) -> None:
...
class B(A, final=True):
pass
class Open(A): # A sibling that says nothing
pass
class Sub(Open):
passIf you declare final without a default,
class B(A, final=True): still builds, but
class Open(A): fails with
TypeError: A.__init_subclass__() missing 1 required positional argument: 'final'.
The solution gives final the default
False, so a subclass that is not final can leave
the keyword out of its header.
# exercise_4.py
from typing import ClassVar
from exceptions import expected
class A:
_final: ClassVar[set[type]] = set()
def __init_subclass__(cls, final: bool = False,
**kwargs: object) -> None:
super().__init_subclass__(**kwargs)
for base in cls.__mro__[1:]:
if base in A._final:
raise TypeError(
f"{base.__name__} is final;"
" you cannot subclass it")
if final:
A._final.add(cls)
class B(A, final=True):
pass
class Open(A): # A sibling that says nothing
pass
class Sub(Open):
pass
print(issubclass(Sub, A))
#: True
with expected(TypeError):
class C(B):
pass
#: [TypeError] B is final; you cannot subclass itAccept the header keyword. The keywords in a
class header travel to __init_subclass__(), so
final=True in class B(A, final=True):
arrives as a parameter of the method Python calls when it
creates B. Declaring final with a
default, final: bool = False, lets every other
subclass omit it.
Pass the other keywords up. The remaining
**kwargs go on to
super().__init_subclass__(), which turns a
misspelled keyword into a TypeError instead of a
silent no-op.
Refuse any descendant of a final class. The
chapter’s final_runtime.py
hard-codes the refusal into B’s own
__init_subclass__(). This version moves the
decision into a set that A owns, and
A.__init_subclass__() walks
cls.__mro__ to ask whether any ancestor declared
itself final. A check of the direct bases in
cls.__bases__ would also refuse every descendant:
A.__init_subclass__() refuses the first subclass of
a final class as Python creates that subclass, so no deeper
descendant exists. The walk stays because it states the rule as
written, “no final class anywhere above,” in one line.
Open and Sub show that the rest of the
hierarchy still subclasses freely:
A.__init_subclass__() raises a
TypeError only for a class whose
__mro__ holds one of the classes in
A._final.
inspect-based describe() helperUsing
inspect_tour.pyas a model, write a functiondescribe(func)that prints a function’s name, itsinspect.signature(), and its docstring (or"(no docstring)"ifinspect.getdoc()returnsNone), then call it ongreetand on a lambda.
The
Core Functions lists the inspect calls for
signatures and docstrings. Call inspect.signature()
and inspect.getdoc() on the function, and print the
function’s __name__. Since getdoc()
returns None for a missing docstring, supply the
fallback text with or.
# The shape of exercise_5.py
import inspect
from types import FunctionType
def greet(name: str, loud: bool = False) -> str:
"Return a greeting."
...
def describe(func: FunctionType) -> None:
...# exercise_5.py
import inspect
from types import FunctionType
def greet(name: str, loud: bool = False) -> str:
"Return a greeting."
text = f"Hello, {name}"
return text.upper() if loud else text
def describe(func: FunctionType) -> None:
doc = inspect.getdoc(func)
sig = inspect.signature(func)
print(func.__name__, sig)
print(" ", doc or "(no docstring)")
describe(greet)
#: greet (name: str, loud: bool = False) -> str
#: Return a greeting.
describe(lambda x: x * 2)
#: <lambda> (x)
#: (no docstring)inspect.getdoc() returns None when
a callable has no docstring, so
doc or "(no docstring)" supplies a fallback message
instead of printing None. A lambda
always has a name, "<lambda>", so
func.__name__ works uniformly on both a
def-based function and a lambda, with
no special case needed to tell them apart.
TypeErrorDelete the
# type: ignorecomment frommetaclass_layout_conflict.pyand runtyover the file. Compare theinstance-layout-conflictdiagnostic it reports with theTypeErrorthe program prints. The static report and the runtime failure describe the same collision.
Multiple
Inheritance and Metaclasses introduces the instance layout
conflict in metaclass_layout_conflict.py.
Remove the # type: ignore comment and run
uv run ty check on the file. Read both the summary
line and the info block of the diagnostic, and
compare them with the message the TypeError
carries.
Removing the # type: ignore from metaclass_layout_conflict.py
leaves the class header unsuppressed, inside the listing’s
with expected(TypeError): block:
with expected(TypeError):
class Singleton(type, dict[type, Any]):
passuv run ty check metaclass_layout_conflict.py
then reports:
error[instance-layout-conflict]: Class will raise `TypeError` at runtime
due to incompatible bases
--> metaclass_layout_conflict.py:6:11
|
6 | class Singleton(type, dict[type, Any]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Bases `type` and `dict`
| cannot be combined in multiple inheritance
info: Two classes cannot coexist in a class's MRO if their instances
have incompatible memory layouts
--> metaclass_layout_conflict.py:6:21
|
6 | class Singleton(type, dict[type, Any]):
| ---- --------------- `dict` instances have a
| | distinct memory layout
| | because of the way `dict`
| | is implemented in a C
| | extension
| `type` instances have a distinct memory layout
| because of the way `type` is implemented in a C
| extension
Running the same file prints
[TypeError] multiple bases have instance lay-out conflict.
The diagnostic and the exception describe one collision.
ty’s summary line names the consequence, “Class
will raise TypeError at runtime.” Its
info block explains the rule the interpreter
enforces without explaining: type and
dict are both implemented in C, each with its own
instance layout, so no single object can be both. CPython
discovers that conflict while executing the class
statement and reports it as the terse “instance lay-out
conflict.” ty reaches the same conclusion from the
class header alone, before anything runs, and points at both
bases to say which pair is at fault.
The runtime message tells you something collided. The static
one tells you which two bases collided and why, at the moment
you type the header rather than the moment Python first imports
the module. The chapter’s metaclass_layout_conflict.py
carries the # type: ignore because that listing
exists to show the TypeError, and a suppressed
diagnostic is the cost of demonstrating a crash on purpose.
float subclass with type()Using
type()directly, build a classCelsiuswith a base offloat, an attributeunit = "C", and a methoddescribe(self)returningf"{self} degrees {self.unit}". ConfirmCelsius(21.5).describe()works and thattype(Celsius)istype.
Generating
Classes with type() shows the three-argument
form of type(): a name, a tuple of bases, and a
namespace dictionary. Define describe() as an
ordinary function, then put it in the dictionary beside
unit. Because a function is a descriptor, the
lookup on an instance binds it as a method.
# The shape of exercise_7.py
from typing import Any
def describe(self: Any) -> str:
...If you write the bases as (float), without the
trailing comma, the parentheses group an expression and build no
tuple, so type() raises a TypeError:
type.__new__() argument 2 must be tuple, not type.
The solution writes (float,), a one-element tuple,
which the three-argument form requires for its bases.
# exercise_7.py
from typing import Any
def describe(self: Any) -> str:
return f"{self} degrees {self.unit}"
Celsius = type("Celsius", (float,),
{"unit": "C", "describe": describe})
c = Celsius(21.5)
print(c.describe())
#: 21.5 degrees C
print(type(Celsius) is type)
#: True
print(c + 0.5, isinstance(c, float))
#: 22.0 TrueDefine the method as a function.
describe() annotates self as
Any because the type checker cannot know that this
loose function ends up on a class carrying a unit
attribute. The Any annotation is the cost of
building a class from data rather than from a class
statement.
Assemble the class from data. The three
arguments are the name, the bases, and the namespace, the same
three a class statement assembles for you. A
function defined at module level becomes a method when you put
it in that namespace dict. It needs no decoration, because a
function is a descriptor: the attribute lookup binds it to the
instance.
Confirm the metaclass.
type(Celsius) is type because this
listing calls type() as a constructor rather than
subclassing it. Nothing here involves a metaclass of your
own.
Inherit the base’s arithmetic.
Celsius inherits float’s arithmetic,
so c + 0.5 works, though the sum is a
float rather than a Celsius:
float.__add__() builds its result from
float, and that is why a numeric subclass usually
overrides every operator whose result should keep the subclass’s
type.
bases += (Tag,) into __init__()In
new_vs_init.py, move thebases += (Tag,)line from__new__()into__init__()and predict the outcome before running it. Explain the result in terms of when the class object comes into existence.
__init__()
versus __new__() in a Metaclass contrasts what
each method can still change. Ask whether the class object
exists yet when the metaclass __init__() begins.
Then consider what bases += (Tag,) does to a
parameter, and print Demo.__bases__ to check your
prediction.
# The shape of exercise_8.py
from typing import Any
class Tag:
pass
class Meta(type):
def __init__(cls, name: str, bases: tuple[type, ...],
nmspc: dict[str, Any]) -> None:
# Rebinds a local name, nothing else
...
class Demo(metaclass=Meta):
passThe prediction: the move changes nothing. Tag
stays out of Demo.__bases__, and Python raises no
error.
# exercise_8.py
from typing import Any
class Tag:
pass
class Meta(type):
def __init__(cls, name: str, bases: tuple[type, ...],
nmspc: dict[str, Any]) -> None:
# Rebinds a local name, nothing else
bases += (Tag,)
super().__init__(name, bases, nmspc)
class Demo(metaclass=Meta):
pass
print(Demo.__bases__)
#: (<class 'object'>,)
print(Tag in Demo.__bases__)
#: FalseBy the time __init__() runs, the class object is
complete. type built it inside
__new__(), using the bases the class header
supplied there, and laid out its __mro__ from them.
The bases parameter of __init__()
reports the tuple __new__() used rather than
choosing a new one, so bases += (Tag,) rebinds a
local name, and Demo.__bases__ stays the same.
Passing the longer tuple on to type.__init__()
changes nothing either, since type.__init__()
checks its arguments and then ignores them.
new_vs_init.py makes the
same point from the other side, with its
added_in_init key. __new__() must make
every decision about what the class is: its name, its
bases, and the namespace type builds it from.
__init__() receives the completed class object and
can change it in place, which is why
setattr(cls, ...) still works there.
KNOWN_COMMANDS check
commander.pyvalidatesclass_nameagainstKNOWN_COMMANDSbefore splicing it into source text. Remove that check, callCommand.make_class()with a name containing a newline and a second statement, and confirm that the injected statement runs.make_class()splices the name in twice, the second time inside a string literal, so a bare newline ends the payload as an unterminated string. The payload’s last line must close or swallow that second splice. Restore the check.
The
Injection Risk explains why exec() on text
built from a caller’s string is dangerous.
make_class() inserts the name into the source
twice, so build a payload that stays valid Python at both
places. Open a triple-quoted string in the payload’s last line
so it absorbs the second insertion, and catch the
KeyError the final lookup raises.
# The shape of ch17_exec_injection.py
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, cast
@dataclass
class Command:
label: str
def run(self) -> str:
...
@classmethod
def make_class(
cls, class_name: str
) -> Callable[[], Command]:
# The KNOWN_COMMANDS check has been removed:
...# ch17_exec_injection.py
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, cast
@dataclass
class Command:
label: str
def run(self) -> str:
return f"Running {self.label}"
@classmethod
def make_class(
cls, class_name: str
) -> Callable[[], Command]:
# The KNOWN_COMMANDS check has been removed:
klass = f"""
class {class_name}(Command):
def __init__(self) -> None:
super().__init__("{class_name}")
"""
namespace: dict[str, Any] = {"Command": Command}
exec(klass, namespace)
return cast(Callable[[], Command],
namespace[class_name])
attack = (
'X(Command):\n'
' pass\n'
'print("injected code ran")\n'
'Y = """ #'
)
try:
Command.make_class(attack)
except KeyError:
print("lookup failed, after the injection ran")
#: injected code ran
#: lookup failed, after the injection ranBreak out of the class block.
print("injected code ran") is not part of any class
body. It runs at module level inside exec(), and
that is the danger: a name that reaches
make_class() unchecked becomes source code, and
source code can do anything the program can do.
Absorb the second splice. The payload needs
a little care, because make_class() splices
class_name in twice. The first splice supplies the
attack lines. The second puts them inside the
super().__init__("...") string literal, where a
bare newline is a SyntaxError before anything runs.
So the payload’s last line opens a triple-quoted string,
Y = """. That string swallows the second splice,
and the trailing # comments out the ")
left over after the string closes. The spliced source compiles,
and the injected print() runs at module level
inside exec(), after the class body has
finished.
Catch the failed lookup. The
KeyError afterward is incidental damage, not
protection. namespace[class_name] looks for a class
named after the whole payload, which the spliced source does not
define. The injected statement ran before that lookup, so
failing the lookup rescues nothing. Restoring the
if class_name not in cls.KNOWN_COMMANDS check
closes the hole at the only point that works: before
make_class() builds the string.
Change
prepare_namespace.py’sNoDuplicatesso that instead of raising an exception, it keeps the first definition of a duplicated name and discards the later one. Give the twoon_openbodies differentprint()calls so you can tell them apart, then confirm thatHandlers().on_open()runs the first one. Explain why no class decorator could achieve the same thing.
When
You Still Need a Metaclass shows __prepare__()
supplying the mapping into which a class body writes. Subclass
dict and override __setitem__() so a
repeated key returns without storing. For the explanation,
consider which class-creation steps run before the body and
which run after it.
# The shape of ch17_keep_first.py
from typing import Any
class KeepFirst(dict[str, Any]):
def __setitem__(self, key: str, value: Any) -> None:
...
class First(type):
@classmethod
def __prepare__(cls, name: str, bases: tuple[type, ...],
**kwargs: Any) -> KeepFirst:
...
class Handlers(metaclass=First):
def on_open(self) -> None:
...
def on_open(self) -> None: # noqa: F811
...If you leave @classmethod off
__prepare__(), Python’s call fills
self with the class name and name with
the bases. The class Handlers statement then fails
with
TypeError: First.__prepare__() missing 1 required positional argument: 'bases',
a message that says nothing about the missing decorator. Python
calls __prepare__() on the metaclass before any
class object exists, so the solution keeps the decorator, as When
You Still Need a Metaclass requires.
# ch17_keep_first.py
from typing import Any
class KeepFirst(dict[str, Any]):
def __setitem__(self, key: str, value: Any) -> None:
if key in self:
return # Discard the later definition
super().__setitem__(key, value)
class First(type):
@classmethod
def __prepare__(cls, name: str, bases: tuple[type, ...],
**kwargs: Any) -> KeepFirst:
return KeepFirst()
class Handlers(metaclass=First):
def on_open(self) -> None:
print("first on_open")
def on_open(self) -> None: # noqa: F811
print("second on_open")
Handlers().on_open()
#: first on_openDiscard a repeated name.
NoDuplicates raises an exception on a repeated key.
KeepFirst returns instead, so Python builds the
second on_open function, hands it to
__setitem__(), and the mapping discards it. The
name still refers to the first function when the body finishes,
as Handlers().on_open() proves.
No class decorator can keep the first definition, and neither
can __init_subclass__() or
__set_name__(). All three receive the class after
its body has finished executing, and by then the body has run
on_open = <second function> as an ordinary
assignment into the namespace mapping. The first function has no
name pointing at it and no reference anywhere, so none of the
three has anything to restore. Among the class-creation steps,
__prepare__() alone sees the assignments one at a
time, as the body makes them, and that is why the chapter calls
it the one with no simpler substitute.