Advanced
24 min read
#Python#Metaprogramming#Metaclasses#type#__init_subclass__#AST#Bytecode#Frameworks

Python Metaprogramming, Metaclasses & AST Rewriting — The Complete Master Notebook

Master advanced Python metaprogramming: the type-object ouroboros, complete metaclass lifecycle (__prepare__, __new__, __init__, __call__), writing a declarative ORM from scratch, and AST code transformation.

Python Metaprogramming, Metaclasses & AST Rewriting

1. The Python Metaclass Hierarchy: The Ouroboros#

In Python, everything is an object, and every object has a type. The type of a class is a metaclass. By default, all classes are instances of the built-in metaclass type.

mermaid
graph TD TypeObj["type (Metaclass)"] -->|instance of| TypeObj TypeObj -->|creates class| ClassObj["Custom Class (e.g., User)"] ClassObj -->|creates instance| InstObj["Instance (e.g., user_1)"]
🐍 Python
# The fundamental circular identity of CPython's type system: print(isinstance(object, type)) # True (object is an instance of type) print(isinstance(type, object)) # True (type is an instance of object) print(type(type) is type) # True (type is its own metaclass!) print(type(object) is type) # True (object's metaclass is type)

2. Dynamic Runtime Class Construction with type()#

The built-in type() function has two signatures:

  1. type(instance) \rightarrow Returns the class of the instance.
  2. type(name: str, bases: tuple, namespace: dict) \rightarrow Dynamically constructs a brand new class in memory!
🐍 Python
# Dynamically create class 'DataSchema' inheriting from object def validate_schema(self) -> bool: return len(self.fields) > 0 DynamicDataSchema = type( "DataSchema", # Class Name (__name__) (object,), # Base Classes (__bases__) { # Class Dictionary (__dict__) "version": "1.4.0", "fields": ["id", "timestamp", "payload"], "is_valid": validate_schema } ) schema_instance = DynamicDataSchema() print(schema_instance.version) # "1.4.0" print(schema_instance.is_valid()) # True

3. The Complete Metaclass Lifecycle#

A custom metaclass intercepts class creation at four distinct hook phases:

mermaid
sequenceDiagram autonumber participant Parser as Class Definition Block participant Meta as Metaclass participant NS as __prepare__ Namespace participant Cls as Resulting Class Object Parser->>Meta: 1. Call __prepare__(mcs, name, bases) Meta-->>Parser: Returns custom namespace mapping (e.g. OrderedDict) Parser->>NS: 2. Execute class body & populate attributes Parser->>Meta: 3. Call __new__(mcs, name, bases, namespace) Meta->>Meta: Mutate/validate class attributes & allocate Class object Meta-->>Cls: Returns freshly allocated Class Parser->>Meta: 4. Call __init__(cls, name, bases, namespace)
🐍 Python
class LifecycleAuditMeta(type): @classmethod def __prepare__(mcs, name, bases, **kwargs): print(f"1. __prepare__: Creating custom attribute namespace for class '{name}'") return dict() def __new__(mcs, name, bases, namespace, **kwargs): print(f"2. __new__: Allocating class object '{name}' with {len(namespace)} attributes") cls = super().__new__(mcs, name, bases, namespace) return cls def __init__(cls, name, bases, namespace, **kwargs): print(f"3. __init__: Initializing class object '{name}'") super().__init__(name, bases, namespace) def __call__(cls, *args, **kwargs): print(f"4. __call__: Instantiating an instance of class '{cls.__name__}'") instance = super().__call__(*args, **kwargs) return instance class MonitoredService(metaclass=LifecycleAuditMeta): service_port = 8080 # Output at class definition time (modules import time): # 1. __prepare__: Creating custom attribute namespace for class 'MonitoredService' # 2. __new__: Allocating class object 'MonitoredService' with 4 attributes # 3. __init__: Initializing class object 'MonitoredService' # Output at instance instantiation time: srv = MonitoredService() # 4. __call__: Instantiating an instance of class 'MonitoredService'

4. Writing a Declarative Mini-ORM from Scratch#

This is how enterprise frameworks like Django ORM and SQLAlchemy declare schemas using descriptors and metaclasses:

