Magic Methods & Operator Overloading

15 questions found

What are 'magic methods' (dunder methods) in Python, and why are they called that?

Beginner
Magic methods are special methods surrounded by double underscores (e.g., __init__, __str__, __add__) that Python calls implicitly in response to built-in operations like construction, printing, arithmetic, or comparisons. They're called 'dunder' methods (double underscore) and let custom classes integrate with Python's built-in syntax and functions.
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return f'Point({self.x}, {self.y})'

p = Point(1, 2)
print(p)  # Point(1, 2) -- uses __repr__
Real-world example A custom Money class defines __add__, __eq__, and __repr__ so instances can be added and compared with natural + and == syntax instead of explicit method calls.

Common follow-ups: What's the difference between __str__ and __repr__?;Which dunder methods does len() rely on?

Data Types & Structures;Dataclasses & NamedTuples

What is the difference between __str__ and __repr__?

Beginner
__str__ should return a readable, user-facing string (used by print() and str()), while __repr__ should return an unambiguous, ideally eval-able representation useful for debugging (used by the REPL and repr()). If __str__ is not defined, Python falls back to __repr__ for str() and print().
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return f'Point(x={self.x}, y={self.y})'
    def __str__(self):
        return f'({self.x}, {self.y})'

p = Point(1, 2)
print(str(p))   # (1, 2)
print(repr(p))  # Point(x=1, y=2)
Real-world example In a debugging session, a list of custom objects printed in the console shows each item's __repr__, making it crucial to implement a clear, informative __repr__ for every domain model class.

Common follow-ups: What does Python use when only __repr__ is defined?;Why should __repr__ ideally be eval-able?

Magic Methods & Operator Overloading;Debugging & Profiling

How do you overload arithmetic operators like + and - using __add__ and __sub__?

Intermediate
Defining __add__(self, other) lets instances of your class respond to the + operator, and similarly __sub__, __mul__, __truediv__, etc. for other operators. Python calls left.__add__(right) first; if that returns NotImplemented, it tries right.__radd__(left) as a fallback, supporting mixed-type operations.
class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    def __repr__(self):
        return f'Vector({self.x}, {self.y})'

print(Vector(1,2) + Vector(3,4))  # Vector(4, 6)
Real-world example A physics simulation's Vector3D class overloads +, -, *, and dot() so simulation code reads like natural math instead of verbose method calls.

Common follow-ups: What does __radd__ handle that __add__ doesn't?;How do you support in-place addition with __iadd__?

Data Types & Structures;Dataclasses & NamedTuples

What do __eq__ and __hash__ need to satisfy together, and what happens if you only override __eq__?

Intermediate
If two objects are equal (__eq__ returns True), they must have the same hash value so they behave consistently in sets and dict keys. By default, defining __eq__ without __hash__ makes the class unhashable (Python sets __hash__ to None automatically), which raises TypeError if you try to put instances in a set or use them as dict keys.
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)
    def __hash__(self):
        return hash((self.x, self.y))

{Point(1,2)}  # works because __hash__ is defined
Real-world example A caching layer uses custom key objects as dict keys, so their class must define both __eq__ and __hash__ consistently or lookups will fail unpredictably.

Common follow-ups: Why does Python disable __hash__ when __eq__ is overridden?;How does this interact with mutable objects?

Data Types & Structures;Dataclasses & NamedTuples

How do __len__, __getitem__, and __setitem__ let a custom class act like a sequence or mapping?

Intermediate
__len__ enables len(obj); __getitem__ enables obj[key] (and, historically, iteration and 'in' checks via integer index fallback); __setitem__ enables obj[key] = value. Implementing these lets custom container classes integrate naturally with built-in syntax, slicing, and functions expecting sequence-like or mapping-like behavior.
class Deck:
    def __init__(self):
        self._cards = list(range(52))
    def __len__(self):
        return len(self._cards)
    def __getitem__(self, i):
        return self._cards[i]

d = Deck()
print(len(d), d[0], d[-1])
Real-world example A custom Matrix class implements __getitem__ and __setitem__ to support matrix[row, col] indexing via tuple keys, mimicking NumPy-style access.

Common follow-ups: How does __getitem__ enable iteration without __iter__?;What does __contains__ do differently from relying on __getitem__?

Iterators & the Iterator Protocol;Data Types & Structures

