May 3, 2025

#4: What Happens Behind the Scenes When a Class is Created in Python

Python class creation process illustration

Photo by Rubaitul Azad on Unsplash

Have you ever wondered what actually happens when you write class MyClass: in Python? While most developers use classes daily, few truly understand the sophisticated process that transforms your class definition into a functional object. This knowledge isn't just academic curiosity—it's the foundation for writing more powerful, flexible, and efficient Python code.

Why Should You Care About Class Creation?

Understanding Python's class creation mechanism opens doors to advanced programming patterns that can revolutionize your code. Here's what you'll be able to do after mastering these concepts:

When you define a class in Python, a complex and fascinating process unfolds behind the scenes. Understanding this process is crucial for advanced Python programming, especially when working with metaclasses, descriptors, and object-oriented design patterns. In this comprehensive guide, we'll explore every step of Python's class creation mechanism.

What You'll Learn

This deep dive will take you through the complete journey of class creation, from the moment Python encounters your class definition to the final, fully-functional class object. You'll discover:

The Class Definition Syntax

When you write a class definition, Python doesn't immediately create the class object. Instead, it goes through several stages to transform your code into a functional class. Let's start with a simple example:

1class MyClass:
2  """A simple example class"""
3  class_var = "I'm a class variable"
4
5  def __init__(self, value):
6      self.instance_var = value
7
8  def my_method(self):
9      return f"Method called with {self.instance_var}"

This seemingly simple definition triggers a sophisticated creation process that involves several key steps: parsing, namespace creation, metaclass selection, and class object construction.

Step 1: Parsing and Compilation

The first step occurs during Python's parsing phase. The parser recognizes the class statement and creates an Abstract Syntax Tree (AST) node representing the class definition. This AST contains information about:

You can examine the AST representation of your class using Python's ast module:

1import ast
2code = '''
3class MyClass:
4  def method(self):
5      pass
6'''
7tree = ast.parse(code)
8print(ast.dump(tree, indent=2))
9
10# Output:
11# Module(
12#   body=[
13#     ClassDef(
14#       name='MyClass',
15#       bases=[],
16#       keywords=[],
17#       decorator_list=[],
18#       body=[
19#         FunctionDef(
20#           name='method',
21#           args=arguments(args=[arg(arg='self')], ...),
22#           body=[Pass()],
23#           ...
24#         )
25#       ]
26#     )
27#   ]
28# )

Step 2: Namespace Preparation

Before executing the class body, Python needs to prepare a namespace where the class's attributes and methods will be stored. This process involves several important steps:

1class MetaClass(type):
2  @classmethod
3  def __prepare__(cls, name, bases, **kwargs):
4      print(f"Preparing namespace for class '{name}'")
5      # Return a custom namespace (could be OrderedDict, dict, etc.)
6      namespace = {}
7      namespace['_creation_order'] = []
8      return namespace
9
10  def __new__(cls, name, bases, namespace, **kwargs):
11      print(f"Creating class '{name}' with MetaClass")
12      return super().__new__(cls, name, bases, namespace)
13
14class MyClass(metaclass=MetaClass):
15  attr1 = "first"
16  attr2 = "second"
17
18# Output:
19# Preparing namespace for class 'MyClass'
20# Creating class 'MyClass' with MetaClass

Step 3: Class Body Execution

Once the namespace is prepared, Python executes the class body. This execution happens in a special scope where assignments create class attributes, and function definitions become methods. Here's what occurs during execution:

1# This demonstrates what happens during class body execution
2class TrackingMeta(type):
3  def __new__(cls, name, bases, namespace):
4      print("Class namespace contents:")
5      for key, value in namespace.items():
6          if not key.startswith('__'):
7              print(f"  {key}: {type(value).__name__} = {value}")
8      return super().__new__(cls, name, bases, namespace)
9
10class ExampleClass(metaclass=TrackingMeta):
11  # Each of these creates an entry in the namespace
12  class_var = "value"
13
14  def __init__(self):
15      pass
16
17  @property
18  def prop(self):
19      return "property value"
20
21  class NestedClass:
22      pass
23
24# Output:
25# Class namespace contents:
26#   class_var: str = value
27#   __init__: function = <function ExampleClass.__init__ at 0x...>
28#   prop: property = <property object at 0x...>
29#   NestedClass: type = <class '__main__.ExampleClass.NestedClass'>

Step 4: Metaclass Instantiation

After the class body execution completes, Python calls the metaclass (usually type or a custom metaclass) to create the actual class object. This involves calling the metaclass's new and init methods. Learn more about object creation in Python's documentation:

