Skip to main content
Python

4 meta-programming techniques in Python

5 mins

A blacksmith crafting tools that are then used to make even more tools.

What is Meta-programming? #

Meta-programming is technique in programming where programs modify at runtime their own structure or behavior.

It can be used to reduce boilerplate code, create more flexible code, and analyze code at runtime.

Python has several features that enable meta-programming, such as decorators, metaclasses, and other dynamic features.

Decorators #

Add functionality to existing code without modifying the original code.

You have probably already used decorators in Python without realizing that they are a form of meta-programming.

Decorators are functions that modify the behavior of other functions or classes. They allow you to add functionality to existing code without modifying the original code.

For example, you can use decorators to log function calls, enforce access control, or cache results.

Here is a simple example of a decorator that logs the function and the time it takes to execute. Useful for adding performance metrics without changing the original function code.

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Executing {func.__name__} with arguments: {args}, {kwargs}")
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Execution time: {end_time - start_time:.4f} seconds")
        return result
    return wrapper

@timing_decorator
def example_function(x):
    time.sleep(x)
    return x * 2


example_function(2)  

# Output:
# Executing example_function with arguments: (2,), {}
# Execution time: 2.0001 seconds

Metaclasses #

Metaclasses define how classes are created. You can think of them as “classes of classes.” They allow you to customize class creation, modify class attributes, and enforce certain behaviors.

The following illustrates how to use a metaclass to automatically add a created_at attribute to any class that uses this metaclass.

Note that a class can have only one metaclass, but it can inherit from multiple classes.

# The metaclass that automatically adds a created_at attribute
class AutoCreatedAtMeta(type):
    def __new__(cls, name, bases, attrs):
        # Automatically add a created_at attribute with the current date and time
        from datetime import datetime
        attrs['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        return super().__new__(cls, name, bases, attrs) 

# A base class to inherit from
class Base:
    pass

# This class will automatically have a created_at attribute and also inherit from Base
class MyClass(Base, metaclass=AutoCreatedAtMeta):
    pass

some_instance = MyClass()
print(some_instance.created_at)  # Output: Current date and time in 'YYYY-MM-DD HH:MM:SS' format

Metaclasses are used in ORMs and frameworks.

Typical use cases for metaclasses include:

  • ORMs: Object-Relational Mappers use metaclasses to define how classes map to database tables.
  • Frameworks: Frameworks like Django use metaclasses to create models and views dynamically.

see DJango Metaclasses for an example of how metaclasses are used to create models in Django.

Dynamic Class Creation #

Dynamic class creation allows you to create classes at runtime based on certain conditions or configurations. This can be useful for creating classes that are not known until runtime, such as when generating classes based on user input or configuration files.

The type() function is not just used to get the type of an object

Although you may be familiar with the type() function in Python, which is used to get the type of an object, it can also be used to create classes dynamically.

When is called with these three parameters

  1. the name of the class
  2. a tuple of base classes
  3. a dictionary of attributes

then it creates a new class dynamically.

A basic example is.

MyClass = type('MyClass', (), {})
obj = MyClass()
print(type(obj).__name__) # Outputs: MyClass

A more pragmatic example below shows a database class being created dynamically based on a table name and its columns. Although the definition of the column names are hard-coded, in practice they could be read from a configuration file or database schema.

def create_model_class(table_name, column_names):
    """Create a database model class dynamically"""

    def __init__(self, **kwargs):
        for column_name in column_names:
            setattr(self, column_name, kwargs.get(column_name))

    def __repr__(self):
        attrs = ', '.join(f'{k}={v}' for k, v in self.__dict__.items())
        return f'{table_name}({attrs})'

    # Create a dictionary to hold the class attributes and methods
    class_dict = {
        '__init__': __init__,
        '__repr__': __repr__,
        'table_name': table_name,
        'fields': column_names
    }

    # Create the class dynamically using type()
    return type(table_name, (), class_dict)


# Usage
User = create_model_class('User', ['name', 'email', 'age'])
user = User(name='Alice', email='alice@example.com', age=25)
print(user)  # Output: User(name=Alice, email=alice@example.com, age=25)

Introspection #

Introspection is the ability of a program to examine its own structure, including its classes, functions, and variables. In Python, you can use built-in functions like dir(), type(), and getattr() to inspect objects at runtime. For example, you can use dir() to list the attributes and methods of an object, and getattr() to access a specific attribute or method dynamically.

class MyClass:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, {self.name}!"


def introspect(obj):
    print(f"Type: {type(obj)}")
    print("Attributes and methods:")
    for attr in dir(obj):
        if attr.startswith('__'):
            # Filter out special methods and attributes
            continue

        if callable(getattr(obj, attr)):
            print(f" - {attr}() (method)")
        else:
            print(f" - {attr} (attribute)")


my_instance = MyClass("Alice")
introspect(my_instance)
# Output:
# Type: <class '__main__.MyClass'>
# Attributes and methods:
# - greet() (method)
# - name (attribute)

Conclusion #

Meta-programming is a powerful feature in Python, allowing for creative and dynamic programming techniques and reducing boilerplate code.

Use wisely.

However, it should be used judiciously, as it can make code harder to understand and debug. Auto-generated code will be harder to track in debugging sessions, as it may not be clear where the code is coming from.