Python 3.14 ·
✓ verified by execution on 2026-07-31
Metaprogramming is simply the act of writing code that manipulates, generates, or inspects other code.
While heavy frameworks like Django and SQLAlchemy use metaprogramming heavily, everyday tools like @dataclass and namedtuple are entirely powered by dynamic class creation and code generation!
Dynamic Classes with type()
You likely know type() as the function that tells you an object’s class. But type is actually the metaclass of all Python objects. When invoked with 3 arguments (name, bases, dict), it acts as a constructor that dynamically creates a brand new class!
python · visualize
# Creating a class dynamicallyRobot = type('Robot', (object,), { 'battery': 100, 'speak': lambda self: "Beep boop!"})# We can instantiate it just like a normal class!wall_e = Robot()print(wall_e.battery)print(wall_e.speak())
This is exactly how collections.namedtuple generates custom tuple subclasses at runtime without writing them to disk!
The ast Module
Often, you want to parse a string of Python code and modify it before it runs. Raw strings are dangerous. The ast module allows you to parse strings into Abstract Syntax Trees (ASTs).
import astsource = "x = 5 + 3"tree = ast.parse(source)# We can inspect the root nodeprint(type(tree).__name__) # Moduleprint(type(tree.body[0]).__name__) # Assign
By walking the AST using ast.NodeVisitor, you can write custom linters, formatters, and security sandboxes.
Secure Execution via types.FunctionType
Passing arbitrary strings into execution builtins is a massive security vulnerability. Instead, you can parse the code into an AST, enforce security rules, compile the AST into a code object, and synthesize a function dynamically!
import astimport typessource = "result = 42 * 2"tree = ast.parse(source)# Compile the safe tree into bytecodecode_obj = compile(tree, filename="<ast>", mode="exec")# Synthesize a dynamic function bypassing global injection!namespace = {}fn = types.FunctionType( compile('def _virtual():\n\t' + source.replace('\n', '\n\t') + '\n\treturn result', '<string>', 'exec').co_consts[0], namespace)print(fn())
Check yourself
What happens when you call the `type()` built-in function with three arguments: name, bases, and dict?
Reveal answer
It creates and returns a brand new class object dynamically at runtime. — `type()` is actually a metaclass constructor. When given three arguments, it dynamically allocates a new class in memory, acting as the foundation for things like `namedtuple`.
Why is the `ast` module generally preferred over string-based `exec()` or `eval()` endpoints when dealing with dynamic code?
Reveal answer
Because `ast` allows you to inspect and modify the parsed syntax tree *before* it is compiled and executed, providing a security sandbox. — Passing arbitrary strings directly into `exec()` is a critical security vulnerability. By parsing strings into an AST first, you can enforce security rules (e.g. banning certain function calls) before passing the tree to `compile()`.
What happens when you pass a compiled AST object into `types.FunctionType`?
Reveal answer
It bypasses standard built-in execution entirely, allowing you to synthesize a callable function object directly in memory with a custom globals dictionary. — `types.FunctionType` expects a compiled code object and a globals dictionary, allowing you to manually construct functions at runtime without ever exposing them to the global namespace via `def`.
Challenges
Challenge 1 +100 XP
Use `type()` to dynamically create a class named `Robot`. It should inherit from `object` and have a class attribute `battery` set to `100`. Print the `battery` attribute of the class.
Import `ast`. Parse the string `source = 'print(42)'` into a tree. Then print the class name of the root node of the tree (e.g. `type(tree).__name__`).