1class DetailedMeta(type):
2  def __new__(cls, name, bases, attrs, **kwargs):
3      print(f"__new__ called with:")
4      print(f"  cls: {cls}")
5      print(f"  name: {name}")
6      print(f"  bases: {bases}")
7      print(f"  kwargs: {kwargs}")
8
9      # Create the class object
10      new_class = super().__new__(cls, name, bases, attrs)
11      print(f"Created class object: {new_class}")
12      return new_class
13
14  def __init__(cls, name, bases, attrs, **kwargs):
15      print(f"__init__ called for class {name}")
16      super().__init__(name, bases, attrs)
17      # Additional initialization can be performed here
18
19class MyClass(metaclass=DetailedMeta, custom_arg="value"):
20  def method(self):
21      pass
22
23# Output:
24# __new__ called with:
25#   cls: <class '__main__.DetailedMeta'>
26#   name: MyClass
27#   bases: ()
28#   kwargs: {'custom_arg': 'value'}
29# Created class object: <class '__main__.MyClass'>
30# __init__ called for class MyClass

Step 5: Attribute Resolution Setup

During class creation, Python sets up the attribute resolution mechanism. This includes configuring the Method Resolution Order (MRO) for method lookups and setting up descriptor protocols:

1class A:
2  def method(self):
3      return "A.method"
4
5class B(A):
6  def method(self):
7      return "B.method"
8
9class C(A):
10  def method(self):
11      return "C.method"
12
13class D(B, C):
14  pass
15
16# Python calculates the MRO during class creation
17print("MRO for class D:")
18for i, cls in enumerate(D.__mro__):
19  print(f"  {i}: {cls.__name__}")
20
21# This affects method resolution
22d = D()
23print(f"d.method(): {d.method()}")  # Will call B.method()
24
25# Output:
26# MRO for class D:
27#   0: D
28#   1: B
29#   2: C
30#   3: A
31#   4: object
32# d.method(): B.method

Step 6: Descriptor and Special Method Processing

Python also processes descriptors and special methods during class creation. Descriptors are objects that define how attribute access is handled:

1class LoggingDescriptor:
2  def __init__(self, name):
3      self.name = name
4
5  def __get__(self, obj, objtype=None):
6      print(f"Getting {self.name}")
7      return obj.__dict__.get(self.name)
8
9  def __set__(self, obj, value):
10      print(f"Setting {self.name} to {value}")
11      obj.__dict__[self.name] = value
12
13class MyClass:
14  # This descriptor is set up during class creation
15  value = LoggingDescriptor('value')
16
17  def __init__(self):
18      self.value = 42
19
20# Usage demonstrates descriptor behavior
21obj = MyClass()  # Sets value to 42 (descriptor.__set__ called)
22print(obj.value)  # Gets value (descriptor.__get__ called)
23
24# Output:
25# Setting value to 42
26# Getting value
27# 42

Exploring the Process with Introspection

Python provides powerful introspection tools to examine the class creation process. You can inspect various aspects of created classes:

1class InspectableClass:
2  def __init__(self):
3      pass
4
5  def method(self):
6      pass
7
8# Inspecting the created class
9print(f"Class name: {InspectableClass.__name__}")
10print(f"Class module: {InspectableClass.__module__}")
11print(f"Class bases: {InspectableClass.__bases__}")
12print(f"Class MRO: {InspectableClass.__mro__}")
13print(f"Class dict keys: {list(InspectableClass.__dict__.keys())}")
14
15# Inspecting method binding
16print(f"Method type: {type(InspectableClass.method)}")
17print(f"Method self: {InspectableClass.method.__self__}")
18
19# Creating an instance and inspecting bound methods
20instance = InspectableClass()
21print(f"Bound method type: {type(instance.method)}")
22print(f"Bound method self: {instance.method.__self__}")
23
24# Output:
25# Class name: InspectableClass
26# Class module: __main__
27# Class bases: (<class 'object'>,)
28# Class MRO: (<class '__main__.InspectableClass'>, <class 'object'>)
29# Class dict keys: ['__module__', '__qualname__', '__init__', 'method', '__dict__', '__weakref__', '__doc__']
30# Method type: <class 'function'>
31# Method self: None
32# Bound method type: <class 'method'>
33# Bound method self: <__main__.InspectableClass object at 0x...>

Advanced Topics: Custom Metaclasses

Understanding class creation enables you to create powerful custom metaclasses that can modify class behavior during creation. For comprehensive examples, check out Real Python's metaclass guide:

