foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • quiz
Python
  • Understand metaprogramming concepts and metaclasses
  • Create custom metaclasses for class customization
  • Implement dynamic class creation and modification
  • Apply metaprogramming in real-world scenarios

Metaprogramming with Metaclasses

Introduction

Metaprogramming is the art of writing code that can manipulate, generate, or modify other code at runtime. It's like giving your program the ability to rewrite itself while it runs. In Python, metaclasses are the most powerful tool for metaprogramming, allowing you to control how classes are created and behave.

This is advanced Python territory - metaclasses can be complex, but they enable incredibly powerful patterns. Think of them as "class factories" that can customize class creation itself.


What is Metaprogramming?

Metaprogramming is writing code that manipulates other code. There are several levels:

1. Introspection

Examining code structure at runtime:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# Introspection
print(Person.__name__)        # "Person"
print(Person.__dict__)        # {'__init__': <function>, ...}
print(hasattr(Person, 'name')) # Check if attribute exists

2. Dynamic Code Execution

# Execute code from string
code = "def greet(name): return f'Hello, {name}!'"
exec(code)
print(greet("Alice"))  # "Hello, Alice!"

# Evaluate expressions
result = eval("2 + 3 * 4")
print(result)  # 14

3. Dynamic Class Creation

# Create class dynamically
MyClass = type('MyClass', (), {'x': 42, 'method': lambda self: self.x})
instance = MyClass()
print(instance.method())  # 42

Understanding Metaclasses

A metaclass is a class whose instances are classes. Just as a regular class defines how instances behave, a metaclass defines how classes behave.

The type Metaclass

type is Python's built-in metaclass. When you create a class:

class MyClass:
    x = 42
    
    def method(self):
        return self.x

# This is equivalent to:
MyClass = type('MyClass', (), {'x': 42, 'method': lambda self: self.x})

The type() constructor takes:

  1. Class name (string)
  2. Base classes (tuple)
  3. Class attributes (dictionary)

Custom Metaclass

class MyMeta(type):
    """Custom metaclass that adds debugging info."""
    
    def __new__(cls, name, bases, dct):
        print(f"Creating class {name}")
        print(f"Bases: {bases}")
        print(f"Attributes: {list(dct.keys())}")
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=MyMeta):
    x = 42
    
    def method(self):
        return self.x

# Output when class is defined:
# Creating class MyClass
# Bases: ()
# Attributes: ['__module__', '__qualname__', 'x', 'method']

Metaclass Methods

Metaclasses can override several methods to customize class creation:

new: Class Creation

class AutoRegister(type):
    """Metaclass that automatically registers classes."""
    registry = {}
    
    def __new__(cls, name, bases, dct):
        # Create the class
        new_class = super().__new__(cls, name, bases, dct)
        
        # Register it
        cls.registry[name] = new_class
        
        return new_class

class Plugin(metaclass=AutoRegister):
    pass

class FilePlugin(Plugin):
    def process(self, file):
        return f"Processing {file}"

class ImagePlugin(Plugin):
    def process(self, image):
        return f"Processing image {image}"

# All classes are automatically registered
print(AutoRegister.registry)
# {'Plugin': <class>, 'FilePlugin': <class>, 'ImagePlugin': <class>}

init: Class Initialization

class ValidateAttributes(type):
    """Metaclass that validates class attributes."""
    
    def __init__(cls, name, bases, dct):
        super().__init__(name, bases, dct)
        
        # Validate required attributes
        if not hasattr(cls, 'required_attr'):
            raise TypeError(f"Class {name} must have 'required_attr'")
        
        # Add default attributes
        if not hasattr(cls, 'created_at'):
            cls.created_at = "auto-generated"

class ValidClass(metaclass=ValidateAttributes):
    required_attr = "present"
    # created_at will be added automatically

print(ValidClass.created_at)  # "auto-generated"

# This would raise TypeError
# class InvalidClass(metaclass=ValidateAttributes):
#     pass

call: Instance Creation

class Singleton(type):
    """Metaclass that creates singleton classes."""
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class DatabaseConnection(metaclass=Singleton):
    def __init__(self, host):
        self.host = host
        print(f"Connecting to {host}")

