Python - Python Data Model (Dunder Methods Deep Dive)

The Python Data Model defines how objects behave and interact within the Python runtime. It is built around a set of special methods, commonly called dunder methods (short for “double underscore”), such as __init__, __str__, and __add__. These methods allow developers to customize the behavior of objects so they integrate naturally with Python’s syntax and built-in functions.

At its core, everything in Python is an object, and the data model provides a consistent way to define how these objects respond to operations like addition, comparison, iteration, attribute access, and more. By implementing dunder methods, you are effectively telling Python how your objects should behave in different contexts.


Object Initialization and Representation

The lifecycle of an object begins with the __new__ and __init__ methods.

  • __new__ is responsible for creating a new instance.

  • __init__ initializes the instance after it is created.

Example:

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

For representation, Python provides:

  • __str__ for user-friendly output

  • __repr__ for developer-focused representation

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

    def __str__(self):
        return f"Person: {self.name}"

    def __repr__(self):
        return f"Person('{self.name}')"

__repr__ is ideally unambiguous and can often be used to recreate the object.


Operator Overloading

Dunder methods allow objects to respond to operators like +, -, *, etc. This is known as operator overloading.

Example:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

Now you can use:

v1 + v2

instead of calling a method explicitly. This makes custom objects behave like built-in types.


Comparison Methods

Objects can define how they are compared using methods like:

  • __eq__ (equality)

  • __lt__ (less than)

  • __gt__ (greater than)

Example:

class Student:
    def __init__(self, marks):
        self.marks = marks

    def __lt__(self, other):
        return self.marks < other.marks

This allows:

s1 < s2

Python can also derive other comparisons if enough methods are implemented.


Attribute Access Control

The data model allows full control over attribute access using:

  • __getattr__

  • __getattribute__

  • __setattr__

Example:

class Demo:
    def __getattr__(self, name):
        return "Attribute not found"

If an attribute is missing, Python calls __getattr__.
__getattribute__ is more powerful and intercepts all attribute access but must be used carefully to avoid infinite recursion.


Iterable and Container Behavior

Objects can behave like collections by implementing:

  • __len__ (length)

  • __getitem__ (indexing)

  • __iter__ (iteration)

Example:

class MyList:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, index):
        return self.data[index]

This allows:

obj[0]

To support iteration:

class Counter:
    def __init__(self, limit):
        self.limit = limit

    def __iter__(self):
        self.current = 0
        return self

    def __next__(self):
        if self.current < self.limit:
            self.current += 1
            return self.current
        else:
            raise StopIteration

Context Management

Objects can define behavior for with statements using:

  • __enter__

  • __exit__

Example:

class FileManager:
    def __enter__(self):
        print("Opening resource")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Closing resource")

This ensures proper resource handling, even in case of errors.


Callable Objects

Objects can behave like functions using __call__.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

Usage:

m = Multiplier(3)
m(5)   # returns 15

Hashing and Immutability

To use objects as keys in dictionaries or elements in sets, you implement:

  • __hash__

  • __eq__

Example:

class Item:
    def __init__(self, value):
        self.value = value

    def __hash__(self):
        return hash(self.value)

    def __eq__(self, other):
        return self.value == other.value

Objects must be immutable for consistent hashing.


Why the Data Model Matters

The Python Data Model is what makes Python expressive and flexible. It allows developers to create objects that behave like built-in types, integrate seamlessly with language features, and provide clean, intuitive APIs. Instead of forcing users to learn new function names, you can make your objects work naturally with operators, loops, and standard constructs.

A deep understanding of dunder methods transforms how you design systems in Python. It moves your code from procedural style to a more Pythonic, object-oriented approach where behavior is embedded directly into the objects themselves.