Skip to content

Metaclasses & Advanced OOP

Overview

Metaclasses control how classes are created:

  • Classes are objects: Created by metaclasses
  • *new* and init****: Creating and initializing classes
  • Method registration: Auto-register methods
  • Validation: Enforce class design rules
  • Implications for ML: Framework architecture, auto-differentiation decorators

What Are Metaclasses?

The Metaclass Hierarchy

# Classes are instances of their metaclass
class Meta(type):
 """A metaclass."""
 pass

class MyClass(metaclass=Meta):
 """A class created by Meta."""
 pass

# The type of MyClass is Meta
print(type(MyClass)) # <class 'Meta'>

# The type of Meta is type
print(type(Meta)) # <class 'type'>

# All classes by default use 'type' as metaclass
class NormalClass:
 pass

print(type(NormalClass)) # <class 'type'>

Creating Classes Dynamically

Using type()

# Define a class dynamically
MyDynamicClass = type('MyDynamicClass', (object,), {
 'x': 10,
 'method': lambda self: self.x * 2
})

# Create instance
obj = MyDynamicClass()
print(obj.x) # 10
print(obj.method()) # 20

# Equivalent to:
class MyDynamicClass2:
 x = 10
 def method(self):
 return self.x * 2

Metaclass new and init

class Meta(type):
 """Metaclass that prints when class is created."""

 def __new__(mcs, name, bases, namespace):
 print(f"Creating class {name}")

 # Add class variable
 namespace['created_by'] = 'Meta'

 # Create the class
 cls = super().__new__(mcs, name, bases, namespace)
 return cls

 def __init__(cls, name, bases, namespace):
 print(f"Initializing class {name}")
 super().__init__(name, bases, namespace)

class MyClass(metaclass=Meta):
 """Defined with Meta metaclass."""
 pass

# Output:
# Creating class MyClass
# Initializing class MyClass

Practical Metaclass Patterns

Pattern 1: Singleton Metaclass

class SingletonMeta(type):
 """Metaclass that ensures only one instance."""

 _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=SingletonMeta):
 def __init__(self):
 print("Creating database connection")
 self.connection = "Connected"

# Only one instance is created
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True - same object!

Pattern 2: Method Registration

class RegistryMeta(type):
 """Metaclass that auto-registers methods."""

 def __new__(mcs, name, bases, namespace):
 namespace['_registry'] = {}

 # Register methods starting with 'handler_'
 for key, value in namespace.items():
 if callable(value) and key.startswith('handler_'):
 event_name = key[8:] # Remove 'handler_'
 namespace['_registry'][event_name] = value

 return super().__new__(mcs, name, bases, namespace)

class EventHandler(metaclass=RegistryMeta):
 def handler_click(self, event):
 print(f"Handling click: {event}")

 def handler_hover(self, event):
 print(f"Handling hover: {event}")

 def regular_method(self):
 print("Not registered")

# Check registry
print(EventHandler._registry)
# {'click'

# Use it
handler = EventHandler()
for event_name in EventHandler._registry:
 print(f"Registered: {event_name}")

Pattern 3: Validation Metaclass

class ValidatedMeta(type):
 """Metaclass that validates class structure."""

 def __new__(mcs, name, bases, namespace):
 # Ensure all methods have docstrings
 for key, value in namespace.items():
 if callable(value) and not key.startswith('_'):
 if not value.__doc__:
 raise TypeError(f"Method {key} must have a docstring")

 return super().__new__(mcs, name, bases, namespace)

class WellDocumented(metaclass=ValidatedMeta):
 def method_with_doc(self):
 """This has a docstring."""
 pass

# This would raise TypeError:
# class BadDocumented(metaclass=ValidatedMeta):
# def method_without_doc(self):
# pass # No docstring!

Advanced OOP Patterns

Abstract Base Classes

from abc import ABC, abstractmethod

class DataLoader(ABC):
 """Abstract base class for data loaders."""

 @abstractmethod
 def load(self):
 """Load data."""
 pass

 @abstractmethod
 def __iter__(self):
 """Iterate over data."""
 pass

class CSVLoader(DataLoader):
 def load(self):
 return "CSV data"

 def __iter__(self):
 return iter([1, 2, 3])

# Cannot instantiate abstract class
# loader = DataLoader() # TypeError!

# Can instantiate concrete class
csv_loader = CSVLoader()
print(csv_loader.load())

Mixins for Composition

class TimingMixin:
 """Mixin to add timing to methods."""

 def timed(self, func):
 def wrapper(*args, **kwargs):
 import time
 start = time.time()
 result = func(*args, **kwargs)
 elapsed = time.time() - start
 print(f"{func.__name__} took {elapsed:.4f}s")
 return result
 return wrapper

class LoggingMixin:
 """Mixin to add logging."""

 def log(self, message):
 print(f"[LOG] {message}")

class Model(TimingMixin, LoggingMixin):
 def forward(self, x):
 self.log("Forward pass started")
 return x * 2

model = Model()
print(model.forward(5))

Real-World ML Examples

PyTorch Module Metaclass Pattern

class ModuleMeta(type):
 """Mimics PyTorch Module registration."""

 def __new__(mcs, name, bases, namespace):
 # Collect parameters and modules
 namespace['_parameters'] = {}
 namespace['_modules'] = {}

 for key, value in namespace.items():
 if hasattr(value, '__call__') and hasattr(value, 'weight'):
 # Register as parameter
 namespace['_parameters'][key] = value

 return super().__new__(mcs, name, bases, namespace)

class Layer(metaclass=ModuleMeta):
 def __init__(self, in_features, out_features):
 self.weight = [1.0] * out_features
 self.bias = [0.0] * out_features

# Automatically registered
print(Layer._parameters) # Lists registered parameters

JAX PyTree Registration

class PyTreeMeta(type):
 """Metaclass for registering custom PyTree types."""

 _pytrees = {}

 def __new__(mcs, name, bases, namespace):
 cls = super().__new__(mcs, name, bases, namespace)

 # Register custom pytree
 if 'flatten' in namespace and 'unflatten' in namespace:
 mcs._pytrees[name] = cls

 return cls

class CustomArray(metaclass=PyTreeMeta):
 def __init__(self, data):
 self.data = data

 def flatten(self):
 return self.data, None

 @classmethod
 def unflatten(cls, aux, data):
 return cls(data)

# Automatically registered for JAX
print(PyTreeMeta._pytrees)

Summary: When to Use What

Pattern Use Case Complexity
@property Computed attributes Low
Descriptors Attribute access control Medium
ABC (Abstract) Define interfaces Low
Mixins Code reuse Medium
Metaclasses Control class creation High

Best Practices

  1. Use @property first: Simplest for computed attributes
  2. Avoid metaclasses: Use only when necessary (very rare!)
  3. Prefer composition: Mixins over multiple inheritance
  4. Use ABCs: Define clear interfaces
  5. Document heavily: Advanced OOP is hard to understand

-

  • [01 Classes & Inheritance](/05-py3/02-object-oriented-patterns/(01-classes-inheritance/) - Class fundamentals
  • 02 Decorators - Decorator patterns
  • 03 Magic Methods - Special methods
  • [05 Custom Bytecode & Metaprogramming](/05-py3/09-bytecode-and-execution/(05-custom-bytecode-metaprogramming/) - Metaprogramming