🐍 Python
from typing import Any, Dict class Field: """Descriptor representing a typed database column.""" def __init__(self, data_type: type, primary_key: bool = False): self.data_type = data_type self.primary_key = primary_key self.name = "" def __set_name__(self, owner, name): self.name = name self.private_name = f"_{name}" def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self.private_name, None) def __set__(self, instance, value): if value is not None and not isinstance(value, self.data_type): raise TypeError(f"Field '{self.name}' must be of type {self.data_type.__name__}, got {type(value).__name__}") setattr(instance, self.private_name, value) class ModelMeta(type): def __new__(mcs, name, bases, namespace): fields: Dict[str, Field] = {} # Collect and extract all Field descriptors declared in class for attr_name, attr_val in list(namespace.items()): if isinstance(attr_val, Field): fields[attr_name] = attr_val namespace["_fields"] = fields namespace["_table_name"] = namespace.get("table_name", name.lower() + "s") return super().__new__(mcs, name, bases, namespace) class Model(metaclass=ModelMeta): def __init__(self, **kwargs): for field_name, field_obj in self._fields.items(): val = kwargs.get(field_name, None) setattr(self, field_name, val) def to_sql_insert(self) -> str: columns = ", ".join(self._fields.keys()) values = ", ".join(repr(getattr(self, f)) for f in self._fields.keys()) return f"INSERT INTO {self._table_name} ({columns}) VALUES ({values});" # Declarative Model Definition class UserRecord(Model): table_name = "users" id = Field(int, primary_key=True) username = Field(str) email = Field(str) user = UserRecord(id=101, username="alice", email="alice@corp.io") print(user.to_sql_insert()) # INSERT INTO users (id, username, email) VALUES (101, 'alice', 'alice@corp.io');

5. Modern Alternative: __init_subclass__#

In modern Python (3.6+), __init_subclass__ eliminates the need for complex metaclasses for 90% of use cases (such as plugin registration, subclass validation, and configuration):

🐍 Python
class SerializablePlugin: _plugin_registry = {} def __init_subclass__(cls, plugin_id: str, auto_register: bool = True, **kwargs): super().__init_subclass__(**kwargs) # Enforce required abstract methods if not hasattr(cls, "serialize") or not callable(getattr(cls, "serialize")): raise TypeError(f"Plugin '{cls.__name__}' must implement a serialize() method.") if auto_register: cls.plugin_id = plugin_id SerializablePlugin._plugin_registry[plugin_id] = cls print(f"[PLUGIN SYSTEM] Registered: '{plugin_id}' -> {cls.__name__}") class JSONSerializer(SerializablePlugin, plugin_id="json"): def serialize(self, data: dict) -> str: import json return json.dumps(data) print(SerializablePlugin._plugin_registry) # {'json': <class '__main__.JSONSerializer'>}

6. Abstract Syntax Tree (AST) Introspection & Code Rewriting#

Python source code is parsed into an Abstract Syntax Tree (AST) before compilation into bytecode. The standard ast module allows you to inspect, analyze, and rewrite code before execution.

🐍 Python
import ast import inspect # 1. Parsing Python source into an AST source_code = """ def calculate_tax(income, rate=0.2): return income * rate """ tree = ast.parse(source_code) print(ast.dump(tree, indent=2)) # 2. AST Visitor: Static Security Linter (Detects dangerous eval() calls) class SecurityLinterVisitor(ast.NodeVisitor): def visit_Call(self, node: ast.Call): if isinstance(node.func, ast.Name) and node.func.id in ("eval", "exec"): print(f" SECURITY ALERT: Dangerous call to '{node.func.id}()' detected at line {node.lineno}!") self.generic_visit(node) unsafe_code = """ def user_input_handler(query): result = eval(query) # Unsafe! return result """ unsafe_tree = ast.parse(unsafe_code) linter = SecurityLinterVisitor() linter.visit(unsafe_tree) # SECURITY ALERT: Dangerous call to 'eval()' detected at line 3!

7. Metaprogramming Architecture Decision Matrix#

ProblemRecommended TechniqueComplexity
Validate/modify subclass attributes__init_subclass__Low
Custom attribute access / type checkingDescriptors (__get__, __set__)Medium
Wrap methods / add logging / cachingFunction / Class DecoratorsLow
Construct classes from dynamic JSON schemastype("ClassName", (bases,), dict)Medium
Intercept class creation / custom namespacesMetaclass (class Meta(type):)High
Static code analysis / bytecode manipulationast module / dis moduleVery High
Knowledge Checkpoint

Metaprogramming, Metaclasses & AST Checkpoint

Q1.In Python's type hierarchy, what is a metaclass, and what is the default metaclass for all standard classes?
AA metaclass is a class whose instances are classes; the default metaclass is `type`.
BA metaclass is an abstract base class; the default is `abc.ABC`.
CA metaclass is a C-extension wrapper; the default is `ctypes.Structure`.
DA metaclass is a decorator factory; the default is `object`.
Q2.What three arguments are required when calling `type()` to dynamically construct a new class at runtime?
A`type(name: str, bases: tuple, namespace: dict)`
B`type(instance: object, class_name: str, methods: list)`
C`type(module: str, docstring: str, fields: dict)`
D`type(superclass: type, subclass: type, config: dict)`
Q3.In the custom metaclass lifecycle, which method is called first to prepare the namespace mapping before the class body is executed?
A`__prepare__(mcs, name, bases, **kwargs)`
B`__new__(mcs, name, bases, namespace)`
C`__init__(cls, name, bases, namespace)`
D`__call__(cls, *args, **kwargs)`
Q4.What lightweight hook method introduced in Python 3.6 allows base classes to customize subclass creation without writing a full custom metaclass?
A`__init_subclass__(cls, **kwargs)`
B`__class_getitem__`
C`__set_name__`
D`__subclasshook__`
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.