Python’s Dunder (Magic) Methods with Examples

Python provides a set of special methods, also known as dunder (double underscore) methods or magic methods, that allow developers to define how objects of a class interact with built-in functions and operators. These methods begin and end with double underscores (__method__) and enable customisation of object behavior.

Dunder Magic Method

Object Creation & Initialization

These methods control object instantiation and destruction.

  • __new__(cls, …) → Controls object creation (before __init__).
  • __init__(self, …) → Initializes the object after creation.
  • __del__(self) → Called when an object is about to be destroyed.
class Example:
    def __new__(cls, *args, **kwargs):
        print("Creating instance")
        return super().__new__(cls)

    def __init__(self, value):
        print("Initializing instance")
        self.value = value

    def __del__(self):
        print("Instance deleted")


obj = Example(42)
del obj
Python

Output

Creating instance
Initializing instance
Instance deleted
Python

Example

Why we got an error

class my_class():

    def __new__(cls):
        print("inside class")

    def __init__(self):
        print("inside init")
   
   def abc(self):
        print("hi")


obj = my_class()
obj.abc() #AttributeError: 'NoneType' object has no attribute 'abc'
Python

__new__ method

  • __new__ is responsible for creating and returning a new instance of the class.
  • If __new__ doesn’t explicitly return an instance (e.g., return super().__new__(cls)), Python won’t proceed to call __init__, and the object creation fails.

    Key point

    Whenever you are over ridding __new__ , always return a valid instance using return super().__new__(cls) unless you are intentionally preventing object creation.

    class MyClass:
        def __new__(cls):
            print("inside __new__")
            return super().__new__(cls)
    
        def __init__(self):
            print("inside __init__")
            
       def abc(self):
            print("hi")    
    
    
    obj = MyClass()
    obj.abc() # hi
    Python

    __new__ we often use in singleton design pattern

    Read more about class method

    String Representation

    • __str__(self) → Defines str(obj), should return a user-friendly string.
    • __repr__(self) → Defines repr(obj), should return a string for debugging.
    • __format__(self, format_spec) → Defines behavior for format(obj).
    • __bytes__(self) → Defines bytes(obj) for conversion to bytes.

    In Python, __str__ and __repr__ are special methods used for string representations of objects. While they seem similar, they serve different purposes.

    • The goal of __repr__ is to provide an unambiguous and developer-friendly representation of the object.
    • The goal of __str__ is to provide a human-readable and informative representation.
    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __str__(self):
            return f"{self.name} is {self.age} years old."
    
        def __repr__(self):
            return f"Person(name={self.name}, age={self.age})"
    
    p = Person("Alice", 30)
    print(str(p))   # Calls __str__
    print(repr(p))  # Calls __repr__
    print(p) # Calls __str__
    
    '''
    Alice is 30 years old.
    Person(name=Alice, age=30)
    Alice is 30 years old.
    '''
    Python

    Case 1: If both __str__ and __repr__ are not defined

    print(p) #<__main__.Person object at 0x106316f10>
    print(str(p))   # <__main__.Person object at 0x106316f10>
    print(repr(p))  # <__main__.Person object at 0x106316f10>
    Python

    Case 2 : If __str__ is not defined but __repr__ is defined

    print(p) #Person(name=Alice, age=30)
    print(str(p))   # Person(name=Alice, age=30)
    print(repr(p))  # Person(name=Alice, age=30)
    Python

    Case 3: if repr is not defined but str is defined

    print(p) #Alice is 30 years old.
    print(str(p))   # Alice is 30 years old.
    print(repr(p))  # <__main__.Person object at 0x106316f10>
    Python

    Case 4: if both repr and str is defined

    print(p) #Alice is 30 years old.
    print(str(p))   #Alice is 30 years old.
    print(repr(p))  #Person(name=Alice, age=30)
    Python

    Python’s fallback behavior:

    • If both are missing, Python uses <ClassName object at MemoryAddress>
    • If __str__ is missing, Python falls back to __repr__.

    str --> rep --> default 
    C

    Attribute Handling

    • __dict__ → Stores an object’s instance attributes as a dictionary.
    • __getattr__(self, name) → Defines behaviour when an attribute is accessed but not found.
    • __setattr__(self, name, value) → Defines behavior when an attribute is set.
    • __delattr__(self, name) → Defines behavior when an attribute is deleted.
    • __dir__(self) → Customizes dir(obj).

    __dict__

    In Python, dict is a special attribute of objects that stores an object’s attributes (instance variables) in a dictionary format. It is commonly used to inspect an object’s properties dynamically.

    For an Instance of a Class

    class Movie:
        def __init__(self, title, year):
            self.title = title
            self.year = year
    
    movie = Movie("Inception", 2010)
    print(movie.__dict__) # {'title': 'Inception', 'year': 2010}
    Python

    For a Class

    class Movie:
        genre = "Sci-Fi"
    
        def get_title(self):
            print("title")
    
    print(Movie.__dict__)
    Python

    Output

    {'__module__': '__main__', 'genre': 'Sci-Fi', 'get_title': <function Movie.get_title at 0x1063477e0>, '__dict__': <attribute '__dict__' of 'Movie' objects>, '__weakref__': <attribute '__weakref__' of 'Movie' objects>, '__doc__': None}
    
    Python

    Modifying Attributes Dynamically

    You can also modify an object’s attributes dynamically using dict:

    movie.__dict__["director"] = "Christopher Nolan"
    print(movie.director)  # Output: Christopher Nolan
    Python

    Arithmetic Operations :Operator Overloading

    • These methods allow objects to work with mathematical operators.
    • Operators Overloading

    Used to override operators like +, -, *, /, etc.

    • __add__(self, other) → Defines self + other
    • __sub__(self, other) → Defines self – other
    • __mul__(self, other) → Defines self * other
    • __truediv__(self, other) → Defines self / other
    • __floordiv__(self, other) → Defines self // other
    • __mod__(self, other) → Defines self % other
    • __pow__(self, other) → Defines self ** other
    • __radd__(self, other) → Defines other + self (for right operand)
    • __iadd__(self, other) → Defines self += other (in-place addition)
    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)
    
        def __sub__(self, other):
            return Vector(self.x - other.x, self.y - other.y)
    
        def __mul__(self, scalar):
            return Vector(self.x * scalar, self.y * scalar)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v1 = Vector(2, 3)
    v2 = Vector(1, 1)
    
    print(v1 + v2)   # Vector(3, 4)
    print(v1 - v2)   # Vector(1, 2)
    print(v1 * 2)    # Vector(4, 6)
    
    Python

    Comparison Operators

    Used for defining behavior of comparison operators.

    • __eq__(self, other) → self == other
    • __ne__(self, other) → self != other
    • __lt__(self, other) → self < other
    • __le__(self, other) → self <= other
    • __gt__(self, other) → self > other
    • __ge__(self, other) → self >= other
    class Box:
        def __init__(self, volume):
            self.volume = volume
    
        def __eq__(self, other):
            return self.volume == other.volume
    
        def __lt__(self, other):
            return self.volume < other.volume
    
        def __gt__(self, other):
            return self.volume > other.volume
    
    b1 = Box(10)
    b2 = Box(20)
    
    print(b1 == b2)  # False
    print(b1 < b2)   # True
    print(b1 > b2)   # False
    Python

    Type Conversion

    • __int__(self) → Defines int(obj)
    • __float__(self) → Defines float(obj)
    • __complex__(self) → Defines complex(obj)
    • __bool__(self) → Defines bool(obj)
    • __hash__(self) → Defines hash(obj)

    Container & Sequence Protocols

    For making objects behave like lists, dicts, or sets.

    • __len__(self) → Defines len(obj)
    • __getitem__(self, key) → Allows obj[key]
    • __setitem__(self, key, value) → Allows obj[key] = value
    • __delitem__(self, key) → Allows del obj[key]
    • __contains__(self, item) → Defines item in obj
    • __iter__(self) → Defines iteration over an object (for x in obj).
    • __next__(self) → Defines behavior of next(obj).

    Callable Objects

    • __call__(self, …) → Makes an instance callable like a function.

    Context Manager (With Statement)

    • __enter__(self) → Defines what happens at the start of with obj:
    • __exit__(self, exc_type, exc_value, traceback) → Handles cleanup at the end.

    Descriptors (Advanced)

    • __get__(self, instance, owner) → Defines behavior when accessed.
    • __set__(self, instance, value) → Defines behavior when modified.
    • __delete__(self, instance) → Defines behavior when deleted.

    Metaclass Methods

    • __prepare__(cls, name, bases, **kwargs) → Used in metaclasses to prepare a class namespace.
    • __instancecheck__(self, instance) → Defines isinstance(instance, cls).
    • __subclasscheck__(self, subclass) → Defines issubclass(subclass, cls).

    How Are Dunder Methods Called?

    • Dunder (double underscore) methods in Python, also known as magic methods, are automatically invoked by Python when certain operations are performed on an object. These methods don’t need to be explicitly called with self or cls because they are hooked into the Python language itself.
    • This is part of Python’s object-oriented design, allowing objects to behave like built-in types.

    Conclusion

    Python’s dunder methods provide powerful ways to customize class behavior. By overriding these methods, you can integrate custom objects seamlessly with Python’s built-in features. Whether you need arithmetic operations, comparisons, sequence behavior, or even context management, dunder methods make it all possible.

    Resource

    1 thought on “Python’s Dunder (Magic) Methods with Examples”

    Leave a Comment