Manual Class Creation with type(...)¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Metaclass Design Class Creation"]
page["Manual Class Creation with type"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
name["name"]
bases["bases"]
namespace["namespace"]
constructor["type(name, bases, namespace)"]
class_object["new class object"]
instance["instance created later"]
name --> constructor
bases --> constructor
namespace --> constructor
constructor --> class_object --> instance
A class statement is convenient syntax for building a class object. Start Module 09 by making those construction inputs visible before adding a custom metaclass.
The problem: assemble one class from explicit data¶
Imagine a local schema loader has already produced:
- the name
DeliveryNotice - the base class
object - a
channelattribute - a
renderfunction
The pressure is to create one class from that data. Nothing yet requires a policy over
future subclasses. Calling type(...) directly is therefore smaller and more honest than
defining a custom metaclass.
Run the focused evidence¶
From the course directory:
python3 -m unittest discover -s tests -p "test_class_creation_manual.py" -v
python3 -m labs.class_creation
Inspect:
labs/class_creation/manual.pytests/test_class_creation_manual.py- the
manual_constructionobject in the JSON packet
Predict the result before opening the JSON:
- What will
created.__name__be? - What will
created.__bases__contain? - Where will
channelbe stored? - When is the first instance created?
The construction recipe¶
The lab names the three inputs:
@dataclass(frozen=True, slots=True)
class ClassRecipe:
name: str
bases: tuple[type[object], ...]
namespace: dict[str, object]
Then it constructs a class:
def build_class(recipe: ClassRecipe) -> type[object]:
if not recipe.name.isidentifier():
raise ValueError(f"class name must be an identifier: {recipe.name!r}")
namespace = dict(recipe.namespace)
namespace.setdefault("__module__", __name__)
return type(recipe.name, recipe.bases, namespace)
Trace each input to the result:
| Recipe input | Observable result |
|---|---|
name="DeliveryNotice" |
created.__name__ == "DeliveryNotice" |
bases=(object,) |
created.__bases__ == (object,) |
namespace["channel"] |
created.channel == "console" |
namespace["render"] |
an instance binds and calls render as a method |
injected __module__ |
introspection identifies the defining module |
The returned object is a class. It is itself an instance of type.
created = build_class(recipe)
assert isinstance(created, type)
notice = created()
assert type(notice) is created
Class creation happens at the first line. Instance construction happens at the second. Keeping those events separate prevents later confusion about metaclass timing.
Why copy the namespace¶
The lab calls dict(recipe.namespace) before passing it to type.
That copy creates a boundary:
namespace = {"channel": "console"}
created = build_class(ClassRecipe("Delivery", (), namespace))
namespace["channel"] = "webhook"
assert created.channel == "console"
Without the copy, reviewers might incorrectly assume later mutation of the recipe changes the already-created class. The test makes the intended ownership explicit: construction consumes the values present at that moment.
This is a shallow copy. Mutable objects inside the mapping would still be shared. The lab does not claim deep isolation.
Why __module__ matters¶
A class statement normally supplies attributes such as __module__ and __qualname__.
When code creates a class manually, the caller owns enough of that context to keep
introspection understandable.
The lab supplies __module__ when the recipe does not. It deliberately does not invent a
synthetic __qualname__ hierarchy or source location. Manual construction can make source
provenance less obvious, which is one reason ordinary class syntax remains preferable for
ordinary classes.
Failure route: reject invalid construction input¶
The lab rejects a non-identifier name before calling type:
build_class(ClassRecipe("not a class", (), {}))
# ValueError: class name must be an identifier: 'not a class'
This proves a local factory can own local input validation. A custom metaclass would add hierarchy-wide behavior without solving a hierarchy-wide problem.
Other failures remain Python’s responsibility:
- incompatible bases
- invalid metaclass selection derived from those bases
- namespace entries that violate a descriptor or base-class rule
Do not wrap every TypeError merely to make the factory appear more complete.
Where manual construction stops¶
Direct type(...) is a good fit when:
- one explicit call site owns class generation
- the name, bases, and namespace already exist as data
- callers can see when generation happens
- no automatic rule must govern future subclasses
It becomes insufficient when the real invariant is:
- every subclass must be checked automatically
- class-body assignments must be observed while they occur
- one class family needs a consistent construction protocol
Those pressures lead to later sections. They do not retroactively make this factory a metaclass.
Capstone comparison¶
The incident-plugin capstone does not call type(...) from application code to create each
plugin. Plugin authors use ordinary class statements because those declarations are the
reader-facing API.
The capstone still reaches the same primitive: PluginMeta.__new__ delegates to
type.__new__ through super(). Its extra policy is justified only because every plugin
subclass participates in field collection, signature generation, and registration.
That is the transfer:
one explicit generated class
-> direct type(...) call
one automatic policy over a class family
-> consider a metaclass after rejecting lower-power owners
Review exercise¶
For any proposed dynamic class factory, record:
- the exact source of
name,bases, andnamespace - who validates those inputs
- when construction happens
- how the class is discoverable in tracebacks and introspection
- why an ordinary class statement is not clearer
- why future subclasses do or do not need automatic policy
If the final answer is only “metaclasses are more flexible,” the ownership analysis is not complete.
Exit check¶
Continue when you can:
- map all three
type(...)inputs to observable class attributes - explain why the returned class exists before any instance
- identify the shallow namespace-copy boundary
- name one provenance cost of manual construction
- reject a metaclass for this one-call-site problem