1class AutoPropertyMeta(type):
2  """A metaclass that automatically converts _private attributes to properties"""
3  def __new__(cls, name, bases, namespace):
4      # Find private attributes
5      private_attrs = {k: v for k, v in namespace.items()
6                      if k.startswith('_') and not k.startswith('__')}
7
8      # Create properties for private attributes
9      for attr_name, default_value in private_attrs.items():
10          prop_name = attr_name[1:]  # Remove leading underscore
11
12          def make_property(attr):
13              def getter(self):
14                  return getattr(self, attr, default_value)
15              def setter(self, value):
16                  setattr(self, attr, value)
17              return property(getter, setter)
18
19          namespace[prop_name] = make_property(attr_name)
20          del namespace[attr_name]  # Remove the original attribute
21
22      return super().__new__(cls, name, bases, namespace)
23
24class MyClass(metaclass=AutoPropertyMeta):
25  _name = "default"
26  _value = 0
27
28  def __init__(self, name, value):
29      self.name = name  # Uses the auto-generated property
30      self.value = value  # Uses the auto-generated property
31
32# Usage
33obj = MyClass("example", 42)
34print(f"Name: {obj.name}, Value: {obj.value}")
35obj.name = "modified"
36print(f"Modified name: {obj.name}")
37
38# Output:
39# Name: example, Value: 42
40# Modified name: modified

Performance Considerations

Class creation has performance implications, especially when using complex metaclasses or multiple inheritance. For more insights on Python performance, refer to the official profiling guide. Here are some considerations:

1import time
2from timeit import timeit
3
4# Measuring class creation time
5def simple_class_creation():
6  class SimpleClass:
7      def method(self):
8          pass
9  return SimpleClass
10
11def complex_class_creation():
12  class ComplexMeta(type):
13      def __new__(cls, name, bases, attrs):
14          # Simulate complex processing
15          time.sleep(0.001)
16          return super().__new__(cls, name, bases, attrs)
17
18  class ComplexClass(metaclass=ComplexMeta):
19      def method(self):
20          pass
21  return ComplexClass
22
23# Compare creation times
24simple_time = timeit(simple_class_creation, number=100)
25complex_time = timeit(complex_class_creation, number=100)
26
27print(f"Simple class creation: {simple_time:.4f}s")
28print(f"Complex class creation: {complex_time:.4f}s")
29print(f"Overhead: {(complex_time - simple_time) / simple_time * 100:.1f}%")
30
31# Output:
32# Simple class creation: 0.0032s
33# Complex class creation: 0.1045s
34# Overhead: 3165.6%

Debugging Class Creation

When debugging class creation issues, several techniques can help identify problems. The Python debugger (pdb) is especially useful for this:

1class DebugMeta(type):
2  def __new__(cls, name, bases, namespace):
3      print(f"
4DEBUG: Creating class '{name}'")
5      print(f"Bases: {[b.__name__ for b in bases]}")
6      print("Namespace contents:")
7      for key, value in namespace.items():
8          if not key.startswith('__'):
9              print(f"  {key}: {value}")
10
11      # Create the class
12      new_class = super().__new__(cls, name, bases, namespace)
13
14      print(f"MRO: {[c.__name__ for c in new_class.__mro__]}")
15      return new_class
16
17class Parent:
18  parent_attr = "parent"
19
20class Child(Parent, metaclass=DebugMeta):
21  child_attr = "child"
22
23  def method(self):
24      pass
25
26# Output:
27# DEBUG: Creating class 'Child'
28# Bases: ['Parent']
29# Namespace contents:
30#   child_attr: child
31#   method: <function Child.method at 0x...>
32# MRO: ['Child', 'Parent', 'object']

Real-World Applications

Understanding class creation enables powerful programming patterns. For real-world examples, explore:

1# Example: Automatic class registration
2class_registry = {}
3
4class RegisteredMeta(type):
5  def __new__(cls, name, bases, namespace):
6      new_class = super().__new__(cls, name, bases, namespace)
7
8      # Auto-register classes with certain attributes
9      if hasattr(new_class, 'register_name'):
10          class_registry[new_class.register_name] = new_class
11          print(f"Registered {name} as '{new_class.register_name}'")
12
13      return new_class
14
15class Handler(metaclass=RegisteredMeta):
16  register_name = "default_handler"
17
18  def handle(self, data):
19      return f"Handled by {self.__class__.__name__}"
20
21class SpecialHandler(Handler):
22  register_name = "special_handler"
23
24  def handle(self, data):
25      return f"Specially handled: {data}"
26
27# Usage
28print("Registered handlers:", list(class_registry.keys()))
29handler = class_registry["special_handler"]()
30print(handler.handle("test data"))
31
32# Output:
33# Registered Handler as 'default_handler'
34# Registered SpecialHandler as 'special_handler'
35# Registered handlers: ['default_handler', 'special_handler']
36# Specially handled: test data

Key Takeaways

Class creation in Python is a sophisticated process involving multiple stages. Key points to remember:

This deep understanding of Python's class creation mechanism is essential for advanced Python programming, especially when working with frameworks, metaclasses, and complex object-oriented designs. By mastering these concepts, you can create more efficient, flexible, and maintainable Python code.

For further reading, check out Python's Data Model and the PEP 3115 that introduced the __prepare__ method to metaclasses.

Crafted with ❤️, straight from Toronto.

Copyright © 2025, all rights resereved