Special Methods in Python

Python’s special methods, also known as “magic methods” or “dunder methods” (short for “double underscore”), allow you to define how objects of your classes behave with built-in operations.

1. Initialization and Representation

  • __init__(self, ...): This method is called when a new instance of a class is created. It initializes the object’s attributes.

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    p = Person("Alice", 30)
    print(p.name)  # Output: Alice
  • __repr__(self): Defines the "official" string representation of an object. It's meant to be unambiguous and is used by repr(). It should ideally return a string that could be used to recreate the object.

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __repr__(self):
            return f"Person(name={self.name!r}, age={self.age!r})"
    
    p = Person("Alice", 30)
    print(repr(p))  # Output: Person(name='Alice', age=30)
  • __str__(self): Defines the "informal" string representation of an object. It’s meant to be readable and is used by str() and print().

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __str__(self):
            return f"{self.name}, age {self.age}"
    
    p = Person("Alice", 30)
    print(p)  # Output: Alice, age 30

2. Arithmetic Operations

  • __add__(self, other): Implements addition (+).

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            return Point(self.x + other.x, self.y + other.y)
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
    p1 = Point(1, 2)
    p2 = Point(3, 4)
    print(p1 + p2)  # Output: Point(4, 6)
  • __sub__(self, other): Implements subtraction (-).

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __sub__(self, other):
            return Point(self.x - other.x, self.y - other.y)
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
    p1 = Point(5, 6)
    p2 = Point(2, 3)
    print(p1 - p2)  # Output: Point(3, 3)
  • __mul__(self, other): Implements multiplication (*).

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __mul__(self, scalar):
            return Vector(self.x * scalar, self.y * scalar)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v = Vector(2, 3)
    print(v * 3)  # Output: Vector(6, 9)
  • __truediv__(self, other): Implements true division (/).

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __truediv__(self, scalar):
            return Vector(self.x / scalar, self.y / scalar)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v = Vector(10, 20)
    print(v / 2)  # Output: Vector(5.0, 10.0)
  • __floordiv__(self, other): Implements floor division (//).

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __floordiv__(self, scalar):
            return Vector(self.x // scalar, self.y // scalar)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v = Vector(10, 20)
    print(v // 3)  # Output: Vector(3, 6)
  • __mod__(self, other): Implements modulo operation (%).

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __mod__(self, scalar):
            return Vector(self.x % scalar, self.y % scalar)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v = Vector(10, 22)
    print(v % 7)  # Output: Vector(3, 1)
  • __pow__(self, other): Implements exponentiation (**).

    class Number:
        def __init__(self, value):
            self.value = value
    
        def __pow__(self, exponent):
            return Number(self.value ** exponent)
    
        def __repr__(self):
            return f"Number({self.value})"
    
    n = Number(2)
    print(n ** 3)  # Output: Number(8)

3. Comparison Operations

  • __eq__(self, other): Implements equality comparison (==).

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __eq__(self, other):
            return (self.name, self.age) == (other.name, other.age)
    
    p1 = Person("Alice", 30)
    p2 = Person("Alice", 30)
    print(p1 == p2)  # Output: True
  • __ne__(self, other): Implements inequality comparison (!=).

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __ne__(self, other):
            return (self.name, self.age) != (other.name, other.age)
    
    p1 = Person("Alice", 30)
    p2 = Person("Bob", 30)
    print(p1 != p2)  # Output: True
  • __lt__(self, other): Implements less than comparison (<).

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __lt__(self, other):
            return self.age < other.age
    
    p1 = Person("Alice", 30)
    p2 = Person("Bob", 35)
    print(p1 < p2)  # Output: True
  • __le__(self, other): Implements less than or equal to comparison (<=).

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __le__(self, other):
            return self.age <= other.age
    
    p1 = Person("Alice", 30)
    p2 = Person("Bob", 30)
    print(p1 <= p2)  # Output: True
  • __gt__(self, other): Implements greater than comparison (>).

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __gt__(self, other):
            return self.age > other.age
    
    p1 = Person("Alice", 30)
    p2 = Person("Bob", 25)
    print(p1 > p2)  # Output: True
  • __ge__(self, other): Implements greater than or equal to comparison (>=).

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __ge__(self, other):
            return self.age >= other.age
    
    p1 = Person("Alice", 30)
    p2 = Person("Bob", 30)
    print(p1 >= p2)  # Output: True

4. Attribute Access

  • __getattr__(self, name): Called when trying to access an attribute that doesn’t exist.

    class DynamicAttributes:
        def __getattr__(self, name):
            return f"Attribute {name} not found"
    
    obj = DynamicAttributes()
    print(obj.some_attr)  # Output: Attribute some_attr not found
  • __setattr__(self, name, value): Called when setting an attribute’s value. You can use this method to control how attributes are set or to enforce constraints.

    class SafeDict:
        def __init__(self):
            self._data = {}
    
        def __setattr__(self, name, value):
            if name.startswith('_'):
                super().__setattr__(name, value)
            else:
                self._data[name] = value
    
        def __repr__(self):
            return repr(self._data)
    
    obj = SafeDict()
    obj.key = "value"
    print(obj)  # Output: {'key': 'value'}
  • __delattr__(self, name): Called when deleting an attribute.

    class SafeDict:
        def __init__(self):
            self._data = {}
    
        def __delattr__(self, name):
            if name in self._data:
                del self._data[name]
            else:
                super().__delattr__(name)
    
        def __repr__(self):
            return repr(self._data)
    
    obj = SafeDict()
    obj.key = "value"
    del obj.key
    print(obj)  # Output: {}

5. Container and Sequence Operations

  • __getitem__(self, key): Called to retrieve an item using the obj[key] syntax.

    class MyContainer:
        def __init__(self):
            self.data = [1, 2, 3, 4, 5]
    
        def __getitem__(self, index):
            return self.data[index]
    
    container = MyContainer()
    print(container[2])  # Output: 3
  • __setitem__(self, key, value): Called to set an item using the obj[key] = value syntax.

    class MyContainer:
        def __init__(self):
            self.data = [1, 2, 3, 4, 5]
    
        def __setitem__(self, index, value):
            self.data[index] = value
    
    container = MyContainer()
    container[2] = 10
    print(container.data)  # Output: [1, 2, 10, 4, 5]
  • __delitem__(self, key): Called to delete an item using the del obj[key] syntax.

    class MyContainer:
        def __init__(self):
            self.data = [1, 2, 3, 4, 5]
    
        def __delitem__(self, index):
            del self.data[index]
    
    container = MyContainer()
    del container[2]
    print(container.data)  # Output: [1, 2, 4, 5]
  • __len__(self): Called to return the length of an object using len(obj).

    class MyContainer:
        def __init__(self, items):
            self.items = items
    
        def __len__(self):
            return len(self.items)
    
    container = MyContainer([1, 2, 3])
    print(len(container))  # Output: 3
  • __contains__(self, item): Called to check for membership using the item in obj syntax.

    class MyContainer:
        def __init__(self, items):
            self.items = items
    
        def __contains__(self, item):
            return item in self.items
    
    container = MyContainer([1, 2, 3])
    print(2 in container)  # Output: True
  • __iter__(self): Called to return an iterator object.

  • __next__(self) defines the behavior of an iterator.

    class Countdown:
        def __init__(self, start):
            self.start = start
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.start <= 0:
                raise StopIteration
            current = self.start
            self.start -= 1
            return current
    
    # Usage
    countdown = Countdown(5)
    for number in countdown:
        print(number)
    # Output:
    # 5
    # 4
    # 3
    # 2
    # 1

6. Callable Objects

  • __call__(self, ...): Allows an instance of a class to be called as a function.

    class Adder:
        def __init__(self, increment):
            self.increment = increment
    
        def __call__(self, value):
            return value + self.increment
    
    add_five = Adder(5)
    print(add_five(10))  # Output: 15

7. Context Management

  • __enter__(self): Defines the setup code for a context manager. This method is called when entering the context of the with statement.

    class Resource:
        def __enter__(self):
            print("Resource acquired")
            return self
    
        def __exit__(self, exc_type, exc_value, traceback):
            print("Resource released")
    
    with Resource() as res:
        print("Inside with block")
    # Output:
    # Resource acquired
    # Inside with block
    # Resource released
  • __exit__(self, exc_type, exc_value, traceback): Defines the cleanup code for a context manager. This method is called when exiting the context of the with statement.

    class Resource:
        def __enter__(self):
            print("Resource acquired")
            return self
    
        def __exit__(self, exc_type, exc_value, traceback):
            print("Resource released")
            # Optionally suppress exceptions
            return False
    
    with Resource() as res:
        print("Inside with block")
    # Output:
    # Resource acquired
    # Inside with block
    # Resource released

8. Descriptors

  • __get__(self, instance, owner): Defines behavior for accessing a value. Used in descriptor objects.

    class Descriptor:
        def __get__(self, instance, owner):
            return "Descriptor value"
    
    class MyClass:
        attr = Descriptor()
    
    obj = MyClass()
    print(obj.attr)  # Output: Descriptor value
  • __set__(self, instance, value): Defines behavior for setting a value. Used in descriptor objects.

    class Descriptor:
        def __set__(self, instance, value):
            print(f"Setting value: {value}")
    
    class MyClass:
        attr = Descriptor()
    
    obj = MyClass()
    obj.attr = 42  # Output: Setting value: 42
  • __delete__(self, instance): Defines behavior for deleting a value. Used in descriptor objects.

    class Descriptor:
        def __delete__(self, instance):
            print("Deleting value")
    
    class MyClass:
        attr = Descriptor()
    
    obj = MyClass()
    del obj.attr  # Output: Deleting value

9. Object Lifecycle

  • __new__(cls, ...): Called to create a new instance of a class. It’s a static method used to control the creation of a new instance before __init__ is called.

    class Singleton:
        _instance = None
    
        def __new__(cls, *args, **kwargs):
            if cls._instance is None:
                cls._instance = super().__new__(cls)
            return cls._instance
    
    a = Singleton()
    b = Singleton()
    print(a is b)  # Output: True
  • __del__(self): Called when an object is about to be destroyed, i.e., its destructor. This method is used for cleanup before the object is garbage collected.

    class Resource:
        def __init__(self):
            print("Resource acquired")
    
        def __del__(self):
            print("Resource released")
    
    obj = Resource()
    del obj
    # Output:
    # Resource acquired
    # Resource released

Last updated

Was this helpful?