Skip to content

Dunder or Magic Methods#

Dunder (double underscore) methods — __init__, __repr__, __eq__, and dozens more — are Python's data model hooks. They let your objects integrate with built-in syntax: print(obj), obj[key], for x in obj, len(obj), obj + other. Mastering the essential subset separates intermediate Python from production-ready code.

This page is the full reference promised in Introdicion to OOPs and Classes and OOP Basics.

How to use this page

Skim the category tables first, then deep-read object representation, equality/hashing, and container protocols. Related: Polymorphism, Custom Containers.

At a glance
Track Python Advanced → Object Oriented Programming
Sections 12 major topics
Outline Use the right-hand TOC to jump

Topics: Naming and philosophy · Object creation and destruction · String representation · Comparison and hashing · Arithmetic and reflected operations · Container and sequence protocol · Callable objects · Context managers · … (+4 more)

  1. Naming and philosophy
  2. Object creation and destruction
  3. String representation
  4. Comparison and hashing
  5. Arithmetic and reflected operations
  6. Container and sequence protocol
  7. Callable objects
  8. Context managers
  9. Attribute access hooks
  10. Boolean and numeric conversion
  11. Dunder methods for interviews — priority list
  12. __slots__ — memory, not really a dunder method

Naming and philosophy#

Term Meaning
Dunder Double underscore prefix and suffix: __name__
Magic method Informal — not actually magic; invoked by Python syntax
Special method Official term in Python data model
Rich comparison __eq__, __lt__, etc.

You never call obj.__add__(other) directly in application code — use obj + other. Python may fall back to reversed operations (__radd__) when needed.


Object creation and destruction#

class Resource:
    def __new__(cls, *args, **kwargs):
        """Allocate object — rarely overridden."""
        print("new")
        return super().__new__(cls)

    def __init__(self, name: str):
        """Initialize instance — common."""
        print("init")
        self.name = name

    def __del__(self):
        """Destructor — unreliable timing; prefer context managers."""
        print("del")
Method When Typical override?
__new__(cls, ...) Before __init__, creates instance Singletons, immutable types, subclasses of str/int
__init__(self, ...) After __new__, configures instance Yes — primary constructor
__del__(self) Garbage collection Rarely — use with / __enter__/__exit__
class ImmutablePoint(tuple):
    def __new__(cls, x: float, y: float):
        return super().__new__(cls, (x, y))

    @property
    def x(self) -> float:
        return self[0]

    @property
    def y(self) -> float:
        return self[1]

String representation#

class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

    def __repr__(self) -> str:
        return f"User({self.name!r}, {self.email!r})"

    def __str__(self) -> str:
        return f"{self.name} <{self.email}>"
Method Called by Audience Goal
__repr__ repr(obj), interactive REPL, containers Developers Unambiguous, ideally reconstructable
__str__ str(obj), print(obj) End users Readable
__format__ format(obj, spec) Both Custom format specs
__bytes__ bytes(obj) Binary protocols Byte serialization

Rule: always define __repr__. If you skip __str__, __str__ falls back to __repr__.

users = [User("Alice", "a@x.com")]
print(users)   # [User('Alice', 'a@x.com')] — uses __repr__ in list

__format__ example#

class Percent:
    def __init__(self, value: float):
        self.value = value

    def __format__(self, spec: str) -> str:
        if spec == ".1":
            return f"{self.value:.1f}%"
        return f"{self.value}%"

f"{Percent(0.875):.1}"   # "0.9%"

Comparison and hashing#

class Interval:
    __slots__ = ("start", "end")

    def __init__(self, start: int, end: int):
        self.start = start
        self.end = end

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Interval):
            return NotImplemented
        return self.start == other.start and self.end == other.end

    def __lt__(self, other: "Interval") -> bool:
        return self.end <= other.start

    def __hash__(self) -> int:
        return hash((self.start, self.end))

    def __repr__(self) -> str:
        return f"Interval({self.start}, {self.end})"

Rich comparison methods#

Method Operator
__eq__ ==
__ne__ != (defaults to not __eq__ in 3.x)
__lt__ <
__le__ <=
__gt__ >
__ge__ >=

Define __eq__ and __lt__; Python can derive the others via functools.total_ordering:

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major: int, minor: int):
        self.major = major
        self.minor = minor

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Version):
            return NotImplemented
        return (self.major, self.minor) == (other.major, other.minor)

    def __lt__(self, other: "Version") -> bool:
        return (self.major, self.minor) < (other.major, other.minor)

__eq__ and __hash__ contract#

Rule Detail
Equal objects must have equal hashes If a == b, then hash(a) == hash(b)
Defining __eq__ disables default hash Unless you define __hash__
Mutable objects Should not be dict keys or set members
__hash__ = None Explicitly unhashable
class MutableKey:
    def __init__(self, x: int):
        self.x = x

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, MutableKey):
            return NotImplemented
        return self.x == other.x

    __hash__ = None   # unhashable — safe for mutable objects

Interview trap — dataclass defaults

from dataclasses import dataclass

@dataclass
class A:
    x: int

@dataclass(eq=False)
class B:
    x: int
- Default @dataclass generates __eq__ and __hash__ (if frozen=False, hash is disabled in 3.10+ when eq=True) - @dataclass(frozen=True) generates both __eq__ and __hash__ — safe for dict keys


Arithmetic and reflected operations#

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

    def __add__(self, other: "Vector") -> "Vector":
        return Vector(self.x + other.x, self.y + other.y)

    def __radd__(self, other) -> "Vector":
        """Called when left operand doesn't support __add__."""
        if other == 0:
            return self
        return NotImplemented

    def __mul__(self, scalar: float) -> "Vector":
        return Vector(self.x * scalar, self.y * scalar)

    def __rmul__(self, scalar: float) -> "Vector":
        return self * scalar

    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"

