Ordinary code makes objects. Metaprogramming works one level up, on the classes that make them.
Every object comes from another, special object. These special objects are classes, and you configure them to produce the objects you want.
Classes are also objects, and you can modify objects.
# modify_class.py
from display import display_object
class Clay:
pass
display_object(Clay)
#: [Attributes]
#: None
#: [Methods]
#: None
x = Clay()
Clay.n = 42 # type: ignore
Clay.m = lambda self: f"{self.n = }" # type: ignore
display_object(x)
#: [Attributes]
#: • n = 42 [CV]
#: [Methods]
#: • m(self)
print(x.m()) # type: ignore
#: self.n = 42
print(vars(x))
#: {}Attribute lookup on an instance falls through to its class,
so a lookup on any object of that class finds a change to the
class, including on objects created before the change. The
instance stays as it was. vars(x) shows
x’s instance dictionary still empty.
What creates these “class” objects?
Other special objects, called metaclasses. The default
metaclass is type, and it serves almost every
class. You can customize how Python produces classes by running
extra code or injecting members as it builds each class. That is
metaclass programming.
You have used metaclasses without
writing one. abc.ABCMeta builds abc.ABC,
and makes a class with an unimplemented abstract method refuse
instantiation. enum.EnumType builds each
Enum subclass, turning every class-body assignment
into a member and making for c in Color walk them.
Iterating over a class is behavior on the class object, and a
metaclass puts behavior there. An ordinary class cannot.
You rarely need a metaclass. It is a fascinating tool and tempting to use, but simpler hooks cover almost every case a metaclass handled before those hooks existed:
__init_subclass__()
runs at subclass creation. It replaces most “do something at
each class definition” metaclasses.__set_name__() gives
a class attribute its name, at class-creation time.Use a metaclass only when these cannot do the job.
type()Since metaclasses create classes,
you can call the metaclass yourself. type() with
one argument gives the type of an existing object.
type() with three arguments creates a new class.
These arguments are the name, a tuple of base classes, and a
namespace dictionary of fields and methods. A class definition
is shorthand for calling type():
# class_via_type.py
class C:
pass
D = type("D", (), {}) # The class statement, as a call
print(type(C), type(D))
#: <class 'type'> <class 'type'>
# Both inherit object:
print(C.__bases__, D.__bases__)
#: (<class 'object'>,) (<class 'object'>,)
# Both make ordinary instances:
print(isinstance(C(), C), isinstance(D(), D))
#: True TrueThe tuple of bases and the namespace dictionary add bases, fields, and methods:
# my_list.py
from display import display_object
def howdy(self, you: str) -> None:
print(f"Howdy, {you}")
MyList = type("MyList", (list,), dict(x=42, howdy=howdy))
display_object(MyList)
#: [Attributes]
#: • x = 42 [CV]
#: [Methods]
#: • append(self, object, /)
#: • clear(self, /)
#: • copy(self, /)
#: • count(self, value, /)
#: • extend(self, iterable, /)
#: • howdy(self, you: str) -> None
#: • index(self, value, start=0, stop=92233720368547758...
#: • insert(self, index, object, /)
#: • pop(self, index=-1, /)
#: • remove(self, value, /)
#: • reverse(self, /)
#: • sort(self, /, *, key=None, reverse=False)
ml = MyList()
ml.append("Camembert")
print(ml)
#: ['Camembert']
print(ml.x)
#: 42
ml.howdy("John")
#: Howdy, John
print(ml.__class__.__class__)
#: <class 'type'>Printing the class of the class produces the metaclass.
Generating classes with type() suits a family of
classes that differ only by name. A greenhouse controller runs
scheduled events, one class per kind of event. One module holds
the base class Event, the seven event names, and
make(), which builds the class for one name:
# events.py
from collections.abc import Callable
from dataclasses import dataclass
from typing import Final
type Hour = int
type Minute = int
@dataclass
class Event:
action: str
hour: Hour
minute: Minute
type EventMaker = Callable[[Hour, Minute], Event]
NAMES: Final[frozenset[str]] = frozenset({
"ThermostatDay", "ThermostatNight", "LightOn",
"LightOff", "WaterOn", "WaterOff", "RingBell",
})
def make(name: str) -> EventMaker:
def init(self: Event, hour: Hour,
minute: Minute) -> None:
Event.__init__(self, name, hour, minute)
new_cls = type(name, (Event,), {"__init__": init})
return new_clsA dict comprehension builds all seven classes:
# eager_event_classes.py
from events import NAMES, Event, make
makers = {name: make(name) for name in NAMES}
print(len(makers))
#: 7
light = makers["LightOn"](1, 0)
water = makers["WaterOff"](2, 0)
print(light)
#: LightOn(action='LightOn', hour=1, minute=0)
print(isinstance(light, Event))
#: True
print(type(light) is type(water))
#: FalseEach generated class is a real type, not a label.
light and water belong to distinct
subclasses of Event, so isinstance()
with one of those subclasses tells them apart.
Type checkers read different
amounts of a type() call. ty reads the
bases and a namespace written as a dict literal. It takes the
constructor from the "__init__" entry, so it infers
that each generated class takes an hour and a minute. It then
checks return new_cls against
EventMaker, the two-argument signature
make() declares. If init() gains a
third parameter, ty reports an
invalid-return-type that names the extra parameter.
Pyright builds the class from its bases alone, so it gives the
class Event’s three-argument constructor and
rejects return new_cls. Under Pyright
make() needs
cast(EventMaker, new_cls). Under mypy the result is
an unknown class, so mypy accepts either form.
Under ty a cast replaces a check with a claim,
the distinction Where
Enforcement Lives draws. The cast states the signature. The
checker takes that statement on trust.
make() exists so that
each init() closes over its own name.
A lambda written inline in the comprehension closes over the
comprehension’s variable, so every generated class records the
last name the comprehension visits as its action.
That is the late-binding
trap that late_binding.py
demonstrates.
init() calls
Event.__init__(self, ...) by name instead of
super().__init__(...). It is a nested function, not
a method defined inside a class statement, so the
compiler gives it no __class__ cell for
zero-argument super() to use.
The dict comprehension in eager_event_classes.py
builds all seven classes whether the schedule uses them or not.
Seven classes are few enough to build up front. With hundreds,
the schedule uses few of the classes the comprehension builds.
So the next version delays building each class until the first
lookup of its name. Delaying the build takes a dict
subclass that calls make() from events.py when a lookup
misses:
# greenhouse.py
from events import NAMES, EventMaker, make
class EventMakers(dict[str, EventMaker]):
def __missing__(self, name: str) -> EventMaker:
if name not in NAMES:
raise KeyError(f"Unknown event class: {name!r}")
print(f"Creating {name}")
new_cls = make(name)
self[name] = new_cls
return new_cls
def built(self) -> str:
return (f"{len(self)} of {len(NAMES)}"
" event classes built")EventMakers
subclasses dict, so the call site
makers[name] reads as an ordinary lookup. When the
key is absent, dict.__getitem__() calls
__missing__() and returns its result. The next
lookup of that name finds the stored class, so
__missing__() builds each class once. Without
__missing__(), every caller checks for the name and
builds the missing class.
A new EventMakers starts out empty.
__missing__() checks a name against
NAMES before building anything, so every key the
dict acquires names a declared event.
# greenhouse_demo.py
from typing import Final, cast
from greenhouse import EventMakers
SCHEDULE: Final[tuple[tuple[str, int, int], ...]] = (
("LightOff", 2, 0),
("LightOn", 1, 0),
("RingBell", 7, 0),
("LightOn", 8, 0),
)
makers = EventMakers()
events = [makers[name](hour, minute)
for name, hour, minute in SCHEDULE]
#: Creating LightOff
#: Creating LightOn
#: Creating RingBell
print(makers.built())
#: 3 of 7 event classes built
bell = cast(type, makers["RingBell"])
for event in sorted(
events, key=lambda e: (e.hour, e.minute)):
prefix = "* " if isinstance(event, bell) else ""
print(f"{prefix}{event.hour}:{event.minute:02d}"
f": {event.action}")
#: 1:00: LightOn
#: 2:00: LightOff
#: * 7:00: RingBell
#: 8:00: LightOnOn the first lookup of an event type,
EventMakers builds the class and registers it under
its name, so LightOn gets one Creating
line though SCHEDULE names it twice. An unknown
name raises a KeyError, as a missing key in any
dict does, so a
try: ... except KeyError around a lookup catches
it.
The demo’s loop over the sorted events uses the type
distinction. A distinct subclass lets each event carry its kind
as its type, where isinstance() or a
match class pattern can test it.
Calling the base class,
Event(name, hour, minute), sets the same
action, hour, and minute
as a generated class does, but every entry shares one type. The
loop then has no RingBell class for
isinstance() to test. The * marker
depends on RingBell being a distinct class, not
just a distinct name.
The demo wraps its RingBell lookup in
cast(type, ...). EventMakers declares
its values as EventMaker, a callable, and
isinstance() requires a class, so without the cast
the type checker reports an invalid-argument-type.
Every value __missing__() stores is a class that
type() built, so the cast states that each value is
a class, a fact the declared type leaves out.
exec()The type() approach
in events.py builds a class
from a name, a tuple of bases, and a namespace dict. A second
way is to write an ordinary class statement in an
f-string, then exec() that string as code. Here,
klass holds that class body, which is easier to
read and modify than a namespace dict:
# commander.py
from collections.abc import Callable
from dataclasses import dataclass
from typing import ClassVar, cast
from exceptions import expected
@dataclass
class Command:
label: str
KNOWN_COMMANDS: ClassVar[set[str]] = {
"Start", "Stop", "Pause"}
def run(self) -> str:
return f"Running {self.label}"
@classmethod
def make_class(
cls, class_name: str) -> Callable[[], Command]:
if class_name not in cls.KNOWN_COMMANDS:
raise ValueError(
f"Unknown command: {class_name!r}")
klass = f"""
class {class_name}(Command):
def __init__(self) -> None:
super().__init__("{class_name}")
"""
namespace: dict[str, object] = {"Command": Command}
exec(klass, namespace)
return cast(Callable[[], Command],
namespace[class_name])
for name in ("Start", "Stop", "Pause"):
command_class = Command.make_class(name)
print(command_class().run())
#: Running Start
#: Running Stop
#: Running Pause
with expected(ValueError):
Command.make_class("Reset")
#: [ValueError] Unknown command: 'Reset'make_class() execs
klass into a private namespace dict,
not the module’s namespace, and seeds it with
{"Command": Command} so the generated class can
find its base. exec() also adds a
__builtins__ entry, the dictionary of built-in
names, to any globals mapping that lacks one, so
namespace holds more than classes.
The namespace argument also lets
make_class() read the generated class back as
namespace[class_name]. Called inside a function
with no namespace argument, exec() writes into a
snapshot of the function’s local variables. Since Python 3.13
(PEP 667), each locals() call in a function returns
a new, independent snapshot, so a later locals()
call misses a class that exec() defines there. A
stack frame’s f_locals attribute returns a
write-through proxy, the supported way for a debugger to change
a function’s variables.
The type checker skips the code
inside the string, so namespace carries the
annotation dict[str, object], and
namespace[class_name] is an object.
Returning an object where make_class()
declares Callable[[], Command] draws an
invalid-return-type.
cast(Callable[[], Command], ...) states the actual
no-argument signature at the one place that creates the
class.
EventMakers stores each class it builds.
make_class() builds a fresh class on every call.
Calling make_class("Start") twice produces two
distinct classes.
The __init__() in
klass sits textually inside a class
block, and the compiler treats a block compiled from a string
the same as one read from a file. So this
__init__() gets a __class__ cell and
calls zero-argument super(). init() in
events.py is a nested
function rather than a method in a class body, so it has no such
cell and names Event.__init__() instead.
The klass string is
the danger in this approach. exec() runs its
argument with the full power of the language, and
klass splices class_name into source
text as-is. A name can hold a newline followed by a second
statement. Without validation, that statement breaks out of the
class block and runs anything. An unescaped value
in a hand-built SQL query breaks out the same way.
make_class() checks
class_name against KNOWN_COMMANDS
before it builds klass. Only three known names
reach the template. make() in events.py is safe from
injection, because type(name, (Event,), ...) treats
name as a string value, not as source code. Treat
exec() and eval() like string-built
SQL: safe on values you’ve validated, dangerous on anything that
reaches the program from outside, unchecked.
Both generators,
type() and exec(), have a second
limitation, unrelated to injection. exec()’s
private namespace has no __name__ key,
so the class body’s lookup of __name__ falls
through to the built-ins, and the class gets
__module__ set to "builtins".
pickle.dumps() on an instance then raises a
PicklingError, because pickle looks the class up as
builtins.Start and does not find it.
inspect.getsource() raises a
TypeError, because inspect treats the
class as a built-in, compiled into the interpreter.
type()-built classes
fail differently. type() sets
LightOn‘s __module__ to
events, the module where make() calls
type(). But the class lives in the
makers dict, not as an attribute of
events. Pickle therefore looks for
events.LightOn and does not find that either.
inspect.getsource() raises an OSError
instead of a TypeError. For both generators’
classes, then, pickle and
inspect.getsource() raise exceptions.
inspect.signature() and
inspect.getmembers_static() still work, as
display_object(MyList) shows.
Pickle records a class as its
module and name, and checks that the pair leads back to the
class. If make() also binds each class as an
attribute of its module, globals()[name] = new_cls,
the pair events.LightOn leads to
LightOn, and the round trip works.
inspect.getsource() still raises an
OSError, because the class comes from a
type() call rather than a class
statement in a source file.
Often a base class needs to keep
track of its subclasses. Tracking subclasses is the textbook
justification for a metaclass. Python calls
__init_subclass__() automatically for every new
subclass, so a base class can register its subclasses in a few
lines. Here, each base class tracks its “leaf” subclasses (those
with no subclasses), using __init_subclass__()
instead of a metaclass:
# init_subclass.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)
# Keep only the leaves
Color.registry -= set(cls.__bases__)
class Blue(Color):
pass
class Red(Color):
pass
class Green(Color):
pass
print(sorted(c.__name__ for c in Color.registry))
#: ['Blue', 'Green', 'Red']
class PhthaloBlue(Blue):
pass
class CeruleanBlue(Blue):
pass
print(sorted(c.__name__ for c in Color.registry)) # [1]
#: ['CeruleanBlue', 'Green', 'PhthaloBlue', 'Red']
# A second, independent hierarchy keeps its own registry:
class Shape:
registry: ClassVar[set[type[Shape]]] = set()
def __init_subclass__(cls, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
Shape.registry.add(cls)
Shape.registry -= set(cls.__bases__)
class Round(Shape):
pass
class Square(Shape):
pass
class Circle(Round):
pass
print(sorted(c.__name__ for c in Shape.registry))
#: ['Circle', 'Square']Each new subclass removes its base classes from the registry,
which is why Blue is absent from the
Color print at [1] and
Round is absent from the Shape
registry. None of this needs a metaclass.
__init_subclass__() is implicitly a class
method. Its first argument is the new subclass.
__init_subclass__() runs for classes derived from
the class whose body defines it, and not for that class, so
neither Color nor Shape appears in its
registry.
The keyword arguments come from
the subclass header. Writing
class Blue(Color, shade="cool"): delivers
shade="cool" to __init_subclass__(),
so a subclass can configure its registration. The base that
declares the keyword takes it as a parameter,
def __init_subclass__(cls, shade: str = "", **kwargs: object),
so the keyword binds to shade and
kwargs collects the rest.
super().__init_subclass__(**kwargs) passes
kwargs up the chain, so a base further up can take
the keywords it declared. If a keyword remains when the chain
reaches object.__init_subclass__(), that method
raises a TypeError.
Testing shows that each registry holds only its current leaf classes, and that the two registries are disjoint:
# test_init_subclass.py
import init_subclass
def test_color_registry_holds_only_leaves() -> None:
leaves = {c.__name__
for c in init_subclass.Color.registry}
assert leaves == {"Red", "Green", "PhthaloBlue",
"CeruleanBlue"}
def test_independent_hierarchies_have_separate_registries(
) -> None:
shapes = {c.__name__
for c in init_subclass.Shape.registry}
assert "Round" not in shapes
assert shapes == {"Square", "Circle"}
# Neither registry leaks into the other
assert init_subclass.Shape.registry.isdisjoint(
init_subclass.Color.registry)__init_subclass__()
runs for every subclass definition Python executes. The
registries built on it fail in two ways that every
self-registering design shares, with or without
__init_subclass__(). Factory
covers both: a class in a module nobody imports stays out of the
registry, and keying on cls.__name__ lets two
same-named classes overwrite each other.
Functions and classes carry two
names. __name__ is the bare name from the
def or class header.
__qualname__, the qualified name from PEP
3155, prefixes that name with the classes and functions that
enclose the definition. A class Inner defined in
the body of Outer has the qualified name
Outer.Inner. init() in events.py has the
qualified name make.<locals>.init, where
<locals> marks a definition inside a function
body. A registry or a decorator that keys on a name usually
needs the qualified one, combined with __module__.
Pickle finds a class or function again through that same
pair.
A function is an object, and like most objects it carries a
__dict__. Assigning f.x = 1 stores
1 in f.__dict__ under the key
"x", and the entry stays as long as the function
exists. A decorator can use that storage to mark a method. A
method decorator runs as Python executes the class body, before
the class object exists, so it cannot register the method with
its class. It can leave a mark on the function, and
__init_subclass__() collects the marks once Python
builds the class:
# marked_methods.py
from collections.abc import Callable
from typing import Any, ClassVar
def on[F: Callable[..., object]](
event: str
) -> Callable[[F], F]:
def mark(func: F) -> F:
func.__dict__["event"] = event
return func
return mark
class Widget:
handlers: ClassVar[dict[str, Callable[..., Any]]] = {}
def __init_subclass__(cls, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
marked = {
attr.__dict__["event"]: attr
for attr in vars(cls).values()
if "event" in getattr(attr, "__dict__", {})
}
cls.handlers = {**cls.handlers, **marked}
def dispatch(self, event: str) -> None:
self.handlers[event](self)
class Button(Widget):
@on("click")
def press(self) -> None:
print("pressed")
@on("hover")
def glow(self) -> None:
print("glowing")
def label(self) -> str:
return "OK"
class SubmitButton(Button):
@on("submit")
def send(self) -> None:
print("sent")
print(sorted(Button.handlers))
#: ['click', 'hover']
print(sorted(SubmitButton.handlers))
#: ['click', 'hover', 'submit']
Button().dispatch("click")
#: pressed
print(Button.press.__dict__)
#: {'event': 'click'}
# ty: Function `press` has no attribute `event`:
# print(Button.press.event)mark() returns the same F it
receives, so press() keeps its signature through
the decoration.
When the class Button
statement finishes, __init_subclass__() walks
vars(cls), the new class’s own namespace, and files
each function that carries an "event" key under its
event. The namespace also holds the bookkeeping entries every
class carries, such as __module__, a string with no
__dict__. For those entries, the {}
default in getattr() makes the "event"
test false.
On the right side of the
assignment, cls.handlers still resolves to the base
class’s dictionary, so each new table starts from the inherited
handlers. handlers holds the functions the class
body defined, not bound methods, so dispatch()
passes self to the function it finds.
The listing writes func.__dict__["event"] rather
than func.event to satisfy the type checker. A
function’s type declares a closed set of attributes, so the type
checker reports Button.press.event as an unresolved
attribute, and it reports the assignment
func.event = event the same way. Every object’s
type declares __dict__ as a
dict[str, Any], so indexing it type-checks.
The standard library marks
functions the same way. @abstractmethod sets
__isabstractmethod__ = True on the function.
ABCMeta collects the marked names into the class’s
__abstractmethods__, and
object.__new__() raises a TypeError
for a class whose __abstractmethods__ is nonempty.
@final sets __final__, and
@override sets __override__. near_miss.py in Template
Method reads __final__ through
getattr() to refuse an override at runtime.
functools.wraps() copies the wrapped function’s
__dict__ onto the wrapper, so the wrapper of a
decorator written with @wraps also carries any mark
applied beneath it.
A mark suits data that describes
the function and that other code reads later, as
handlers does here. A call count, or any state that
changes with each call, belongs in a closure variable or on a callable
object, where the type checker verifies its type.
Sometimes you need to forbid inheritance. The modern way to
say so is the typing.final decorator:
# final.py
from typing import final
@final
class B:
pass
# ty: Class `C` cannot inherit from final class `B`:
# class C(B): pass
b = B()
print(type(b).__name__)
#: BType checkers such as
ty, mypy, and Pyright check @final
statically. At runtime the decorator marks the class by setting
__final__ = True, as test_final.py below
confirms. The interpreter ignores that mark, so it still runs
class C(B): pass.
Older literature claims that
making the interpreter refuse subclassing requires a metaclass.
__init_subclass__() does the job at each subclass
creation:
# final_runtime.py
from exceptions import expected
class A:
pass
class B(A):
def __init_subclass__(cls, **kwargs: object) -> None:
raise TypeError(
f"{B.__name__} is final; "
f"you cannot subclass it")
with expected(TypeError):
class C(B):
pass
#: [TypeError] B is final; you cannot subclass itThe check runs at class-creation time. Python builds
B normally. A class’s own
__init_subclass__() does not run for that class. At
B’s creation, Python runs the version
B inherits from A, which is
object’s default and accepts every subclass.
A decorator like
@final is a marker. It sets an attribute
and leaves enforcement to the type checker. Use the runtime
version in the rare case where the interpreter must refuse the
subclass too.
The test file confirms that:
@final marker is present.# test_final.py
import final
import final_runtime
import pytest
def test_final_decorator_marks_class() -> None:
assert final.B.__final__ is True # type: ignore
def test_runtime_final_cannot_be_subclassed() -> None:
with pytest.raises(TypeError):
class Sub(final_runtime.B):
pass
def test_runtime_non_final_base_can_be_subclassed() -> None:
class Ok(final_runtime.A):
pass
assert issubclass(Ok, final_runtime.A)Each device for constraining a class answers one question: who enforces a rule about a class, and when? The language devices you have met divide into four families.
@final and
@override are markers. At runtime each sets a
single attribute, __final__ or
__override__, that the interpreter ignores and your
own code can read, as near_miss.py does. The
type checker enforces each marker’s rule, and Making a Class Final builds the
runtime half with __init_subclass__().
@dataclass
is mirrored machinery. At runtime it is a code
generator, synthesizing __init__() and its siblings
at class-creation time. The type checker does not run the
decorator. It recognizes the name and re-implements the
generator’s rules statically: the synthesized signature, the
frozen write-ban, the field-ordering rule. The typing
specification mandates those rules, so every conformant checker
derives the same class. Some rules end up enforced twice,
independently. If you declare a field without a default after
one with a default, the checker reports that field before
anything runs, while the interpreter raises its own
TypeError at class creation.
A third family carries two
real semantics. @abstractmethod makes the
checker report an abstract instantiation, and separately makes
the runtime refuse one. assert_never()
proves exhaustiveness statically and raises an
AssertionError at runtime when a value outside the
declared type reaches it.
In the fourth family, a library
enforces the checker’s types at runtime. Annotations remain in
the running program, as The
inspect Module shows, so a library can read
them and check values as the program runs.
@dataclass_transform
Is a ClaimWhere does the checker get its rules for
@dataclass? For the standard library, the checker’s
authors wrote those rules into the checker. For a class-building
decorator of your own, the checker’s rules come from what you
declare. @dataclass_transform marks a decorator as
dataclass-like, and the declaration alone changes the class the
checker infers:
# claimed_transform.py
from typing import dataclass_transform
from exceptions import expect
@dataclass_transform()
def model[T](cls: type[T]) -> type[T]:
return cls # The claim, with nothing behind it
@model
class User:
name: str
age: int = 0
# The checker accepts this call:
expect(TypeError, User, "Guido", 30)
#: [TypeError] User() takes no argumentsThe checker synthesizes
User.__init__(self, name: str, age: int = 0) from
the field declarations, as it does for @dataclass.
It accepts the declaration and never runs model(),
so the call passes the check and fails at runtime. This
model() returns the class unchanged, so
User keeps object’s constructor, which
takes no arguments. The declaration is a claim, and this one is
false.
Libraries like attrs and Pydantic make the claim true by
generating the methods their @dataclass_transform
declares. The declaration then makes every checker that follows
the typing specification synthesize each decorated class’s
constructor from its field declarations and check each call
against that constructor. The shortest model() that
makes its claim true delegates the generation:
# kept_transform.py
from dataclasses import dataclass
from typing import dataclass_transform
@dataclass_transform(frozen_default=True)
def model[T](cls: type[T]) -> type[T]:
return dataclass(frozen=True)(cls)
@model
class User:
name: str
age: int = 0
u = User("Guido", 30)
print(u)
#: User(name='Guido', age=30)
# ty: Property `age` defined in `User` is read-only:
# u.age = 9Now the runtime and the checker agree. The runtime
User is a real frozen data class, and
frozen_default=True tells the checker that classes
built by model() reject assignment statically
too.
@dataclass_transform
generalizes one shape: a decorator that builds a class’s methods
from its field declarations. The checker cannot represent a
class built any other way, because it neither imports nor
executes your code. The checker learns about a class from two
sources alone: the constructs it recognizes and emulates, and
what you declare. That is why commander.py uses a
cast() to state the real signature. Its class
exists only as text in a
string.
__set_name__()A descriptor is any
object whose class defines at least one of
__get__(), __set__(), or
__delete__(). Most descriptors define
__get__() and add the others as needed. When a
class attribute holds a descriptor, that descriptor takes over
access to the attribute. Instead of going to the instance’s
__dict__, a read calls __get__() and a
write calls __set__(). Decorators
depends on this, naming the descriptor and leaving the protocol
to this section. A function is an object like any other, and its
class defines __get__(), so every function is a
descriptor:
# function_is_descriptor.py
from dataclasses import dataclass
@dataclass
class Person:
name: str
def greet(self) -> str:
return f"Hello, {self.name}"
# def created a plain function in the class namespace:
plain = Person.__dict__["greet"]
print(type(plain).__name__, hasattr(plain, "__get__"))
#: function True
# Reading it through an instance triggers __get__(),
# which returns a bound method:
p = Person("Ann")
print(p.greet())
#: Hello, Ann
print(plain.__get__(p, Person)())
#: Hello, Annplain.__get__(p, Person)() is
p.greet() with the binding written out. Method
binding is the descriptor protocol at work.
Telling a class attribute its
name is another job that once needed a metaclass. In
x = Field() below, Field() runs before
the assignment, so Field’s constructor finishes
before Python binds the name x, too early to learn
it. Python delivers that name automatically. When a
class body finishes executing, Python calls
__set_name__(owner, name) on every class attribute
that defines it, descriptor or otherwise, passing the freshly
created class as owner and the attribute’s name as
name. Field pairs
__set_name__() with __get__() and
__set__(), the descriptor protocol, and uses the
delivered name to build its storage key. A print()
at the top of each method traces the descriptor’s whole life,
from naming at class creation through every read and write:
# set_name.py
from typing import Any
class Field:
def __set_name__(self, owner: type, name: str) -> None:
print(f"{name}.__set_name__ on {owner.__name__}")
self.name = name
self.storage = f"_{name}"
def __get__(self, obj: Any,
owner: type | None = None) -> Any:
via = "class" if obj is None else "instance"
print(f"{self.name}.__get__ via {via}")
if obj is None:
return self
return getattr(obj, self.storage)
def __set__(self, obj: Any, value: Any) -> None:
print(f"{self.name}.__set__ = {value}")
setattr(obj, self.storage, value)
class Point:
x = Field()
y = Field()
#: x.__set_name__ on Point
#: y.__set_name__ on Point
p = Point()
p.x = 3
#: x.__set__ = 3
p.y = 4
#: y.__set__ = 4
print(p.x, p.y)
#: x.__get__ via instance
#: y.__get__ via instance
#: 3 4
print(isinstance(Point.x, Field))
#: x.__get__ via class
#: TrueThe first two trace lines appear before any instance exists.
Python calls __set_name__() as it finishes
executing the class Point statement, once for each
Field, passing each one the new class and its own
attribute name. From then on, every read and write routes
through the descriptor instead of going to the instance’s
__dict__. In print(p.x, p.y), Python
evaluates both arguments before calling print(), so
both __get__ lines appear ahead of
3 4. The final access, Point.x, goes
through the class rather than an instance, so
__get__() receives obj=None and
reports via class. That branch returns
self, the descriptor object, which is why
isinstance(Point.x, Field) is
True.
Each Field stores
values under _x or _y in the
instance’s __dict__. The underscore prefix keeps
each storage key apart from the attribute’s name. A descriptor
that defines __set__() is a data
descriptor, and on every lookup a data descriptor takes
precedence over the instance’s __dict__. If
__get__() asks obj for
"x", the attribute’s own name, that lookup routes
back to the descriptor and calls __get__() again,
until Python raises a RecursionError.
Point holds its descriptors under x
and y, so a lookup of _x goes straight
to the instance’s __dict__.
A descriptor that defines
__get__() alone is a non-data descriptor,
and the precedence reverses. The instance’s
__dict__ comes first. That is why assigning to
greet on a Person instance shadows the
method on that one instance, while p.x = 3 cannot
shadow Field, because Field defines
__set__().
The test file confirms that:
__set_name__() gives the descriptor its
name.# test_set_name.py
import set_name
def test_descriptor_learns_its_name() -> None:
field = vars(set_name.Point)["x"]
assert field.name == "x"
assert field.storage == "_x"
def test_values_stored_under_storage_keys() -> None:
p = set_name.Point()
p.x = 3
p.y = 4
assert (p.x, p.y) == (3, 4)
assert p.__dict__ == {"_x": 3, "_y": 4}
def test_descriptor_on_class_returns_itself() -> None:
assert isinstance(set_name.Point.x, set_name.Field)
assert set_name.Point.x is vars(set_name.Point)["x"]Field shows the protocol and stores the value
you assign. Use a descriptor when the write must do more than
store. Positive checks each value before storing
it, so every attribute declared with one raises a
ValueError for a value of zero or less:
# validating_descriptor.py
from exceptions import expected
class Positive:
def __set_name__(self, owner: type, name: str) -> None:
self.storage = f"_{name}"
def __get__(self, obj: object,
owner: type | None = None) -> float:
return getattr(obj, self.storage)
def __set__(self, obj: object, value: float) -> None:
if value <= 0:
raise ValueError(f"{value} is not positive")
setattr(obj, self.storage, value)
class Rectangle:
width = Positive()
height = Positive()
def __init__(
self, width: float, height: float
) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
r = Rectangle(3.0, 4.0)
print(r.area())
#: 12.0
with expected(ValueError):
r.width = -1.0
#: [ValueError] -1.0 is not positive
print(r.width)
#: 3.0Rectangle writes
Positive() twice and no checking code of its own.
self.width = width inside __init__()
routes through Positive.__set__() like any other
write, so the constructor’s two assignments validate its
arguments. The rejected assignment leaves _width as
it was, so r.width still reads the 3.0
the constructor stored.
A property protects
one attribute the same way, but Rectangle then
carries the check twice, once per attribute. With a descriptor,
one class holds the rule, and each attribute that needs it says
Positive().
Positive.__get__()
omits the obj is None branch that
Field has, so it works through an instance and
fails through the class. Reading Rectangle.width
passes None as obj and raises an
AttributeError. A descriptor meant for wider use
returns self there, as Field does.
A metaclass is a subclass of type, and you write
one when the simpler hooks are not enough. You attach it with
the metaclass= keyword in the class header. Python
then uses your metaclass, instead of type, to build
the class.
# simple_meta.py
from typing import Any
from display import display_object
class SimpleMeta(type):
def __init__(cls, name: str, bases: tuple[type, ...],
namespace: dict[str, Any]) -> None:
super().__init__(name, bases, namespace)
setattr(cls, "uses_metaclass", lambda self: "Yes!")
class Simple(metaclass=SimpleMeta):
def ping(self) -> None: pass
@staticmethod
def pong() -> None: pass
display_object(Simple)
#: [Attributes]
#: None
#: [Methods]
#: • ping(self) -> None
#: • pong() -> None
#: • uses_metaclass(self)
print(Simple().uses_metaclass()) # type: ignore
#: Yes!SimpleMeta.__init__() runs
once, as the class Simple statement finishes, and
patches a new method onto the freshly built class. The injected
value is a lambda, but a function is a descriptor, so
Simple().uses_metaclass() binds it like any other
method.
Since a metaclass is a subclass of type, writing
class Simple(SimpleMeta): means something else.
That syntax makes SimpleMeta an ordinary base
class, so Simple inherits type and
becomes a second metaclass, not a class built by
SimpleMeta. metaclass= specifies what
builds a class, independent of its base classes. Python computes
a new class’s metaclass from all of its bases, so
class Child(Simple): also gets
SimpleMeta as its metaclass.
By convention the first argument
of a metaclass method is cls rather than
self, except for __new__(), whose
first argument is the metaclass and usually takes the name
mcls or mcs. Here cls is
the class object under construction, Simple. As
with any subclass, call the base-class version first through
super().
Metaprogramming and static typing
conflict. A type describes a known set of attributes and
signatures, but a metaclass changes that structure at runtime,
adding attributes the class does not declare and replacing
methods like __new__(). The type checker never runs
the metaclass, so it reports the lines that use those changes as
errors.
Three ways suppress the type checker’s reports, from narrowest to broadest:
setattr(cls, "name", value)
adds an attribute through a string the type checker does not
track.# type: ignore silences one line,
as on Simple().uses_metaclass() in simple_meta.py.Any-typed name,
klass: Any = cls, stops attribute checking for
everything reached through that name.Prefer the narrowest escape that fits, because a broad
Any also suppresses reports of genuine
mistakes.
__init__()
versus __new__() in a MetaclassMetaclass examples appear to use
__new__() and __init__()
interchangeably. The difference is timing.
__new__() runs before the class object exists, so
it can change the name, bases, and namespace that Python uses to
build it. __init__() runs after the class exists,
so changing those arguments leaves the class as it was, though
you can still modify the finished class object:
# new_vs_init.py
from typing import Any
class Tag:
pass
class Meta(type):
def __new__(mcls, name: str, bases: tuple[type, ...],
namespace: dict[str, Any]) -> type:
# Before creation: these changes take effect
namespace["added_in_new"] = 42
bases += (Tag,)
return super().__new__(mcls, name, bases, namespace)
def __init__(cls, name: str, bases: tuple[type, ...],
namespace: dict[str, Any]) -> None:
super().__init__(name, bases, namespace)
# No effect: the class is already built
namespace["added_in_init"] = 99
# Effect: this modifies the finished class
setattr(cls, "patched_in_init", 3.14)
class Built(metaclass=Meta):
pass
for attr in ("added_in_new", "added_in_init",
"patched_in_init"):
print(attr, hasattr(Built, attr))
#: added_in_new True
#: added_in_init False
#: patched_in_init True
print("has Tag base:", Tag in Built.__bases__)
#: has Tag base: Trueadded_in_init
reports False because type.__new__()
copies namespace into the new class’s own
__dict__ as it builds the class. By the time
__init__() runs, the two mappings are independent,
so mutating the original dict leaves the class unchanged.
setattr(cls, ...) still works because it modifies
the class object.
Override __new__()
when you must change name, bases, or
the namespace (including special members like
__slots__) before Python builds the class.
Otherwise, prefer __init__(), which receives the
finished class and returns None.
A method defined on the metaclass
becomes a method of the class object, callable on the class but
not on its instances. Such methods are sometimes called
metamethods. They differ from
classmethods. A classmethod stays
callable on both the class and its instances, while a metamethod
works only through the class. The class is an instance of the
metaclass. The class’s own instances are not.
One useful metamethod is
__call__(). It is the same method that makes any
object callable. obj() invokes
type(obj).__call__(obj, ...). A class is an object,
an instance of its metaclass, so ClassName()
invokes __call__() on the metaclass the same way.
That __call__() runs first when you create an
instance of the class. __new__() and
__init__() normally run only because the default
type.__call__() calls them. A metaclass that
overrides __call__() replaces that step, so its
code determines whether __new__() and
__init__() run. That code can also return a cached
instance instead of building a new one. Caching there is one way
to build a Singleton:
# singleton.py
from typing import Any, ClassVar
class Singleton(type):
# A shared dict of class objects : instances
_instances: ClassVar[dict[type, Any]] = {}
def __call__[T](
cls: type[T], *args: Any, **kwargs: Any) -> T:
if cls not in Singleton._instances:
print(f"building {cls.__name__}")
Singleton._instances[cls] = type.__call__(
cls, *args, **kwargs)
else:
print(f"reusing {cls.__name__}")
return Singleton._instances[cls]
class ASingleton(metaclass=Singleton):
pass
class BSingleton(metaclass=Singleton):
pass
a = ASingleton()
#: building ASingleton
b = ASingleton() # [1]
#: reusing ASingleton
assert a is b
c = BSingleton()
#: building BSingleton
d = BSingleton()
#: reusing BSingleton
assert c is d
assert a is not cThe ASingleton() call at [1] ends
in __call__(), which returns the cached instance.
__new__() and __init__() run once per
class, on the first call. Each class gets its own entry in the
_instances dictionary, so the singletons are
independent.
The [T] on __call__() ties its
return type to cls, so ty infers
ASingleton as the type of ASingleton()
instead of Any. Without the [T], every
singleton comes back as Any under ty
and Pyright. A misspelled attribute access on the result then
passes the check. Under mypy ASingleton() has the
type ASingleton with or without the
[T], because mypy ignores a metaclass
__call__() return type.
That same [T] is why
the body calls type.__call__(cls, ...) instead of
the more usual super().__call__(...). Annotating
the first parameter as type[T] drops
Singleton from the type of cls.
ty and mypy accept a zero-argument
super() only when they can confirm that
cls is a Singleton. Pyright accepts it
without the check. At run time both forms call
type.__call__() on cls with the same
arguments.
You might expect to parameterize1 the class, with
class Singleton[T](type) and
_instances: ClassVar[dict[type, T]]. That fails
twice. A ClassVar means one shared value for the
whole class, so it cannot depend on a type parameter that varies
per instantiation. And each class must write
class ASingleton(metaclass=Singleton[ASingleton]):,
naming ASingleton before its class body finishes
defining it.2 The method-level
[T] on __call__() avoids both
problems. It binds T from cls at the
call site, ASingleton(), and that call runs after
ASingleton exists.
The metaclass version works, but it is heavier than the problem usually requires. Singleton covers the lighter alternatives, from a class decorator down to a module.
Singleton stores its cache in
_instances, a dict attribute, rather
than inheriting from dict. Can a metaclass inherit
from more than one class, the way an ordinary class can?
Trying the obvious version fails. type and
dict are both built-in types with their own C-level
instance layout, and CPython allows multiple inheritance only
when the bases’ layouts are compatible, one extending the
other:
# metaclass_layout_conflict.py
from typing import Any
from exceptions import expected
with expected(TypeError):
class Singleton(type, dict[type, Any]): # type: ignore
pass
#: [TypeError] multiple bases have instance lay-out conflictThe failure comes from the instance layouts, not from
metaclasses. class X(list, dict): pass fails the
same way. Neither base is a metaclass. Because
type, list, and dict each
bring a layout of their own, no class can combine two of
them.
The # type: ignore comment appears because
ty checks this rule statically. At check time, its
instance-layout-conflict check reports the very
TypeError this example exists to demonstrate at run
time. The comment suppresses the diagnostic only because
provoking that crash is educational.
Python 3.15 adds the @typing.disjoint_base
marker (PEP 800), which tells the type checker that a class of
your own follows the same rule as type and
dict: no class can inherit from it and from another
unrelated disjoint base. The type checker reports a class that
inherits from two such bases as an
instance-layout-conflict, and it treats an
isinstance() branch that requires both bases as
unreachable, since no object can be an instance of both.
A metaclass can multiply inherit like any other class, as long as the extra class is a mix-in with no competing layout:
# mixin.py
from exceptions import expected
class Mixin:
def helper(self) -> str:
return "hi"
class HelperMeta(type, Mixin):
pass
class Sub(metaclass=HelperMeta):
pass
print(Sub.helper())
#: hi
with expected(AttributeError): # A metamethod: class only
Sub().helper() # type: ignore
#: [AttributeError] 'Sub' object has no attribute 'helper'Lookup on Sub finds
helper() through its metaclass, and lookup on a
Sub instance does not. The
AttributeError comes from the metamethod rule at
the start of Intercepting Instance
Creation. Lookup on a Sub instance searches
Sub and its bases. Mixin sits among
the bases of HelperMeta, outside that search. A
classmethod defined in Sub works
through both Sub and its instances.
A metaclass that needs a dict holds one in an
attribute, as Singleton._instances does, and keeps
the single layout it inherits from type.
Multiple inheritance fails a second way, from the other direction. A class has a single metaclass, so Python cannot choose a metaclass for a class whose bases come from two unrelated metaclasses:
# multiple_metaclass_inheritance.py
from exceptions import expected
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
with expected(TypeError):
class C(A, B): # type: ignore
pass
#: [TypeError] metaclass conflict: the metaclass of a
#: derived class must be a (non-strict) subclass of the
#: metaclasses of all its bases
class MetaC(MetaA, MetaB):
pass
class D(A, B, metaclass=MetaC):
pass
print(type(D).__name__)
#: MetaCThe expected() helper wraps the message across
three lines to fit the page. Python reports it as a single line,
and that line states the fix. D’s metaclass,
MetaC, must be a subclass of every base’s
metaclass, MetaA and MetaB both.
As with the layout conflict, ty catches this one
statically. It reports conflicting-metaclass and
names both MetaA and MetaB, so the
line carries a # type: ignore.
Both failures have the same shape: an inheritance graph that looks legal until you check each base’s layout or metaclass. That shape is one more reason to reserve metaclasses, and arguably multiple inheritance, for cases that need them.
Use a metaclass when you need to change the class object rather than react to its creation:
__call__() in singleton.py, or the
__iter__() that lets EnumType make
for c in Color work).__prepare__() so the class body populates a
custom dictionary.__init_subclass__() requires a common base to
define it. Such a family shares a metaclass.The first case gives the class object an
__iter__():
# iterable_class.py
from collections.abc import Iterator
from typing import Any
class IterableMeta(type):
def __iter__(cls) -> Iterator[Any]:
return (
v for k, v in vars(cls).items()
if not k.startswith("_")
)
class Color(metaclass=IterableMeta):
red = "red"
green = "green"
blue = "blue"
for c in Color:
print(c)
#: red
#: green
#: bluePython calls IterableMeta.__iter__() when you
write for c in Color, which iterates over the class
object, not an instance of it. In Color’s namespace
the underscore-prefixed names that __iter__() skips
are the dunder bookkeeping every class carries. The values come
out in source order because a class’s namespace keeps the order
in which the body defines its names, a guarantee since Python
3.6 (PEP 520). A metaclass, a class decorator, or
__init_subclass__() that walks
vars(cls) sees the members in the order the source
lists them.
A class decorator cannot make for c in Color
work. Python looks up a special method such as
__iter__() on the object’s type and skips the
object’s own namespace, so for c in Color consults
type(Color). An __iter__ that a
decorator assigns onto Color serves instances of
Color. For the class object, Python finds a special
method on its metaclass.
__prepare__() has no simpler substitute
either:
# prepare_namespace.py
from typing import Any
from exceptions import expected
class NoDuplicates(dict[str, Any]):
def __setitem__(self, key: str, value: Any) -> None:
if key in self:
raise TypeError(f"{key} defined twice")
super().__setitem__(key, value)
class Strict(type):
@classmethod
def __prepare__(cls, name: str, bases: tuple[type, ...],
**kwargs: Any) -> NoDuplicates:
return NoDuplicates()
with expected(TypeError):
class Handlers(metaclass=Strict):
def on_open(self) -> None: ... # [1]
def on_close(self) -> None: ...
def on_open(self) -> None: ... # noqa: F811 # [2]
#: [TypeError] on_open defined twice__prepare__() runs
before the class body does, and the mapping it returns becomes
the namespace for that body. Every def and every
assignment in the body becomes a __setitem__() call
on that mapping, so NoDuplicates.__setitem__()
receives the on_open at [2] under a
key the mapping already holds. Python then passes the finished
mapping to type.__new__().
__prepare__() must
carry @classmethod. Python calls it on the
metaclass before any class object exists, so an ordinary method
receives the class name as its self and leaves
bases unfilled. The resulting
TypeError reports a missing bases
argument and never mentions @classmethod.
No other hook can detect the
on_open at [2].
__init_subclass__(), __set_name__(),
and a class decorator all run after the body has finished, by
which time [2] has overwritten [1].
The static half of the check is ruff’s report of the same
mistake. The # noqa: F811 suppresses that report so
the listing passes ruff. __prepare__()
detects the mistake at run time, including on names the body
computes.
These needs are real but rare.
For everything else, __init_subclass__(),
__set_name__(), and class
decorators are simpler and easier to read.
A class decorator receives the finished class, so it can add,
replace, or inspect members. It runs after
type.__new__() has used the name, the bases, and
the namespace, so changing those means building a replacement
class.
A class decorator cannot give the class object behavior of
its own. Setting __call__ from a decorator makes
instances callable. Only a metaclass makes the class callable in
a new way. The case for a metaclass rests on that limit. The
class object needs behavior that only its metaclass, chosen
before the class exists, can supply.
Subscripting a class is the
exception. For a class Box, Box[int]
uses the metaclass’s __getitem__() if the metaclass
defines one, and otherwise calls
Box.__class_getitem__(int), a method PEP 560 added
in Python 3.7. __class_getitem__() is implicitly a
class method, as __init_subclass__() is, so a class
that defines it becomes subscriptable without a metaclass.
list, dict, and the other built-in
containers define it to return a
types.GenericAlias, which is how
list[int] works at runtime.
typing.Generic defines it for the classes you write
with type parameters.
When a class header lists a GenericAlias as a
base, as dict[str, EventMaker] does for
EventMakers in greenhouse.py, Python
records the plain dict in __bases__.
types.get_original_bases(EventMakers), new in
Python 3.12, returns the bases as the header wrote them, with
dict[str, EventMaker] intact.
inspect ModuleUp to now, you’ve modified
classes. type() builds them, and metaclasses and
__init_subclass__() run code during their creation.
The inspect module is the other half of
metaprogramming. It reads the structure of live objects. It
answers questions like which members an object has, what a
function’s signature is, and what its docstring says.
inspect works on any
live object: modules, classes, functions, methods, and
instances.
inspect.signature(callable)
returns a Signature object describing the
parameters, their annotations, and their defaults.inspect.getdoc(obj)
returns the docstring with its common indentation removed.inspect.getmembers(obj) and
inspect.getmembers_static(obj) return an object’s
(name, value) pairs. The _static
variant reads them without running properties or other
descriptors.inspect.isclass(),
inspect.isfunction(), and
inspect.ismethod() classify what you find.# inspect_tour.py
import inspect
def greet(name: str, loud: bool = False) -> str:
"Return a greeting."
text = f"Hello, {name}"
return text.upper() if loud else text
print(inspect.signature(greet))
#: (name: str, loud: bool = False) -> str
print(inspect.getdoc(greet))
#: Return a greeting.
print(inspect.isfunction(greet), inspect.isclass(greet))
#: True False
print(list(inspect.signature(greet).parameters))
#: ['name', 'loud']signature() recovers
the full call interface, annotations and defaults included, as a
structured object rather than a string. Python keeps type
annotations (a.k.a. type hints) at runtime, although it does
not check them. Under the deferred evaluation of PEP 649,
the annotations stay attached to the function, and Python
evaluates them when code first reads them.
signature() reads that stored data (not the
original source text) to build the Signature
object.
display_object()
relies on inspect functions.
getmembers_static() finds the members,
signature() renders each method, and
get_annotations() supplies the declared types,
returning a class’s declared annotations in a dict.
display_object()’s source and its display options
are reference material, collected in display_object()
Reference at the end of this chapter.
The typing module adds two functions for a Protocol
class, both new in Python 3.13.
typing.is_protocol(cls) reports whether
cls is a Protocol.
typing.get_protocol_members(cls) returns the
frozenset of attribute and method names that a
structural match requires, and raises a TypeError
for a class that is not a Protocol.
display_object()
walks every member that inspect.getmembers_static()
returns. The static variant reads members from the
__dict__ of the object and of each of its classes,
without invoking descriptors, properties, or
__getattr__(). Inspecting an object therefore reads
what those dictionaries hold, which makes the tool safe to point
at something unfamiliar.
The tool sorts each member into
one of two lists. Callables become methods, printed with the
signature that inspect.signature() reports, or
(...) when a built-in has no inspectable signature.
Everything else becomes an attribute, printed as
name: type = value. The declared type comes from
the class annotations, which _annotations() gathers
across the whole inheritance chain, calling
inspect.get_annotations() on each class in FORWARDREF
format. In that format, a name the module never defined
prints as a ForwardRef instead of raising a
NameError. An attribute with no annotation, such as
one assigned dynamically, prints as name = value.
The value is the member’s repr(), truncated to keep
the line within max_width.
An attribute tagged [CV] lives on the class or a
base class rather than in obj’s own
__dict__. The tag is short for “class variable,”
another name for a class attribute. When obj is a
class, every attribute lives on a class, so all of them carry
the tag. In Comparing
Ordinary Classes and Data Classes, show(D) in
classvar_dataclass.py tags
both D.x and D.s. D
declares them in its body, but show(D) inspects the
class, where both live.
For an instance, the tag says whether the value lives on the
class or on the object, the same rule Stars.rating
demonstrates in Class
Attributes. In that same chapter 12 comparison,
show(B()) in class_with_defaults.py
tags B.x and B.s.
display_object(Messenger("iris", 12, 3.14)) in
chapter 12 tags none, since @dataclass assigns
every field straight onto the new instance. The tag comes from
where the value lives, so it applies whether or not the
attribute’s declaration uses typing.ClassVar.
Deferred evaluation lets an
annotation specify a class that the module defines later. Python
evaluates an annotation when code first reads it, so a class can
declare a field whose type appears further down the module. A
metaprogram reads annotations early. A decorator, an
__init_subclass__() hook, or a metaclass runs while
Python creates the class. If one of them reads the annotations
then, a name in those annotations may refer to a class the
module defines later. Evaluating them at that moment raises a
NameError.
The standard library’s
annotationlib module handles this case. Its
get_annotations() takes a format
argument that chooses how to evaluate the annotations, and
inspect.get_annotations() accepts the same
argument.
Format.VALUE, the default, evaluates each
annotation to a real object, and raises a NameError
when a name is missing.Format.FORWARDREF
evaluates what it can, and substitutes an
annotationlib.ForwardRef for each name it cannot
resolve.Format.STRING evaluates nothing, and returns
each annotation as a string that matches the source as closely
as it can.The listing reads one forward-referencing annotation in all three formats:
# forward_formats.py
from annotationlib import Format, get_annotations
from exceptions import expect
class Order:
item: str
buyer: Customer
expect(NameError, get_annotations, Order,
format=Format.VALUE)
#: [NameError] name 'Customer' is not defined
fwd = get_annotations(Order, format=Format.FORWARDREF)
print(fwd["item"])
#: <class 'str'>
ref = fwd["buyer"]
print(type(ref).__name__, ref.__forward_arg__)
#: ForwardRef Customer
print(get_annotations(Order, format=Format.STRING))
#: {'item': 'str', 'buyer': 'Customer'}
class Customer:
pass
print(get_annotations(Order)["buyer"] is Customer)
#: TrueOrder specifies Customer before
Customer exists. The VALUE request
raises the same NameError that a decorator applied
to Order would get. STRING returns
text instead of objects, so it succeeds before
Customer exists. Once Customer exists,
the default VALUE request succeeds, because the
class caches its annotations only after an evaluation
succeeds.
A tool that reads annotations at definition time therefore
requests FORWARDREF. It keeps the real objects it
gets back, and holds on to each ForwardRef until
the name exists, when the reference’s evaluate()
method resolves it to the class.
Every hook in this chapter is an ordinary function that Python calls at a known moment during class construction.
# hook_order.py
from typing import Any
class Watched:
def __set_name__(self, owner: type, name: str) -> None:
print(f"__set_name__({owner.__name__}, {name})")
class Meta(type):
@classmethod
def __prepare__(cls, name: str, bases: tuple[type, ...],
**kwargs: Any) -> dict[str, Any]:
print(f"__prepare__ {name}")
return {}
def __new__(mcls, name: str, bases: tuple[type, ...],
namespace: dict[str, Any]) -> type:
print(f"__new__ {name} enter")
cls = super().__new__(mcls, name, bases, namespace)
print(f"__new__ {name} exit")
return cls
def __init__(cls, name: str, bases: tuple[type, ...],
namespace: dict[str, Any]) -> None:
super().__init__(name, bases, namespace)
print(f"__init__ {name}")
def tag[T](cls: type[T]) -> type[T]:
print(f"decorator {cls.__name__}")
return cls
class Base(metaclass=Meta):
def __init_subclass__(cls, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
print(f"__init_subclass__ {cls.__name__}")
#: __prepare__ Base
#: __new__ Base enter
#: __new__ Base exit
#: __init__ Base
@tag
class Derived(Base):
field = Watched()
print("class body")
#: __prepare__ Derived
#: class body
#: __new__ Derived enter
#: __set_name__(Derived, field)
#: __init_subclass__ Derived
#: __new__ Derived exit
#: __init__ Derived
#: decorator DerivedBase’s four lines
are the bare sequence. They also show that
Base.__init_subclass__() does not run for
Base, the rule Making a Class Final needs.
__prepare__() runs before the body, so its line
comes first. type.__new__() calls
__set_name__() and __init_subclass__()
as it assembles the class, so their lines sit between
__new__ Derived enter and
__new__ Derived exit, inside the metaclass’s own
construction step. The decorator is last, because it receives a
finished class.
__init_subclass__().__set_name__().type() with three arguments, or an
exec()ed class body when the definition is easier
to read as source.__new__().__prepare__().__call__().inspect.None of these hooks is a separate facility added to the
language. A class is an object that Python builds at run time by
executing its body, and hook_order.py displays
each step of that construction as it runs. The one hook missing
from its trace is __call__(), which runs later
still, each time someone calls the finished class.
display_object()
ReferenceThe rest of
display_object() is presentation: how it formats
what inspect reports, and which members it shows.
Read it when a listing’s output raises a question, and skip it
otherwise.
display_object()Throughout the book you’ve seen display_object()
show the layout of an object. Here, the utils/
prefix on the file marker puts the file in the shared
utils/ directory at the top of the
Examples tree, where any chapter can import it:
# utils/display.py
import inspect
from annotationlib import Format
from collections.abc import Callable, Sequence
from typing import Final
ALL_DUNDERS = sentinel("ALL_DUNDERS")
REDEFINED_DUNDERS = sentinel("REDEFINED_DUNDERS")
INTERESTING_DUNDERS: Final[tuple[str, ...]] = (
"__init__", "__repr__", "__eq__", "__hash__",
)
def _annotations(cls: type) -> dict[str, object]:
# Annotations declared on the class or any of its bases:
return {**inspect.get_annotations(
base, format=Format.FORWARDREF)
for base in reversed(cls.__mro__)}
def _type_name(annotation: object) -> str:
# A readable annotation name, keeping [parameters]:
if isinstance(annotation, type):
return annotation.__name__
return str(annotation)
def _redefined(name: str, value: object) -> bool:
# Restricted to INTERESTING_DUNDERS: every class has
# __module__, __dict__, and other bookkeeping dunders
# that always differ from object's, so comparing those
# never filters anything out.
if name not in INTERESTING_DUNDERS:
return False
return getattr(object, name, None) is not value
def _show_dunder(
dunder: Sequence[str] | ALL_DUNDERS | REDEFINED_DUNDERS,
name: str,
value: object,
) -> bool:
if dunder is ALL_DUNDERS:
return True
if dunder is REDEFINED_DUNDERS:
return _redefined(name, value)
return name in dunderdisplay_object()
hides standard dunder members by default. Pass their names in
dunder to keep specific ones.
dunder=["__init__"] shows __init__ and
hides the other dunders. Pass the ALL_DUNDERS
sentinel instead to keep every dunder member, including the
interpreter’s own machinery. ALL_DUNDERS is useful
for exploring an unfamiliar object, but it lists a class’s own
choices among everything object and the interpreter
add, where they are hard to find.
INTERESTING_DUNDERS lists the four a reader
typically customizes when defining a class:
__init__, __repr__,
__eq__, and __hash__. Pass it as
dunder to see those four without the surrounding
noise.
Under INTERESTING_DUNDERS, every class shows all
four, because a class inherits object’s version of
any it leaves out, and the report lists an inherited dunder the
same way as one the class wrote. REDEFINED_DUNDERS
filters harder. Among those same four, it keeps only the ones
whose value differs from object’s version.
The comparison uses
is, not ==, since a dunder inherited
unchanged from object is the same function object,
not merely an equal one.
dunder’s type is
Sequence[str] | ALL_DUNDERS | REDEFINED_DUNDERS,
naming each sentinel value rather than the generic
sentinel class, so the type checker narrows
dunder to Sequence[str] once it rules
out both sentinels. The type checker then accepts
name in dunder as written.
display_object() reports differently under
INTERESTING_DUNDERS and
REDEFINED_DUNDERS, on an empty class and on one
that redefines almost everything:
# dunder_modes.py
from dataclasses import dataclass
from display import (INTERESTING_DUNDERS, REDEFINED_DUNDERS,
display_object)
class Plain:
pass
@dataclass
class Point:
x: int
y: int
display_object(Plain, INTERESTING_DUNDERS)
#: [Attributes]
#: None
#: [Methods]
#: • __eq__(self, value, /)
#: • __hash__(self, /)
#: • __init__(self, /, *args, **kwargs)
#: • __repr__(self, /)
display_object(Plain, REDEFINED_DUNDERS)
#: [Attributes]
#: None
#: [Methods]
#: None
display_object(Point, REDEFINED_DUNDERS)
#: [Attributes]
#: • __hash__ = None [CV]
#: [Methods]
#: • __eq__(self, other)
#: • __init__(self, x: int, y: int) -> None
#: • __repr__(self)
display_object(Point, REDEFINED_DUNDERS,
exclude=("__hash__",))
#: [Attributes]
#: None
#: [Methods]
#: • __eq__(self, other)
#: • __init__(self, x: int, y: int) -> None
#: • __repr__(self)Plain inherits all
four from object, so
INTERESTING_DUNDERS shows object’s
versions and REDEFINED_DUNDERS shows
None under both headings. @dataclass
writes three of them and sets __hash__ to
None, so Point reports a
__hash__ attribute rather than a method. The last
call drops that row, for the same reason comparison.py in Data
Classes as Types passes
exclude=("__hash__",).
exclude drops specific names regardless of what
dunder otherwise shows, and it applies to every
member, dunder or ordinary. The exclude check runs
before the dunder test, so an excluded name stays
out of [Attributes] and [Methods] no
matter which mode selects it. Excluding __hash__
helps when an earlier listing has explained its row and
repeating the row would add noise.
The rest of the file selects the members, formats each one, and prints the report:
# utils/display.py (continued)
def _shared(obj: object, name: str) -> bool:
# A class has no instance-level storage to compare
# against, so every attribute it shows is class-level
# storage by construction. For an instance, only a name
# missing from its own __dict__ is:
if inspect.isclass(obj):
return True
return name not in getattr(obj, "__dict__", {})
def _truncate(text: str, budget: int) -> str:
# Fit the budget, marking a cut with an ellipsis:
if len(text) <= budget:
return text
if budget < 4: # No room for text plus the ellipsis
return "..."[:max(budget, 0)]
return text[:budget - 3] + "..."
def _format_method(
name: str, value: Callable[..., object], max_width: int
) -> str:
try:
sig = str(inspect.signature(value))
except (ValueError, TypeError):
sig = "(...)"
sig = _truncate(sig, max_width - len(name) - 4)
return f" • {name}{sig}"
def _format_attribute(
obj: object,
name: str,
value: object,
annotations: dict[str, object],
max_width: int,
) -> str:
label = name
if name in annotations:
label = f"{name}: {_type_name(annotations[name])}"
tag = " [CV]" if _shared(obj, name) else ""
budget = max_width - len(label) - len(tag) - 7
val_str = _truncate(repr(value), budget)
return f" • {label} = {val_str}{tag}"
def display_object(
obj: object,
dunder: (Sequence[str] | ALL_DUNDERS
| REDEFINED_DUNDERS) = (),
max_width: int = 57,
exclude: Sequence[str] = (),
) -> None:
# For a class, the class; for an instance, its class:
cls = obj if inspect.isclass(obj) else type(obj)
annotations = _annotations(cls)
attributes: list[str] = []
methods: list[str] = []
# Read members statically, without triggering
# dynamic descriptors:
for name, value in inspect.getmembers_static(obj):
if name in exclude:
continue
is_dunder = (name.startswith("__")
and name.endswith("__"))
if is_dunder and not _show_dunder(
dunder, name, value):
continue # Skip standard dunder clutter
if callable(value):
methods.append(
_format_method(name, value, max_width))
else:
attributes.append(_format_attribute(
obj, name, value, annotations, max_width
))
print("[Attributes]")
print("\n".join(attributes) or " None")
print("[Methods]")
print("\n".join(methods) or " None")Importing into any example works
because the example tooling puts utils/ on the
import path, not because Python searches other directories
automatically. tools/run_examples.py sets
PYTHONPATH to the tree’s utils/
directory before running each script. The same directory reaches
pytest through pythonpath in
pyproject.toml. Without either,
from display import display_object fails with
ModuleNotFoundError.
# demo_display_object.py
from dataclasses import dataclass
from display import ALL_DUNDERS, display_object
@dataclass
class Fraggle:
"""A small dataclass for the demo."""
x: int
y: float = 1.14659
z: str = "blivet"
def f(self) -> None: ...
def g(self, x: int) -> float:
return 0.001
def h(self, s: str) -> str:
return f"h({s})"
display_object(Fraggle) # [1]
#: [Attributes]
#: • y: float = 1.14659 [CV]
#: • z: str = 'blivet' [CV]
#: [Methods]
#: • f(self) -> None
#: • g(self, x: int) -> float
#: • h(self, s: str) -> str
display_object(Fraggle(9, 2.3)) # [2]
#: [Attributes]
#: • x: int = 9
#: • y: float = 2.3
#: • z: str = 'blivet'
#: [Methods]
#: • f(self) -> None
#: • g(self, x: int) -> float
#: • h(self, s: str) -> strThe calls at [1] and [2] display
the same class two ways. display_object(Fraggle)
inspects the class object. It lists y and
z, the fields with defaults. x’s
declaration is x: int with no default, so on the
class it is an annotation with no bound attribute, and
getmembers_static() skips it.
display_object(Fraggle(9, 2.3)) inspects an
instance, whose attributes hold its field values, so
x now appears beside y and
z. The method list is the same either way, because
methods live on the class.
# demo_display_object.py (continued)
# ALL_DUNDERS also reveals what @dataclass generated:
display_object(Fraggle(9, 2.3), dunder=ALL_DUNDERS) # [1]
#: [Attributes]
#: • __annotations_cache__ = {'x': <class 'int'>, ... [CV]
#: • __class__ = <attribute '__class__'> [CV]
#: • __dataclass_fields__ = {'x': Field(name='x',t... [CV]
#: • __dataclass_params__ = _DataclassParams(init=... [CV]
#: • __dict__ = <attribute '__dict__'> [CV]
#: • __doc__ = 'A small dataclass for the demo.' [CV]
#: • __firstlineno__ = 5 [CV]
#: • __hash__ = None [CV]
#: • __match_args__ = ('x', 'y', 'z') [CV]
#: • __module__ = '__main__' [CV]
#: • __static_attributes__ = () [CV]
#: • __weakref__ = <attribute '__weakref__'> [CV]
#: • x: int = 9
#: • y: float = 2.3
#: • z: str = 'blivet'
#: [Methods]
#: • __annotate_func__(format, /)
#: • __delattr__(self, name, /)
#: • __dir__(self, /)
#: • __eq__(self, other)
#: • __format__(self, format_spec, /)
#: • __ge__(self, value, /)
#: • __getattribute__(self, name, /)
#: • __getstate__(self, /)
#: • __gt__(self, value, /)
#: • __init__(self, x: int, y: float = 1.14659, z: str ...
#: • __init_subclass__(type, /)
#: • __le__(self, value, /)
#: • __lt__(self, value, /)
#: • __ne__(self, value, /)
#: • __new__(*args, **kwargs)
#: • __reduce__(self, /)
#: • __reduce_ex__(self, protocol, /)
#: • __replace__(self, /, **changes)
#: • __repr__(self)
#: • __setattr__(self, name, value, /)
#: • __sizeof__(self, /)
#: • __str__(self, /)
#: • __subclasshook__(type, object, /)
#: • f(self) -> None
#: • g(self, x: int) -> float
#: • h(self, s: str) -> strThe call at [1] passes ALL_DUNDERS.
A @dataclass produces many of these dunders:
__dataclass_fields____dataclass_params____match_args____replace____hash__, set to
None__init__, __eq__, and
__repr__The generated
__init__(), __eq__(), and
__repr__() give Fraggle a constructor,
equality, and a repr() that you did not write.
Beyond those three, the output holds Fraggle’s
fields and methods, two annotation entries, and the bookkeeping
every class carries.
The output also shows the
deferred evaluation of PEP 649 at work on a class.
__annotate_func__ is the code that computes the
annotations, and __annotations_cache__ holds the
result after the first request. __annotate__, the
name PEP 649 gives the function, is a descriptor on
type that reads __annotate_func__, so
it lives on the metaclass rather than in the class’s own
namespace.
Try each exercise before opening its solution.
init_subclass.py, add a
class Yellow(Color) and then
Gold(Yellow). Predict Color.registry
after each new class, then confirm.set_name.py, add a third
Field() attribute, z, to
Point, set p.z = 9, and confirm
p.__dict__ now also holds _z.singleton.py, add a third
class CSingleton(metaclass=Singleton) and confirm
c1 = CSingleton(); c2 = CSingleton(); c1 is c2 is
True, while c1 is a (comparing across
the different singleton classes) is False.final_runtime.py so a
class declares itself final with a keyword in its header,
class B(A, final=True):, using the
**kwargs that __init_subclass__()
receives. Confirm that a non-final sibling of B
still subclasses freely.inspect_tour.py as a
model, write a function describe(func) that prints
a function’s name, its inspect.signature(), and its
docstring (or "(no docstring)" if
inspect.getdoc() returns None), then
call it on greet and on a lambda.# type: ignore comment from metaclass_layout_conflict.py
and run ty over the file. Compare the
instance-layout-conflict diagnostic it reports with
the TypeError the program prints. The static report
and the runtime failure describe the same collision.type() directly, build a class
Celsius with a base of float, an
attribute unit = "C", and a method
describe(self) returning
f"{self} degrees {self.unit}". Confirm
Celsius(21.5).describe() works and that
type(Celsius) is type.new_vs_init.py, move the
bases += (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.commander.py validates
class_name against KNOWN_COMMANDS
before splicing it into source text. Remove that check, call
Command.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.prepare_namespace.py’s
NoDuplicates so that instead of raising an
exception, it keeps the first definition of a duplicated name
and discards the later one. Give the two on_open
bodies different print() calls so you can tell them
apart, then confirm that Handlers().on_open() runs
the first one. Explain why no class decorator can achieve the
same thing.The book uses pytest’s own spelling,
parametrize, for
@pytest.mark.parametrize, and “parameterize”
everywhere else. Testing explains the four
possible spellings.↩︎
C++ templates can do this via the Curiously Recurring Template Pattern (CRTP):
template <typename T>
class Singleton {
public:
static T& instance() {
static T inst;
return inst;
}
};
class ASingleton : public Singleton<ASingleton> {};The C++ compiler instantiates templates on demand, rather
than executing them the way Python executes a class
statement. A C++ class name is a valid incomplete type
the moment the compiler sees class ASingleton,
before it reads a single member.
Singleton<ASingleton> can use that name as
its template argument while the class remains incomplete. The
compiler compiles its member functions when something first
calls them, and by then ASingleton is complete.
Python evaluates Singleton[ASingleton] eagerly,
before it binds the name ASingleton, and offers no
equivalent incomplete-type stage to exploit.↩︎