How do context manager magic methods __enter__ and __exit__ work, and what does __exit__'s return value control?

Advanced
__enter__(self) runs at the start of a with block and its return value is bound to the as target. __exit__(self, exc_type, exc_val, exc_tb) runs on block exit, receiving exception info if one occurred; returning True suppresses the exception, while returning False (or None) lets it propagate normally.
class Transaction:
    def __enter__(self):
        print('BEGIN')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print('ROLLBACK')
            return False
        print('COMMIT')

with Transaction():
    print('doing work')
Real-world example A database Transaction class uses __enter__/__exit__ to automatically COMMIT on success and ROLLBACK if any exception occurs inside the with block, without repeating try/finally everywhere.

Common follow-ups: How does contextlib.contextmanager simplify writing this?;What happens if __exit__ itself raises an exception?

File I/O & Context Managers;Exception Handling

How does __call__ let an instance of a class be invoked like a function?

Intermediate
Defining __call__(self, *args, **kwargs) makes instances callable with parentheses, e.g., obj(x, y), which Python translates to obj.__call__(x, y). This is used for stateful callables like decorators-as-classes, memoized functions, or configurable strategy objects that need to remember state between calls.
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    def __call__(self, x):
        return x * self.factor

double = Multiplier(2)
print(double(5))  # 10 -- called like a function
Real-world example A machine learning preprocessing pipeline defines transform classes with __call__ so each transform can be applied like a function while still holding configuration state (e.g., normalization parameters).

Common follow-ups: How does __call__ enable class-based decorators?;What's the performance difference vs a plain function?

Decorators;Descriptors & Properties

What is the difference between __new__ and __init__, and when would you override __new__?

Advanced
__new__(cls, ...) is a static method that actually creates and returns the new instance (allocating memory), called before __init__. __init__(self, ...) then initializes that already-created instance and returns None. You override __new__ for cases __init__ can't handle: controlling instance creation for immutable types (like subclassing tuple or str), implementing singletons, or returning an instance of a different class.
class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a, b = Singleton(), Singleton()
print(a is b)  # True
Real-world example A configuration manager implements __new__ to enforce a single shared instance across the entire application, ensuring all code reads the same in-memory config object.

Common follow-ups: Why can't __init__ alone implement a singleton reliably?;How does __new__ interact with immutable types like int subclasses?

Metaclasses & Class Customization;Multiple Inheritance & MRO

How do comparison magic methods like __lt__, __le__, __gt__, and __ge__ support custom sorting?

Intermediate
Implementing these lets instances be compared with <, <=, >, >= respectively, which sorted() and list.sort() rely on by default (via < comparisons) when no key function is given. Using @functools.total_ordering, you can define just __eq__ and one of these to get all six comparison methods automatically.
class Task:
    def __init__(self, priority):
        self.priority = priority
    def __lt__(self, other):
        return self.priority < other.priority

tasks = [Task(3), Task(1), Task(2)]
tasks.sort()  # sorts using __lt__
Real-world example A priority queue of custom Task objects relies on __lt__ so heapq.heappush/heappop can order tasks without needing a separate key function.

Common follow-ups: Why does heapq rely specifically on __lt__?;How does total_ordering reduce the boilerplate here?

functools & Functional Programming Tools;Data Types & Structures

How do __iadd__ and other in-place operator methods (__isub__, __imul__) differ from their non-in-place counterparts?

Advanced
__iadd__(self, other) implements the += operator and can mutate self in place and return self, avoiding creating a new object (unlike __add__, which must return a new object). If __iadd__ isn't defined, Python falls back to __add__ followed by reassignment (x = x + y), which is less efficient for mutable, resizable containers.
class Bag:
    def __init__(self, items=None):
        self.items = items or []
    def __iadd__(self, other):
        self.items.extend(other)
        return self  # mutate and return self

b = Bag([1,2])
b += [3,4]
print(b.items)  # [1,2,3,4]
Real-world example A custom mutable Matrix class implements __iadd__ so accumulating large matrices with += avoids allocating a brand-new matrix object on every update, improving performance in tight loops.

Common follow-ups: What happens if __iadd__ returns a different object than self?;Why do immutable types typically not define __iadd__?

Data Types & Structures;Memory Management & Garbage Collection

Showing 1–10 of 15