Exercise Answers: Class-Creation Evidence Studio Review¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Metaclass Design Class Creation"]
page["Exercise Answers: Class-Creation Evidence Studio Review"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
claim["claim"]
source["source path"]
test["focused test"]
output["observable output"]
limit["non-claim"]
claim --> source --> test --> output --> limit
Use these answers to review reasoning and evidence, not to compare spelling. A different implementation is defensible when it preserves the requested contract, names its owner, and proves the same observable behavior.
Answer 1: Extend the explicit construction recipe¶
Defensible implementation¶
Add the optional input to the recipe:
@dataclass(frozen=True, slots=True)
class ClassRecipe:
name: str
bases: tuple[type[object], ...]
namespace: dict[str, object]
qualname: str | None = None
Validate and apply it in the factory:
def build_class(recipe: ClassRecipe) -> type[object]:
if not recipe.name.isidentifier():
raise ValueError(f"class name must be an identifier: {recipe.name!r}")
if recipe.qualname is not None and not recipe.qualname.strip():
raise ValueError("explicit qualname cannot be blank")
namespace = dict(recipe.namespace)
namespace.setdefault("__module__", __name__)
if recipe.qualname is not None:
namespace.setdefault("__qualname__", recipe.qualname)
return type(recipe.name, recipe.bases, namespace)
Representative assertions:
namespace = {"channel": "console"}
created = build_class(
ClassRecipe(
"Delivery",
(),
namespace,
qualname="generated.Delivery",
)
)
assert created.__qualname__ == "generated.Delivery"
assert namespace == {"channel": "console"}
assert build_class(ClassRecipe("Plain", (), {})).__qualname__ == "Plain"
Reasoning¶
The pressure still belongs to the explicit factory. One call receives one recipe and creates one class. No future subclass must inherit a policy, and no assignment-time event must be preserved.
Common wrong turns¶
- Mutating
recipe.namespaceleaks factory bookkeeping into caller-owned data. - Installing a metaclass adds hierarchy-wide authority without a hierarchy-wide rule.
- Silently replacing a blank qualname hides invalid provenance input.
What this proves¶
The factory controls explicit introspection metadata while preserving the original construction contract.
What this does not prove¶
It does not recreate compiler provenance, source locations, or nested lexical scope.
Capstone connection¶
The capstone uses ordinary class statements, so Python supplies this identity metadata. Its metaclass should not invent provenance it did not receive.
Answer 2: Explain and test one metaclass collision¶
Defensible implementation¶
Make both policies cooperative:
events: list[str] = []
class AuditMeta(type):
def __new__(mcs, name, bases, namespace):
events.append(f"audit:{name}")
return super().__new__(mcs, name, bases, namespace)
class PolicyMeta(type):
def __new__(mcs, name, bases, namespace):
events.append(f"policy:{name}")
return super().__new__(mcs, name, bases, namespace)
After defining the two base classes, clear setup events and define the joint owner:
events.clear()
class JointMeta(AuditMeta, PolicyMeta):
pass
class Combined(AuditedBase, PolicyBase, metaclass=JointMeta):
pass
assert events == ["audit:Combined", "policy:Combined"]
before_instance = list(events)
Combined()
assert events == before_instance
The order follows JointMeta.__mro__. Both methods must delegate for the chain to reach
type.__new__.
Reasoning¶
The joint metaclass satisfies Python’s subtype requirement and preserves both toy hooks. That is mechanical composition evidence.
A separate semantic test is still needed. For example, if both policies inject an
attribute named policy, a test must specify which value is allowed or reject the
combination.
Common wrong turns¶
- Calling
type.__new__directly inAuditMetabypassesPolicyMeta. - Asserting only
type(Combined) is JointMetaproves selection, not policy composition. - Treating the absence of
TypeErroras a complete integration test hides collisions.
What this proves¶
Cooperative metaclass methods execute according to the combined metaclass MRO.
What this does not prove¶
It does not prove their mutations, registries, or failure semantics are compatible.
Capstone connection¶
PluginMeta rejects automatic conflict repair. A real joint owner would need tests at
least as explicit as this trace plus domain-specific compatibility checks.
Answer 3: Move work to its honest hook¶
Defensible placement¶
Validate the class-body declaration in __new__:
def __new__(mcs, name, bases, namespace):
events.append(f"new:{name}")
if "policy_version" not in namespace:
raise TypeError(f"{name} must declare policy_version")
return super().__new__(mcs, name, bases, namespace)
Record the final MRO in metaclass __init__:
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
events.append(f"init:{name}")
registry.append(
{
"name": cls.__name__,
"mro": [owner.__name__ for owner in cls.__mro__],
}
)
The critical failure assertion is:
before = list(registry)
with pytest.raises(TypeError):
class MissingVersion(metaclass=LifecycleMeta):
pass
assert registry == before
assert events[-1] == "new:MissingVersion"
Reasoning¶
The raw namespace distinguishes “declared here” from “inherited later,” so validation
belongs in __new__. The final MRO exists on the returned class, so its record belongs in
__init__.
Moving validation to __init__ would allow the class object to exist before rejection.
Moving final-MRO bookkeeping before super().__new__ would be premature because no class
or MRO exists yet.
Common wrong turns¶
- Using
hasattrchecks inherited values and weakens the “declared here” rule. - Appending registry state before validation risks entries for rejected classes.
- Adding instance
__init__events confuses two construction lifecycles.
What this proves¶
The hook split follows required evidence and failure timing.
What this does not prove¶
It does not make global registry mutation safe under reload or concurrent imports.
Capstone connection¶
The shipped PluginMeta uses the same split: structural contract generation in
__new__, finished-class registration in metaclass __init__.
Answer 4: Narrow the declaration language¶
Defensible implementation¶
Use an explicit declaration marker:
@dataclass(frozen=True, slots=True)
class Declaration:
value: object
def is_tracked(value: object) -> bool:
return isinstance(value, Declaration)
Then narrow assignment policy:
class DeclarationNamespace(dict[str, object]):
def __init__(self) -> None:
super().__init__()
self.public_names: list[str] = []
def __setitem__(self, key: str, value: object) -> None:
previous = self.get(key)
if key in self and (is_tracked(previous) or is_tracked(value)):
raise TypeError(f"duplicate tracked declaration: {key}")
if is_tracked(value):
self.public_names.append(key)
super().__setitem__(key, value)
The four required tests are important because a one-sided check is easy to bypass:
tracked -> tracked # reject
tracked -> ordinary # reject
ordinary -> tracked # reject
ordinary -> ordinary # allow
Reasoning¶
The language protects only names that contribute to its declared contract. Ordinary helper replacement remains ordinary Python behavior.
Common wrong turns¶
- Checking only the new value lets an ordinary value overwrite a tracked declaration.
- Checking only the old value lets a tracked declaration overwrite an ordinary helper.
- Inspecting
vars(cls)after creation cannot recover the overwritten value.
What this proves¶
The custom mapping preserves and enforces one assignment-time fact.
What this does not prove¶
It does not validate the declaration’s inner value or guarantee arbitrary tooling compatibility.
Capstone connection¶
DefinitionNamespace uses Field and action markers instead of the exercise’s
Declaration, with the same old-or-new tracked rule.
Answer 5: Prove registration does not require a metaclass¶
Defensible implementation¶
Reuse PluginRegistry from a normal base:
def make_subclass_plugin_family(registry):
class PluginBase:
__abstract__ = True
group = "default"
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.__abstract__ = bool(vars(cls).get("__abstract__", False))
inherited_group = next(
(
str(base.group)
for base in cls.__bases__
if getattr(base, "group", None) is not None
),
"default",
)
cls.group = str(vars(cls).get("group", inherited_group))
cls.plugin_name = str(
vars(cls).get("plugin_name", _slug(cls.__name__))
)
if not cls.__abstract__:
registry.register(cls.group, cls.plugin_name, cls)
return PluginBase
The comparison should conclude:
| Requirement | __init_subclass__ |
metaclass |
|---|---|---|
| automatic concrete registration | yes | yes |
| duplicate assignment observation | no | yes, with __prepare__ |
| raw namespace transformation | no direct pre-creation window | yes |
| metaclass conflict cost | no new metaclass | yes |
Reasoning¶
The base hook preserves the registration invariant with less class-creation authority. The registry remains unchanged because state ownership is independent from hook choice.
Common wrong turns¶
- Inheriting
__abstract__ = Truewithout checkingvars(cls)accidentally keeps every descendant abstract. - Storing registry data on the metaclass mixes policy and state again.
- Claiming the metaclass is preferable because its implementation already exists ignores the owner question.
What this proves¶
Registration-only requirements stop at __init_subclass__.
What this does not prove¶
It does not preserve duplicate assignments or provide pre-creation namespace shaping.
Capstone connection¶
The capstone crosses the metaclass boundary because it also needs
DefinitionNamespace, field/action collection, and generated class structure.
Answer 6: Produce a capstone class-creation audit¶
Defensible evidence map¶
| Hook trace | Source owner | Focused proof |
|---|---|---|
prepare:DefinitionNamespace |
PluginMeta.__prepare__ and DefinitionNamespace |
duplicate tracked declaration and ordinary replacement tests |
body:complete |
Python class-body execution into the returned mapping | recorded namespace_names |
new:shape |
PluginMeta.__new__ |
inherited/declared collection and generated-signature assertions |
init:register |
PluginMeta.__init__ and _register_plugin |
deterministic registry and duplicate-key tests |
The lower-power decision is:
registration only
-> __init_subclass__ is enough
tracked assignment events plus generated family contract
-> PluginMeta is justified
The package-discovery decision is:
owner: explicit discovery service
reason: installed and enabled packages are deployment/I/O facts, while PluginMeta sees
only classes already imported into the current process
Required JSON review¶
The report should show:
constructed: falseexecuted: false- all four hook events
- accepted class-creation powers
- rejected discovery, external I/O, conflict repair, and reload reconciliation
Validate the captured file:
Common wrong turns¶
- Constructing a plugin to inspect class creation mixes the lifecycle under review.
- Claiming import-time tests prove reload safety overstates the evidence.
- Recommending entry-point scanning from
PluginMeta.__init__hides I/O in import. - Treating the generated signature as validation ignores descriptor ownership.
What this proves¶
The shipped class family has inspectable namespace, shaping, and registration ownership without instance execution.
What this does not prove¶
Focused tests do not prove arbitrary metaclass compatibility, reload reconciliation, cross-process discovery, or safe external I/O.
Remaining risk¶
The registry is process-global import-time state. Tests can reset it, but real reload and dynamic-import policy remains outside the framework contract.
Studio completion review¶
Strong submissions preserve this chain:
explicit construction
-> inherited metaclass selection
-> observable hook timing
-> declaration-time evidence
-> lower-power registration alternative
-> justified capstone metaclass
If an answer starts with “use a metaclass” before naming a vanished fact or automatic family invariant, revisit the owner decision.
Return to the Module 09 overview or continue to Module 10.