OOP – Part 2: Magic methods with Python

Written by

Magic methods (also known as dunder methods, for “double underscore”) are special methods in Python that enable operator overloading and hook into built-in behaviors.

🔹 __init__: Object Constructor

Called when an object is created.

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

dog = Dog("Fido")
print(dog.name)  # Fido

🔹 __str__ and __repr__: String Representations

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

    def __str__(self): return f"Dog named {self.name}"
    def __repr__(self): return f"Dog({self.name!r})"

dog = Dog("Fido")
print(dog)        # Dog named Fido
print(repr(dog))  # Dog('Fido')

🔹 __len__: Length of Object

Used by len(obj)

class Basket:
    def __init__(self, items): self.items = items
    def __len__(self): return len(self.items)

b = Basket(["apple", "banana"])
print(len(b))  # 2

🔹 __getitem__, __setitem__, __delitem__: Indexing

class Container:
    def __init__(self): self.data = {}

    def __getitem__(self, key): return self.data[key]
    def __setitem__(self, key, value): self.data[key] = value
    def __delitem__(self, key): del self.data[key]

c = Container()
c['x'] = 42
print(c['x'])  # 42
del c['x']

🔹 __call__: Makes Object Callable Like a Function

class Greeter:
    def __call__(self, name): return f"Hello, {name}!"

greet = Greeter()
print(greet("Alice"))  # Hello, Alice!

🔹 __eq__, __lt__, etc.: Comparisons

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

    def __eq__(self, other): return self.x == other.x
    def __lt__(self, other): return self.x < other.x

a = Point(1)
b = Point(2)
print(a == b)  # False
print(a < b)   # True

🔹 __add__, __sub__, __mul__, etc.: Operator Overloading

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

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

    def __repr__(self): return f"Vector({self.x})"

v1 = Vector(3)
v2 = Vector(4)
print(v1 + v2)  # Vector(7)

🔹 __enter__ and __exit__: Context Manager (with)

class FileManager:
    def __enter__(self):
        print("Open resource")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Close resource")

with FileManager():
    print("Using resource")
# Output:
# Open resource
# Using resource
# Close resource

🔹 __getattr__ and __setattr__: Dynamic Attribute Access

class Dynamic:
    def __getattr__(self, name): return f"No attribute '{name}'"
    def __setattr__(self, name, value):
        print(f"Setting {name} = {value}")
        super().__setattr__(name, value)

d = Dynamic()
print(d.foo)  # No attribute 'foo'
d.x = 10      # Setting x = 10

🔹 __del__: Object Destructor (⚠️ rarely needed)

class Temp:
    def __del__(self):
        print("Object destroyed")

t = Temp()
del t  # Object destroyed (maybe)

🔹 __new__: Call the constructor (⚠️ rarely needed)

__new__ is called before __init__ and is responsible for actually creating the instance of the class. It’s most useful when:

  • You’re subclassing immutable types (int, str, tuple)
  • You need to control the instantiation process (e.g., singleton pattern)

Subclassing immutable types:

class MyInt(int):
    def __new__(cls, value):
        return super().__new__(cls, value + 1)

x = MyInt(5)
print(x)  # 6

Singleton design pattern:

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
print(a is b)  # True

__new__ vs __init__

_new____init__
Creates the object (allocates memory)Initializes the object after it’s created
First method called during instantiationSecond method called
Used for immutable types and special creationUsed for setting attributes

Cheatsheet of magic methods

PurposeMethod
Constructor__new__, __init__
String display__str__, __repr__
Collections__iter__, __next__
Operators__add__, __eq__, etc.
Iteration__iter__, __next__
Context manager__enter__, __exit__
Attribute access__getattr__, __setattr__