Vector(1, 2) + Vector(3, 4)   # Vector(4, 6)
3 * Vector(1, 2)              # Vector(3, 6) — via __rmul__
Category Methods
Addition __add__, __radd__, __iadd__ (in-place +=)
Subtraction __sub__, __rsub__, __isub__
Multiplication __mul__, __rmul__, __imul__
Division __truediv__, __rtruediv__, __floordiv__, ...
Unary __neg__, __pos__, __abs__

Return NotImplemented (not raise TypeError) when the operation doesn't apply — Python tries the other operand's reflected method.


Container and sequence protocol#

class Deck:
    def __init__(self, cards: list[str]):
        self._cards = list(cards)

    def __len__(self) -> int:
        return len(self._cards)

    def __getitem__(self, index: int | slice):
        return self._cards[index]

    def __setitem__(self, index: int, value: str) -> None:
        self._cards[index] = value

    def __delitem__(self, index: int) -> None:
        del self._cards[index]

    def __contains__(self, item: str) -> bool:
        return item in self._cards

    def __iter__(self):
        return iter(self._cards)

    def __reversed__(self):
        return reversed(self._cards)
Method Syntax Notes
__len__ len(obj) Also enables truthiness if no __bool__
__getitem__ obj[key] Slice support requires handling slice type
__setitem__ obj[key] = val Mutable mapping/sequence
__delitem__ del obj[key]
__contains__ x in obj Optional — falls back to iteration
__iter__ for x in obj Must return iterator
__next__ next(it) On iterator object, not always on container

Minimal iterable vs full sequence#

# Iterable only — works with for loops
class CountDown:
    def __init__(self, start: int):
        self.start = start

    def __iter__(self):
        return CountDownIterator(self.start)

class CountDownIterator:
    def __init__(self, n: int):
        self.n = n

    def __iter__(self):
        return self

    def __next__(self):
        if self.n <= 0:
            raise StopIteration
        self.n -= 1
        return self.n + 1

Or use a generator:

class CountDown:
    def __init__(self, start: int):
        self.start = start

    def __iter__(self):
        for i in range(self.start, 0, -1):
            yield i

Callable objects#

class Multiplier:
    def __init__(self, factor: int):
        self.factor = factor

    def __call__(self, x: int) -> int:
        return x * self.factor

double = Multiplier(2)
double(5)   # 10 — same as double.__call__(5)

Useful for stateful decorators and functor patterns. See Callable Objects.


Context managers#

class Timer:
    def __enter__(self):
        import time
        self._start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.perf_counter() - self._start
        return False   # don't suppress exceptions

with Timer() as t:
    sum(range(1_000_000))
print(t.elapsed)

Prefer @contextmanager from contextlib for simple cases.


Attribute access hooks#

class Proxy:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, name: str):
        """Called when normal lookup fails."""
        return getattr(self._target, name)

    def __setattr__(self, name: str, value):
        if name == "_target":
            super().__setattr__(name, value)
        else:
            setattr(self._target, name, value)
Method When invoked
__getattribute__ Every attribute access (dangerous to override carelessly)
__getattr__ Access fails via normal lookup
__setattr__ Any attribute assignment
__delattr__ del obj.attr
__dir__ dir(obj) — customize autocomplete

Avoid overriding __getattribute__ unless necessary — easy to cause infinite recursion.


Boolean and numeric conversion#

class Stack:
    def __init__(self):
        self._items: list[int] = []

    def push(self, x: int) -> None:
        self._items.append(x)

    def __len__(self) -> int:
        return len(self._items)

    def __bool__(self) -> bool:
        return len(self._items) > 0

    def __int__(self) -> int:
        return self._items[-1] if self._items else 0
Method Called by
__bool__ bool(obj), if obj:
__int__ int(obj)
__float__ float(obj)
__index__ obj in slice/index contexts needing integer

If __bool__ is undefined, Python falls back to __len__ (non-zero → True).


Dunder methods for interviews — priority list#

Priority Methods Why
Must know __init__, __repr__, __eq__, __hash__ Debugging, collections, equality
High __lt__, __len__, __getitem__, __iter__, __contains__ Sorting, containers
Medium __call__, __enter__/__exit__, __add__ Patterns, resources
Situational __new__, __slots__, __getattr__ Advanced design

__slots__ — memory, not really a dunder method#

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y
Effect Detail
No __dict__ per instance Fixed attributes only
Faster attribute access Marginal
Lower memory Meaningful at millions of instances
Restrictions No arbitrary new attrs; weakref needs __weakref__ slot

Interview traps (quick reference)#

Trap What goes wrong Safe approach
__eq__ without isinstance check TypeError comparing to other types Return NotImplemented
Mutable object as dict key Corrupt hash table / subtle bugs __hash__ = None or frozen dataclass
a == b True but different hashes Violates invariant — set/dict break Ensure hash matches equality
Calling __add__ directly Bypasses Python coercion rules Use + operator
__getattr__ for existing attrs Never called for existing names Use __getattribute__ or normal attrs
Empty __iter__ without __next__ Returns non-iterator Return iterator object or generator
Defining only __eq__ expecting sort TypeError without __lt__ Add ordering or @total_ordering

Mental model checklist#

  1. What is the difference between __repr__ and __str__?
  2. Why must equal objects share the same hash value?
  3. When should __eq__ return NotImplemented vs False?
  4. What is the difference between __iter__ and __next__?
  5. How does Python resolve 3 * Vector(...) when int doesn't know about Vector?

What's next#

Topic Page
Custom container deep dive Custom Containers
Polymorphism Polymorphism
Callable instances Callable Objects
Basics OOP Introdicion to OOPs