# Only one instance ever created
db1 = DatabaseConnection("localhost")
db2 = DatabaseConnection("localhost")

print(db1 is db2)  # True

Advanced Metaclass Patterns

Attribute Injection

class InjectAttributes(type):
    """Metaclass that injects attributes into classes."""
    
    def __new__(cls, name, bases, dct):
        # Add common attributes to all classes
        dct['created_by_metaclass'] = True
        dct['version'] = '1.0'
        
        # Add methods dynamically
        def get_info(self):
            return f"{self.__class__.__name__} v{self.version}"
        
        dct['get_info'] = get_info
        
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=InjectAttributes):
    def __init__(self, value):
        self.value = value

instance = MyClass(42)
print(instance.created_by_metaclass)  # True
print(instance.get_info())            # "MyClass v1.0"

Method Modification

class AddLogging(type):
    """Metaclass that adds logging to all methods."""
    
    def __new__(cls, name, bases, dct):
        for attr_name, attr_value in dct.items():
            if callable(attr_value) and not attr_name.startswith('_'):
                # Wrap method with logging
                original_method = attr_value
                def logged_method(self, *args, **kwargs):
                    print(f"Calling {attr_name} on {self.__class__.__name__}")
                    result = original_method(self, *args, **kwargs)
                    print(f"{attr_name} returned: {result}")
                    return result
                
                dct[attr_name] = logged_method
        
        return super().__new__(cls, name, bases, dct)

class Calculator(metaclass=AddLogging):
    def add(self, a, b):
        return a + b
    
    def multiply(self, a, b):
        return a * b

calc = Calculator()
result = calc.add(3, 5)  # Logs the method call

Class Validation

class StrictMeta(type):
    """Metaclass that enforces coding standards."""
    
    def __new__(cls, name, bases, dct):
        # Check for required docstring
        if not dct.get('__doc__'):
            raise TypeError(f"Class {name} must have a docstring")
        
        # Check method naming conventions
        for attr_name in dct:
            if callable(dct[attr_name]) and not attr_name.startswith('_'):
                if not attr_name.islower():
                    raise TypeError(f"Method {attr_name} should be lowercase")
        
        # Check for required methods
        required_methods = ['__str__', '__repr__']
        for method in required_methods:
            if method not in dct:
                raise TypeError(f"Class {name} must implement {method}")
        
        return super().__new__(cls, name, bases, dct)

class ValidClass(metaclass=StrictMeta):
    """This class follows all the rules."""
    
    def __init__(self, value):
        self.value = value
    
    def __str__(self):
        return f"ValidClass({self.value})"
    
    def __repr__(self):
        return f"ValidClass({self.value!r})"
    
    def process_data(self):  # lowercase method name
        return self.value * 2

# This would raise TypeError due to missing docstring
# class InvalidClass(metaclass=StrictMeta):
#     pass

Real-World Applications

ORM (Object-Relational Mapping)

class ModelMeta(type):
    """Metaclass for database models."""
    def __new__(cls, name, bases, dct):
        # Create table name from class name
        if not hasattr(cls, 'table_name'):
            dct['table_name'] = name.lower() + 's'
        
        # Collect field definitions
        fields = {}
        for attr_name, attr_value in dct.items():
            if isinstance(attr_value, Field):
                fields[attr_name] = attr_value
        
        dct['_fields'] = fields
        
        # Add database methods
        def save(self):
            # Save to database logic
            print(f"Saving {self.__class__.__name__} to {self.table_name}")
        
        def delete(self):
            # Delete from database logic
            print(f"Deleting {self.__class__.__name__} from {self.table_name}")
        
        dct['save'] = save
        dct['delete'] = delete
        
        return super().__new__(cls, name, bases, dct)

class Field:
    """Base field class."""
    def __init__(self, field_type):
        self.field_type = field_type

class CharField(Field):
    def __init__(self, max_length):
        super().__init__('VARCHAR')
        self.max_length = max_length

class Model(metaclass=ModelMeta):
    pass

class User(Model):
    name = CharField(100)
    email = CharField(255)
    
    def __init__(self, name, email):
        self.name = name
        self.email = email

user = User("Alice", "alice@example.com")
user.save()    # "Saving User to users"
user.delete()  # "Deleting User to users"

