type() for Dynamic Class Creation#
type is not only the metaclass of every class — it is also a callable factory that builds classes at runtime from a name, base tuple, and namespace dictionary. Framework authors, plugin systems, and code generators use it when class shape depends on data only known at runtime.
How to use this page
Read with Metaclasses — class Foo: pass ultimately calls type. For safer structured records, compare with @dataclass in Classes and OOP Basics. For executing generated code strings, see Code Generations using exec() and eval().
At a glance
| Track | Python Advanced → Meta Programming |
| Sections | 10 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Two faces of type · Minimal dynamic class · Inheritance at runtime · Building classes from configuration · type() with a custom metaclass · types.new_class — the precise tool · Methods and the namespace dict · __slots__ and memory-conscious dynamic classes · … (+2 more)
- Two faces of
type - Minimal dynamic class
- Inheritance at runtime
- Building classes from configuration
type()with a custom metaclasstypes.new_class— the precise tool- Methods and the namespace dict
__slots__and memory-conscious dynamic classes- Security and trust boundaries
- Comparison table
Two faces of type#
class Dog:
pass
type(Dog) # <class 'type'> — query: what is Dog's class?
type("Cat", (), {}) # <class 'Cat'> — factory: build a new class
| Call form | Purpose |
|---|---|
type(obj) |
Return the class of obj |
type(name, bases, namespace) |
Create a new class dynamically |
Minimal dynamic class#
def greet(self) -> str:
return f"Hello, {self.name}!"
Person = type("Person", (), {"greet": greet})
p = Person()
p.name = "Ada"
p.greet() # 'Hello, Ada!'
Equivalent static form:
The namespace dict can include any attribute that a class body could define: methods, class variables, __slots__, descriptors, docstrings.
Animal = type(
"Animal",
(),
{
"__doc__": "A dynamically created animal.",
"kingdom": "Animalia",
"__repr__": lambda self: f"<Animal {self.name!r}>",
},
)
Inheritance at runtime#
class Base:
def ping(self) -> str:
return "pong"
def shout(self) -> str:
return self.ping().upper()
Loud = type("Loud", (Base,), {"shout": shout})
Loud().shout() # 'PONG'
issubclass(Loud, Base) # True
Multiple inheritance works like static classes:
Mixin = type("Mixin", (), {"tag": "mixed"})
Combo = type("Combo", (Base, Mixin), {})
Combo.__mro__
# (<class 'Combo'>, <class 'Base'>, <class 'Mixin'>, <class 'object'>)
MRO details: Method Resolution Order.
Building classes from configuration#
A common pattern: JSON/YAML/schema → class definition.
from typing import Any
def model_from_fields(name: str, fields: dict[str, type]) -> type:
def __init__(self, **kwargs: Any) -> None:
for key, expected_type in fields.items():
if key not in kwargs:
raise TypeError(f"missing required field: {key}")
value = kwargs[key]
if not isinstance(value, expected_type):
raise TypeError(
f"{key} expected {expected_type.__name__}, "
f"got {type(value).__name__}"
)
setattr(self, key, value)
def __repr__(self) -> str:
parts = ", ".join(f"{k}={getattr(self, k)!r}" for k in fields)
return f"{name}({parts})"
namespace = {"__init__": __init__, "__repr__": __repr__}
return type(name, (), namespace)
User = model_from_fields("User", {"id": int, "email": str})
u = User(id=1, email="ada@example.com")
repr(u) # "User(id=1, email='ada@example.com')"
Production alternative
For field-driven classes, prefer dataclasses.make_dataclass or Pydantic models — better validation, typing, and IDE support than hand-rolled type().
type() with a custom metaclass#
class VerboseMeta(type):
def __new__(mcs, name, bases, namespace, **kwargs):
print(f"Building {name}")
return super().__new__(mcs, name, bases, namespace)
Dynamic = type(
"Dynamic",
(),
{"x": 1, "__metaclass__": VerboseMeta}, # WRONG — see below
)
Interview trap — __metaclass__ in namespace is ignored
Python 3 does not read __metaclass__ from the namespace dict. Pass metaclass= via a metaclass-aware helper or use types.new_class:
import types
Dynamic = types.new_class(
"Dynamic",
(),
{"metaclass": VerboseMeta},
lambda ns: ns.update({"x": 1}),
)
See Metaclasses for the full metaclass protocol.
types.new_class — the precise tool#
types.new_class mirrors the class statement, including metaclass, __prepare__, and execution of a body function:
import types
def body(ns: dict) -> None:
ns["value"] = 42
def double(self) -> int:
return self.value * 2
ns["double"] = double
Widget = types.new_class("Widget", (), {}, body)
Widget().double() # 84
| Tool | When to use |
|---|---|
type(name, bases, ns) |
Quick dynamic class, full namespace already built |
types.new_class(...) |
Need metaclass, __prepare__, or body-function semantics |
class statement |
Static, readable, import-time definition |
Methods and the namespace dict#
Functions placed in the namespace become descriptors (function objects) that bind to instances on access — same as def inside a class body:
def __init__(self, value: int) -> None:
self.value = value
def increment(self, step: int = 1) -> int:
self.value += step
return self.value
Counter = type("Counter", (), {"__init__": __init__, "increment": increment})
Counter().increment(5)
For closures over per-class state, attach class variables in the namespace:
def make_counter() -> type:
def __init__(self) -> None:
self.count = 0
def tick(self) -> int:
self.count += 1
return self.count
return type("Counter", (), {"__init__": __init__, "tick": tick})
__slots__ and memory-conscious dynamic classes#
Point = type("Point", (), {"__slots__": ("x", "y")})
p = Point()
p.x = 1 # OK
# p.z = 3 # AttributeError — no __dict__
Dynamic classes support the same optimization knobs as static ones.
Security and trust boundaries#
Never feed untrusted input into type()
If the name, base classes, or namespace contents come from user input, an attacker can inject arbitrary methods or inherit dangerous mixins. Treat dynamic class creation like exec() / eval() — only trusted, validated schemas.
| Risk | Mitigation |
|---|---|
| Arbitrary code in namespace | Build namespace yourself; never exec into it from users |
| Evil base classes | Whitelist allowed bases |
| Class name injection | Sanitize name to valid identifiers |
Comparison table#
| Feature | class statement |
type(...) |
make_dataclass |
|---|---|---|---|
| Readable | Excellent | Poor | Good |
| Runtime fields | No | Yes | Yes |
| Metaclass support | Native | Via types.new_class |
Limited |
| Validation | Manual | Manual | Basic |
| IDE / typing | Best | Weak | Good |
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
type("C", (), {"__metaclass__": M}) |
Metaclass ignored | types.new_class(..., metaclass=M) |
Expecting type and class to differ |
They use the same protocol | Understand sugar vs factory |
| User-controlled namespace | Code execution | Whitelist fields, no raw exec |
Forgetting __init__ in namespace |
Instances lack initializer | Include or inherit __init__ |
| Dynamic class per request in web app | Memory leak / metaclass churn | Cache by schema hash |
Mental model checklist#
- What three arguments does
type(name, bases, ns)accept? - How is
type("X", (), {"f": f})equivalent to aclassstatement? - Why doesn't
__metaclass__in the namespace dict work in Python 3? - When is
types.new_classrequired over baretype()? - What are safer alternatives for schema-driven classes?
What's next#
| Topic | Page |
|---|---|
| Metaclass protocol | Metaclasses |
| Class decorators | Class Decorators |
| exec / eval risks | Code Generations using exec() and eval() |
| OOP foundations | Classes and OOP Basics |
| Introspection | Inspect Module for Introspection |