Python - Python Data Model (Deep Dive into Dunder Methods)

The Python Data Model defines how objects behave and interact within the Python interpreter. It is essentially a set of rules and protocols that govern how objects respond to built-in operations such as addition, comparison, attribute access, iteration, and more. These behaviors are implemented using special methods, commonly known as dunder methods (double underscore methods), such as __init__, __str__, __len__, etc.

Understanding and mastering the Python Data Model allows you to create objects that behave like built-in types and integrate seamlessly with Python syntax.


1. Object Creation and Initialization

Object creation in Python involves two important methods:

  • __new__(cls, ...): Responsible for creating a new instance of a class. It is a static method and is rarely overridden unless you need control over instance creation (for example, implementing immutable types or singletons).

  • __init__(self, ...): Initializes the instance after it is created.

Example:

class Sample:
    def __new__(cls):
        print("Creating instance")
        return super().__new__(cls)

    def __init__(self):
        print("Initializing instance")

Here, __new__ runs first, followed by __init__.


2. String Representation

These methods define how objects are represented as strings:

  • __str__(self): Returns a user-friendly string (used by print()).

  • __repr__(self): Returns an unambiguous string representation (used in debugging and interpreter).

Example:

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}')"

3. Operator Overloading

Dunder methods allow you to define how operators behave with objects:

  • __add__(self, other) for +

  • __sub__(self, other) for -

  • __eq__(self, other) for ==

  • __lt__(self, other) for <

Example:

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

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

This allows objects of Vector to be added using +.


4. Attribute Access Control

These methods give fine-grained control over attribute access:

  • __getattr__(self, name): Called when an attribute is not found.

  • __getattribute__(self, name): Called for every attribute access.

  • __setattr__(self, name, value): Called when setting an attribute.

  • __delattr__(self, name): Called when deleting an attribute.

Example:

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

Use __getattribute__ carefully because it intercepts all attribute access and can easily lead to recursion errors.


5. Container and Collection Behavior

To make objects behave like sequences or collections:

  • __len__(self)len(obj)

  • __getitem__(self, key)obj[key]

  • __setitem__(self, key, value)obj[key] = value

  • __iter__(self) → iteration support

Example:

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

    def __len__(self):
        return len(self.data)

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

6. Callable Objects

Objects can be made callable like functions using:

  • __call__(self, ...)

Example:

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

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

Now, Multiply(2)(5) returns 10.


7. Context Management

To support the with statement:

  • __enter__(self)

  • __exit__(self, exc_type, exc_value, traceback)

Example:

class FileHandler:
    def __enter__(self):
        print("Entering")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exiting")

8. Memory Optimization with __slots__

__slots__ restricts dynamic attribute creation and reduces memory usage by avoiding __dict__.

Example:

class Point:
    __slots__ = ['x', 'y']

This is useful when creating many instances of a class.


9. Comparison and Hashing

  • __eq__(self, other) defines equality.

  • __hash__(self) allows objects to be used in sets and as dictionary keys.

Important rule: if two objects are equal, their hash values must also be equal.


10. Customizing Truth Value

  • __bool__(self) defines truthiness of an object.

  • If not defined, Python falls back to __len__().

Example:

class Test:
    def __bool__(self):
        return False

Conclusion

The Python Data Model is what makes Python highly flexible and expressive. By implementing dunder methods, you can:

  • Make custom objects behave like built-in types

  • Integrate with Python syntax and operators

  • Control object lifecycle, memory, and interaction

  • Build clean, intuitive APIs

A deep understanding of these methods allows you to write more Pythonic, efficient, and maintainable code, especially in advanced applications such as frameworks, libraries, and system-level programming.