Plugin System

class PluginMeta(type):
    """Metaclass for plugin system."""
    plugins = {}
    
    def __new__(cls, name, bases, dct):
        new_class = super().__new__(cls, name, bases, dct)
        
        # Register plugin if it has required methods
        if hasattr(new_class, 'process') and callable(getattr(new_class, 'process')):
            cls.plugins[name] = new_class
        
        return new_class

class Plugin(metaclass=PluginMeta):
    """Base plugin class."""
    pass

class TextPlugin(Plugin):
    def process(self, data):
        return data.upper()

class NumberPlugin(Plugin):
    def process(self, data):
        return data * 2

# All plugins are automatically registered
print(PluginMeta.plugins)  # {'TextPlugin': <class>, 'NumberPlugin': <class>}

# Use plugins dynamically
def process_with_plugin(data, plugin_name):
    plugin_class = PluginMeta.plugins[plugin_name]
    plugin = plugin_class()
    return plugin.process(data)

print(process_with_plugin("hello", "TextPlugin"))   # "HELLO"
print(process_with_plugin(5, "NumberPlugin"))       # 10

API Framework

class APIMeta(type):
    """Metaclass for REST API endpoints."""
    
    def __new__(cls, name, bases, dct):
        # Collect HTTP methods
        methods = {}
        for attr_name, attr_value in dct.items():
            if hasattr(attr_value, '_http_method'):
                methods[attr_value._http_method] = attr_value
        
        dct['_http_methods'] = methods
        
        # Create URL pattern
        if not hasattr(cls, 'url_pattern'):
            dct['url_pattern'] = f"/{name.lower()}"
        
        return super().__new__(cls, name, bases, dct)

def http_method(method):
    """Decorator to mark HTTP methods."""
    def decorator(func):
        func._http_method = method
        return func
    return decorator

class APIEndpoint(metaclass=APIMeta):
    pass

class UserAPI(APIEndpoint):
    url_pattern = "/users"
    
    @http_method('GET')
    def list_users(self):
        return ["Alice", "Bob", "Charlie"]
    
    @http_method('POST')
    def create_user(self, data):
        return f"Created user: {data['name']}"

# Framework can now automatically route requests
print(UserAPI.url_pattern)        # "/users"
print(UserAPI._http_methods)      # {'GET': <function>, 'POST': <function>}

Metaclass vs Class Decorator

Feature Metaclass Class Decorator
When applied Class creation After class creation
Can modify Class structure Class instances
Inheritance Affects subclasses Must be reapplied
Complexity High Low
Use case Framework creation Instance modification

Best Practices

1. Use Metaclasses Sparingly

Metaclasses are powerful but complex. Only use them when:

  • You need to modify class creation itself
  • You're building a framework
  • You need to enforce class-level constraints

2. Prefer Class Decorators When Possible

# Often better than a metaclass
def add_timestamps(cls):
    cls.created_at = datetime.now()
    cls.updated_at = datetime.now()
    return cls

@add_timestamps
class MyModel:
    pass

3. Document Metaclass Behavior

class MyMeta(type):
    """
    Custom metaclass that enforces the following rules:
    1. All classes must have a docstring
    2. All methods must be lowercase
    3. Required methods: __str__, __repr__
    """
    # Implementation

4. Handle Inheritance Carefully

class BaseMeta(type):
    def __new__(cls, name, bases, dct):
        # Check if any base class already has this metaclass
        for base in bases:
            if isinstance(base, cls):
                # Handle inheritance case
                pass
        return super().__new__(cls, name, bases, dct)

Key Points to Remember

  • Metaclasses control how classes are created and behave
  • type is the base metaclass for all Python classes
  • __new__ creates the class, __init__ initializes it, __call__ creates instances
  • Metaclasses are powerful but should be used sparingly
  • Common uses: ORMs, plugin systems, API frameworks, validation
  • Class decorators are often simpler alternatives for many use cases

Metaprogramming with metaclasses represents the pinnacle of Python's flexibility. In our next intermediate topic, we'll explore regular expressions - powerful text processing patterns that are essential for data validation, parsing, and manipulation in real-world applications.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service