Filter concepts by levelShowing all levels.

Python · Object-Oriented Python

Dunder methods

Concepts
12

The methods Python calls for you behind the scenes — construction, string display, comparison, hashing, truthiness, containers, iteration, indexing, the with statement, and calling an instance like a function.

This section

Construction and display

How Name(...) actually builds an object, and the two different strings it can show.

__new__ and __init__

standardintermediate

Name(...) actually runs two steps: __new__(cls, ...) creates and returns a fresh, empty object, then __init__(self, ...) sets up its attributes. __new__ is rarely overridden — almost all classes only ever touch __init__.

Think of it as

__new__ is the factory floor that stamps out a blank object; __init__ is the technician who fills it in afterward. Overriding __init__ is like changing what the technician does to an object already on the bench — overriding __new__ is like changing the stamping machine itself, needed only when the blank object has to be built differently (an immutable type, a singleton) before any setup can even begin.

python
class Name:
    def __new__(cls, *args, **kwargs):
        instance = super().__new__(cls)   # actually allocate
        return instance

    def __init__(self, *args, **kwargs):
        ...                                 # then set up attributes

What we're doing: Override __new__ to make a class a singleton — every call to Name() after the first returns the SAME object, something __init__ alone cannot do.

singleton.pypython
class Config:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, value=None):
        if value is not None:
            self.value = value


a = Config("first")
b = Config("second")

print(a is b)
print(b.value)
4
__new__ runs before __init__ — this is where the singleton check has to happen, since __init__ runs every time regardless.
5
cls._instance is only created on the first call — every later call returns that same stored object.
9
__init__ still runs on every call (b = Config("second") triggers it too), which is why value is only set when explicitly passed.
Output
True
second

Why this works: a is b is True because __new__ returned the exact same cls._instance both times — Config("second") never actually creates a new object, since cls._instance was already set from the first call. __init__ still runs on the second call too (that part is unavoidable — Python calls __init__ whenever __new__ returns a cls instance), which is why b.value ends up "second": the singleton's data can still be overwritten by a later call, even though the object itself is not new.

Overriding __new__ but forgetting it must return an instance of cls

Wrong

python
class Broken:
    def __new__(cls, *args):
        print("creating...")   # forgot to return anything!

b = Broken()
print(b)   # None — __init__ never even ran

Better

python
class Fixed:
    def __new__(cls, *args):
        print("creating...")
        return super().__new__(cls)   # must return the instance

f = Fixed()
print(f)   # a real Fixed object

What you see: b is None, and __init__ never printed anything — no error is raised, which makes this a silent, confusing bug rather than a loud one.

Why: __new__ implicitly returns None if nothing is returned, exactly like any other function — and Python only calls __init__ when __new__ returns an instance of cls specifically. Returning None means construction quietly produces None instead of a Broken object, with no exception anywhere to point at the mistake.

Name(...) is two steps, not one

__new__(cls)

creates a blank object

__init__(self)

sets up its attributes

Name(...)

returns the finished object

  1. __new__(cls) — creates a blank object
  2. __init__(self) — sets up its attributes
  3. Name(...) — returns the finished object

The two-step construction pipeline

The two-step construction pipeline
StepWhat runs
1. Name(args)Python calls type.__call__, which starts the pipeline
2. __new__(cls, args)creates and returns a new, empty object
3. __init__(self, args)runs on that object, only if step 2 returned a cls instance
Resultthe fully constructed object is handed back to the caller

Together

python
class Traced:
    def __new__(cls, *args):
        print("1. __new__ creates the object")
        return super().__new__(cls)

    def __init__(self, name):
        print("2. __init__ sets it up")
        self.name = name

t = Traced("example")
print(t.name)

Remember: Name(...) is two steps: __new__ creates the object, __init__ sets it up. Override __new__ only for singletons or immutable-type subclassing.

See also: constructors · class methods · static methods

__str__ and __repr__

corebeginner

__repr__ returns an unambiguous, developer-facing string — ideally valid Python that recreates the object. __str__ returns a reader-facing string, used by print()/str()/f-strings. If __str__ is missing, Python falls back to __repr__.

Think of it as

__repr__ is the label on an evidence bag — precise, unambiguous, meant for someone debugging the case. __str__ is the caption under a magazine photo — meant for a casual reader, allowed to be prettier and vaguer. print(obj) shows the caption if there is one; the REPL always shows the evidence-bag label, because a REPL session is inherently about inspecting, not casual reading.

python
class Name:
    def __repr__(self):
        return f"Name({self.value!r})"   # unambiguous, ideally valid Python

    def __str__(self):
        return f"{self.value}"            # reader-friendly

What we're doing: Define both dunders and show str()/print() preferring __str__ while repr()/the REPL/container display always use __repr__.

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

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"

    def __str__(self):
        return f"({self.x}, {self.y})"


p = Point(1, 2)
print(p)
print(str(p))
print(repr(p))
print([p])
6
__repr__ is the developer-facing form — here, valid Python that would recreate an identical Point.
9
__str__ is the reader-facing form — simpler, no class name, meant for display rather than debugging.
17
print([p]) shows __repr__, not __str__ — a list's own __repr__ calls repr() on each element, never str().
Output
(1, 2)
(1, 2)
Point(1, 2)
[Point(1, 2)]

Why this works: print(p) and str(p) both use __str__ because that is exactly what str() and print() are defined to prefer. repr(p) uses __repr__ directly. print([p]) is the one that surprises people: a list's own __repr__ builds its display by calling repr() — not str() — on every element, which is why the Point inside it shows as Point(1, 2) even though a bare print(p) showed (1, 2).

Defining only __str__ and expecting the REPL or a container to use it

Wrong

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

    def __str__(self):
        return f"({self.x}, {self.y})"
    # no __repr__ defined

p = Point(1, 2)
print([p])   # NOT "[(1, 2)]" as might be expected

Better

python
class Point:
    def __repr__(self):             # define this too
        return f"Point({self.x!r}, {self.y!r})"

    def __str__(self):
        return f"({self.x}, {self.y})"

What you see: print([p]) shows [<__main__.Point object at 0x...>] — the default, unhelpful repr — instead of using the __str__ that was actually defined.

Why: A list's display always goes through repr() on each element, never str() — defining only __str__ leaves __repr__ at Python's default (<ClassName object at 0x...>), which is exactly what shows up inside any container. The fix is defining __repr__ too — by convention, __repr__ should exist on nearly every class; __str__ is the optional, reader-friendly addition on top of it.

Two audiences, two dunders

__str__

print(obj) — for a reader

__repr__

repr(obj), REPL — for a developer

no __str__?

falls back to __repr__

  1. __str__ — print(obj) — for a reader
  2. __repr__ — repr(obj), REPL — for a developer
  3. no __str__? — falls back to __repr__

Which dunder gets called, and when

Which dunder gets called, and when
SituationMethod called
print(obj), str(obj), f"{obj}"__str__ — falls back to __repr__ if undefined
repr(obj), bare obj in the REPL__repr__ always
Inside a list/dict: print([obj])__repr__ — a container's own repr calls repr() on each item
Neither definedPython's default: <ClassName object at 0x...>

Together

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

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"

    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(1, 2)
print(p)
print([p])

Remember: __repr__ is for developers (unambiguous, recreatable); __str__ is for readers. Without __str__, print()/str() fall back to __repr__ — never the reverse.

See also: equality dunders · f strings · classes and objects

Advertisement

Comparison and hashing

Equality, the four ordering operators, and the hash that has to stay consistent with equality.

__eq__ and __ne__

coreintermediate

__eq__(self, other) defines what == does; without it, Python falls back to comparing identity (is). __ne__ is derived automatically from __eq__'s result — Python negates it, so __ne__ almost never needs defining separately.

Think of it as

Python's default == is asking 'are these the exact same physical object,' like comparing two people by fingerprint. __eq__ redefines the question to 'do these represent the same VALUE,' like comparing two people by name — two different Point(1, 2) objects can now be considered equal, the way two different people can share a name, even though they are not literally the same object in memory.

python
class Name:
    def __eq__(self, other):
        if not isinstance(other, Name):
            return NotImplemented
        return self.value == other.value   # compare by value, not identity

What we're doing: Show that Python's default == compares identity, then override __eq__ to compare by value instead, and confirm __ne__ follows automatically.

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

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y


p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = p1

print(p1 == p2)
print(p1 is p2)
print(p1 == p3)
print(p1 != p2)
6
__eq__ overrides the default is-based comparison — Point instances are now compared by value instead of identity.
7
isinstance(other, Point) guards against comparing to an unrelated type — NotImplemented lets Python try the other side's __eq__ instead of assuming inequality.
9
The actual comparison is by value: matching x and matching y, regardless of whether they are the same object.
Output
True
False
True
False

Why this works: p1 == p2 is True because __eq__ compares x and y by value, and both objects happen to hold (1, 2) — even though p1 is p2 is False, confirming they really are two separate objects in memory. p1 != p2 is False automatically: Python derives __ne__ from __eq__'s result without needing a separate definition, negating True to False.

Defining __eq__ and expecting instances to stay hashable automatically

Wrong

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

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

p = Point(1, 2)
s = {p}   # TypeError!

Better

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

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))   # explicitly restore hashability

p = Point(1, 2)
s = {p}   # works

What you see: TypeError: cannot use 'Point' as a set element (unhashable type: 'Point') — raised the moment the object is put in a set or used as a dict key.

Why: Python automatically sets __hash__ to None on any class that defines __eq__ but not __hash__, because the two must stay consistent — equal objects are required to have equal hashes, and Python cannot verify that automatically, so it disables hashing rather than risk it being wrong. Defining __hash__ explicitly (matching what __eq__ actually compares) is required to make the class hashable again.

== goes through __eq__ if it exists

a == b

calls a.__eq__(b)

custom __eq__

compares values, not identity

a != b

automatically negates __eq__'s result

  1. a == b — calls a.__eq__(b)
  2. custom __eq__ — compares values, not identity
  3. a != b — automatically negates __eq__'s result

Default identity comparison vs. a custom __eq__

Default identity comparison vs. a custom __eq__
Situation== result
No __eq__ definedTrue only if it is the exact same object (is)
__eq__ defined, comparing matching valuesTrue — even for two separate objects
__eq__ defined, comparing an unrelated typeshould return NotImplemented, not False directly
!= with __eq__ but no __ne__automatically the negation of __eq__'s result

Together

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

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)
print(p1 != p2)

Remember: Defining __eq__ replaces identity comparison with value comparison, but sets __hash__ to None — define __hash__ too if instances need to go in a set or dict.

See also: hash dunder · less than le dunders · string representation dunders

__lt__ and __le__

standardintermediate

__lt__(self, other) defines <; __le__(self, other) defines <=. Unlike __eq__/__ne__, Python does not derive one from the other — each needs its own method, or functools.total_ordering can fill in the rest.

Think of it as

Each comparison operator is a separate question Python has to be taught to answer — defining < does not teach Python anything about <=, the way teaching someone 'taller than' says nothing about 'at least as tall as' without a second, explicit lesson. functools.total_ordering is a shortcut tutor: give it __eq__ and just one of __lt__/__le__/__gt__/__ge__, and it derives the rest through pure logic.

python
class Name:
    def __lt__(self, other):
        if not isinstance(other, Name):
            return NotImplemented
        return self.value < other.value

    def __le__(self, other):
        if not isinstance(other, Name):
            return NotImplemented
        return self.value <= other.value

What we're doing: Define __lt__ and __le__ explicitly on a class so sorted() and direct comparisons work correctly.

money.pypython
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __eq__(self, other):
        return isinstance(other, Money) and self.cents == other.cents

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.cents < other.cents

    def __le__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.cents <= other.cents


prices = [Money(700), Money(200), Money(500)]
prices.sort(key=lambda m: m.cents)
print([m.cents for m in prices])
print(Money(200) < Money(500))
print(Money(500) <= Money(500))
8
__lt__ defines < — sort() and sorted() both use it internally to compare elements pairwise.
13
__le__ defines <= separately — Python does not derive it from __lt__ or __eq__ automatically.
21
prices.sort() works purely because __lt__ exists — sort only ever needs "is A less than B," never any other operator.
Output
[200, 500, 700]
True
True

Why this works: prices.sort() succeeds because Python's sort implementation only ever calls __lt__ between pairs of elements — it never needs __le__, __gt__, or __ge__ at all, which is why sorting works here even though this class defines only two of the four possible ordering methods. Money(500) <= Money(500) is True because __le__ was defined explicitly and separately from __lt__ — nothing about defining __lt__ taught Python anything about <=.

Defining __lt__ only, then using > or >= and hitting TypeError

Wrong

python
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __lt__(self, other):
        return self.cents < other.cents

a, b = Money(500), Money(200)
print(a > b)   # TypeError — no __gt__ defined!

Better

python
from functools import total_ordering

@total_ordering
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __eq__(self, other):
        return self.cents == other.cents

    def __lt__(self, other):
        return self.cents < other.cents

a, b = Money(500), Money(200)
print(a > b)   # works — total_ordering derived __gt__

What you see: TypeError: '>' not supported between instances of 'Money' and 'Money' — even though a related operator (__lt__) IS defined on the class.

Why: Python treats each comparison operator as an independent question with its own dunder method — defining __lt__ answers "is this less than that," and says nothing about "is this greater than that." @functools.total_ordering exists specifically to remove this tedium: given __eq__ and just one ordering method, it derives correct implementations of the other three using straightforward logic (a > b is exactly not (a < b or a == b), for example).

What has to be defined for full ordering support

What has to be defined for full ordering support
ApproachWhat is needed
Manual, all four operators__lt__, __le__, __gt__, __ge__ defined individually
@functools.total_ordering__eq__ plus just one of the four — the rest derived
Only __lt__ defined< works; <=, >, >= all raise TypeError
No ordering methods at allevery comparison operator raises TypeError

Together

python
from functools import total_ordering

@total_ordering
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __eq__(self, other):
        return self.cents == other.cents

    def __lt__(self, other):
        return self.cents < other.cents

a, b = Money(500), Money(700)
print(a < b, a <= b, a > b, a >= b)

Remember: __lt__ and __le__ are independent — Python derives neither from the other or from __eq__. @functools.total_ordering fills in all four from __eq__ plus one.

See also: greater than ge dunders · equality dunders · hash dunder

__gt__ and __ge__

standardintermediate

__gt__(self, other) defines >; __ge__(self, other) defines >=. Like __lt__/__le__, neither is derived from the other. If a.__gt__(b) returns NotImplemented, Python tries b.__lt__(a) as a fallback before raising TypeError.

Think of it as

a > b and b < a ask the same real-world question from two different directions — Python takes advantage of that by trying the reflected call automatically. If Money(500).__gt__(other_type_object) does not know how to answer, Python quietly asks 'well, does other_type_object know if IT is less than Money(500)?' before giving up — one polite second attempt, not a full derivation of every operator.

python
class Name:
    def __gt__(self, other):
        if not isinstance(other, Name):
            return NotImplemented
        return self.value > other.value

    def __ge__(self, other):
        if not isinstance(other, Name):
            return NotImplemented
        return self.value >= other.value

What we're doing: Define __gt__ and __ge__ alongside __lt__/__le__, and show Python trying the reflected operator automatically when comparing against an unrelated type.

money.pypython
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __lt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.cents < other.cents

    def __gt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented
        return self.cents > other.cents


a, b = Money(700), Money(200)
print(a > b)
print(a.__gt__(b))
print(a.__gt__("not money"))
10
__gt__ is a separate definition from __lt__ — nothing about defining one taught Python anything about the other.
11
The isinstance guard returns NotImplemented for an unrelated type, rather than crashing trying to read other.cents.
17
Calling a.__gt__(b) directly shows exactly what > actually does underneath — no magic beyond a plain method call.
Output
True
True
NotImplemented

Why this works: a > b and a.__gt__(b) print the same True because > is precisely sugar for calling __gt__ — there is no additional magic. a.__gt__("not money") prints NotImplemented (a real, printable singleton value, not an exception) because the isinstance guard catches the mismatched type — if this had been written as a > "not money" directly instead of calling __gt__ explicitly, Python would have gone on to try "not money".__lt__(a) next, and only raised TypeError if that also failed.

Assuming NotImplemented and False mean the same thing when comparing to an unrelated type

Wrong

python
class Money:
    def __gt__(self, other):
        if not isinstance(other, Money):
            return False   # looks reasonable, but wrong
        return self.cents > other.cents

m = Money(500)
print(m > "not money")   # False — looks fine...
print("not money" < m)   # ALSO False — but should this even be comparable?

Better

python
class Money:
    def __gt__(self, other):
        if not isinstance(other, Money):
            return NotImplemented   # lets Python try the reflected side, or raise cleanly
        return self.cents > other.cents

m = Money(500)
print(m > "not money")   # TypeError — correctly refuses an invalid comparison

What you see: Comparing Money to a completely unrelated type like a string silently returns False instead of raising an error, making an invalid comparison look like a valid, meaningful result.

Why: Returning False for an unrelated type answers a question that should never have been askable in the first place — Money(500) > "not money" is nonsensical, not simply false. NotImplemented tells Python the comparison genuinely could not be performed here, which lets Python either find another way to answer it (the reflected fallback) or raise a clear TypeError — the correct outcome for a truly invalid comparison.

a > b tries a reflected fallback before giving up

a.__gt__(b)

tried first

b.__lt__(a)

tried if the first returns NotImplemented

TypeError

raised only if both attempts fail

  1. a.__gt__(b) — tried first
  2. b.__lt__(a) — tried if the first returns NotImplemented
  3. TypeError — raised only if both attempts fail

What Python tries, and in what order

What Python tries, and in what order
ExpressionPython's attempt order
a > ba.__gt__(b), then b.__lt__(a) if the first returns NotImplemented
a >= ba.__ge__(b), then b.__le__(a) if the first returns NotImplemented
Both attempts return NotImplementedTypeError — no comparison was possible
Neither __gt__ nor __ge__ defined at allTypeError immediately — nothing to try

Together

python
class Money:
    def __init__(self, cents):
        self.cents = cents

    def __lt__(self, other):
        return self.cents < other.cents

    def __gt__(self, other):
        return self.cents > other.cents

a, b = Money(700), Money(200)
print(a > b)
print(b < a)   # same real comparison, other direction

Remember: a > b tries a.__gt__(b), then falls back to b.__lt__(a) if the first returns NotImplemented — one reflected attempt, not a full derivation of every operator.

See also: less than le dunders · equality dunders · hash dunder

__hash__

standardintermediate

__hash__(self) returns an int used to place an object in a dict or set. The one hard rule: objects that are == must return the same hash. Defining __eq__ without __hash__ sets __hash__ to None, making instances unhashable.

Think of it as

A hash is a locker number, not the contents of the locker — two people with equal names must be assigned the SAME locker number, or a dict/set built on locker numbers could put "duplicate" entries in different lockers and never find them again. The rule only runs one direction: equal objects need equal hashes, but two different hashes are already proof enough that the objects are not equal.

python
class Name:
    def __eq__(self, other):
        return isinstance(other, Name) and self.value == other.value

    def __hash__(self):
        return hash(self.value)   # hash the same fields __eq__ compares

What we're doing: Define __hash__ consistently with __eq__ so equal-valued objects collapse correctly when placed in a set.

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

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))


p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(hash(p1) == hash(p2))
points = {p1, p2, p3}
print(len(points))
9
__hash__ hashes the SAME fields (x, y) that __eq__ compares — this is what keeps the hash/equality contract consistent.
10
hash((self.x, self.y)) reuses tuple's own well-tested hash combination rather than inventing one.
18
{p1, p2, p3} is a set literal — p1 and p2 are equal AND hash equal, so the set correctly treats them as one entry.
Output
True
2

Why this works: hash(p1) == hash(p2) is True because both hash the same underlying tuple, (1, 2) — which is exactly the consistency the hash/equality contract requires, since p1 == p2 is also True. The set correctly ends up with 2 elements, not 3: p1 and p2 collapse into a single entry because a set uses hash to find the right bucket, then == to confirm a true match within it — both checks agreeing is what makes deduplication work correctly.

Hashing different fields than __eq__ compares

Wrong

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

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash(self.x)   # only x — inconsistent with __eq__!

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(1, 999)   # different y, but SAME hash as p1

print(p1 == p3)             # False — correctly unequal
print(hash(p1) == hash(p3)) # True — but should differ!

Better

python
class Point:
    def __hash__(self):
        return hash((self.x, self.y))   # hash everything __eq__ compares

What you see: No exception is raised anywhere — the bug is silent, and only shows up as subtly wrong dict/set behaviour (unrelated objects colliding into the same bucket far more often than chance would predict).

Why: The hash contract only requires a == b to imply hash(a) == hash(b) — it does NOT forbid unequal objects from sharing a hash too, so hashing only x instead of (x, y) is not technically a broken contract, but it is a bad one: every Point with the same x now collides in the same bucket regardless of y, degrading a dict/set from fast lookups toward a slow linear scan within that bucket.

What happens to __hash__, by what a class defines

What happens to __hash__, by what a class defines
Class defines__hash__ behaviour
Neither __eq__ nor __hash__default identity-based hash — every instance hashable
__eq__ only, no __hash____hash__ set to None automatically — instances become unhashable
__eq__ and __hash__ bothhashable, using whatever __hash__ returns
__hash__ returning a non-intTypeError the moment hash() is actually called

Together

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

    def __eq__(self, other):
        return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))

p1, p2 = Point(1, 2), Point(1, 2)
print(hash(p1) == hash(p2))
print({p1, p2})

Remember: The rule: a == b implies hash(a) == hash(b). Defining __eq__ without __hash__ makes instances unhashable — restore it, hashing the fields __eq__ compares.

See also: equality dunders · hashability · less than le dunders

Advertisement

Truthiness and containers

What makes an object truthy, sized, searchable, iterable, and indexable — the built-in container behaviours.

__bool__

standardintermediate

__bool__(self) defines what bool(obj) and if obj: evaluate to. Without it, Python falls back to __len__ (0 means falsy); without either, every instance is truthy by default.

Think of it as

Truthiness is Python asking a single yes/no question about an object before branching — __bool__ lets a class answer that question directly ("is this cart empty?"), rather than making Python guess from __len__ or default to always "yes."

python
class Name:
    def __bool__(self):
        return self.some_condition   # must return True/False

What we're doing: Define __bool__ so a Cart is falsy when empty, and compare it to a class relying only on __len__ for the same behaviour.

cart.pypython
class Cart:
    def __init__(self, items):
        self.items = items

    def __bool__(self):
        return len(self.items) > 0

    def __len__(self):
        return len(self.items)


empty = Cart([])
full = Cart(["book", "pen"])

print(bool(empty), bool(full))
if empty:
    print("has items")
else:
    print("cart is empty")
5
__bool__ is checked FIRST — it takes priority over __len__ whenever both are defined.
6
The actual rule here — nonempty items list — happens to match what __len__ == 0 would already imply, but __bool__ makes the intent explicit rather than relying on the fallback.
17
if empty: goes through the exact same lookup as bool(empty) — no separate code path for if statements.
Output
False True
cart is empty

Why this works: bool(empty) is False and bool(full) is True because __bool__ directly answers whether the cart has any items — if __bool__ had NOT been defined here, __len__ alone would have produced the identical result, since Python's fallback rule is exactly "falsy when len() == 0." Defining __bool__ anyway makes the truthiness rule explicit rather than incidental, which matters once a class's notion of "empty" ever needs to diverge from its length.

Assuming __len__ alone works when 'empty' isn't the same thing as 'length zero'

Wrong

python
class Connection:
    def __init__(self, is_open):
        self.is_open = is_open
    # relying on... nothing — no __bool__, no __len__

conn = Connection(is_open=False)
if conn:               # always True! is_open is never consulted
    print("using a closed connection by mistake")

Better

python
class Connection:
    def __init__(self, is_open):
        self.is_open = is_open

    def __bool__(self):
        return self.is_open   # truthiness has nothing to do with length

conn = Connection(is_open=False)
if conn:
    print("using a closed connection by mistake")
else:
    print("correctly detected as falsy")

What you see: if conn: takes the truthy branch even though the connection is closed, because with neither __bool__ nor __len__ defined, every instance is unconditionally truthy.

Why: "Emptiness" for a container (a list, a Cart) naturally maps to __len__ == 0, but not every class has a length-like concept at all — a Connection's truthiness is about open/closed state, which __len__ cannot express. __bool__ is the general-purpose hook precisely for classes whose truthiness is not naturally a "how many items" question.

What Python checks, in order, for truthiness

What Python checks, in order, for truthiness
Defined on the classif obj: behaviour
__bool__ definedcalls it directly — whatever it returns
No __bool__, but __len__ definedfalsy only if len(obj) == 0
Neither definedalways truthy, no exceptions
__bool__ returns something other than True/FalseTypeError

Together

python
class Cart:
    def __init__(self, items):
        self.items = items

    def __bool__(self):
        return len(self.items) > 0

empty = Cart([])
full = Cart(["book"])
print(bool(empty), bool(full))
if not empty:
    print("cart is empty")

Remember: __bool__ takes priority over __len__ for truthiness — without either, every instance is truthy. Reach for __bool__ when truthiness is not a "length" question.

See also: container dunders · truthiness · equality dunders

__len__ and __contains__

standardintermediate

__len__(self) defines what len(obj) returns, and must return a non-negative int. __contains__(self, item) defines what item in obj checks. Without __contains__, in falls back to iterating with __iter__ and comparing each element.

Think of it as

A shopping cart class implementing __len__ is answering "how many items," and __contains__ is answering "is this specific item in here" — two related but genuinely separate questions about the same container. Without a direct answer to the second one, Python answers it the slow way: walking every item one at a time and comparing, exactly like checking a cart by hand instead of reading a barcode index.

python
class Name:
    def __len__(self):
        return len(self._items)

    def __contains__(self, item):
        return item in self._items

What we're doing: Define both dunders on a small wrapper class, and show __contains__ being used directly by in rather than falling back to iteration.

cart.pypython
class Cart:
    def __init__(self, items):
        self.items = items

    def __len__(self):
        return len(self.items)

    def __contains__(self, item):
        print(f"checking membership of {item!r} directly")
        return item in self.items


c = Cart(["book", "pen", "notebook"])
print(len(c))
print("pen" in c)
print("laptop" in c)
5
__len__ delegates to the underlying list's own len() — the wrapper just forwards the question.
8
__contains__ is called directly for "pen" in c — the print confirms this path runs, rather than a manual __iter__-based scan.
9
The comparison itself still uses the underlying list's own in, just wrapped behind the class's own method.
Output
3
checking membership of 'pen' directly
True
checking membership of 'laptop' directly
False

Why this works: "pen" in c prints the membership-check message before returning True because __contains__ is called directly, not falls back to manual iteration — this is the entire benefit of defining __contains__ explicitly: it can be implemented far more efficiently than a linear scan (a set-backed lookup, a database query) while in still reads exactly the same at every call site.

Relying on the __iter__ fallback for 'in' when a faster __contains__ would matter

Wrong

python
class HugeCart:
    def __init__(self, items):
        self.items = items   # a huge list, checked often

    def __iter__(self):
        return iter(self.items)
    # no __contains__ — falls back to a full linear scan every time

cart = HugeCart(list(range(1_000_000)))
print(999_999 in cart)   # works, but scans up to a million items

Better

python
class HugeCart:
    def __init__(self, items):
        self.items = items
        self._lookup = set(items)   # O(1) membership, built once

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

    def __contains__(self, item):
        return item in self._lookup   # set membership, not a scan

cart = HugeCart(list(range(1_000_000)))
print(999_999 in cart)   # instant, via the set

What you see: Both versions return the correct answer, but the __iter__-fallback version becomes measurably slower as the container grows, since every "in" check silently scans from the beginning.

Why: The __iter__ fallback for in is correct but always O(n) — Python has no way to know a faster check exists unless __contains__ says so explicitly. Defining __contains__ with a set-backed lookup (or any O(1)/O(log n) structure) changes item in obj from a linear scan into a fast, direct check, with zero change to how callers write the check.

Two separate questions about the same container

__len__

len(obj) — how many

__contains__

item in obj — is this one present

both defined

obj behaves like a real container

  1. __len__ — len(obj) — how many
  2. __contains__ — item in obj — is this one present
  3. both defined — obj behaves like a real container

What in actually does, depending on what is defined

What in actually does, depending on what is defined
Class definesitem in obj behaviour
__contains__calls it directly — fastest, most explicit path
No __contains__, but __iter__falls back to iterating and comparing each element with ==
Neither __contains__ nor __iter__TypeError — not a container Python knows how to search
__len__ alone (no __contains__/__iter__)len(obj) still works; in still raises TypeError

Together

python
class Cart:
    def __init__(self, items):
        self.items = items

    def __len__(self):
        return len(self.items)

    def __contains__(self, item):
        return item in self.items

c = Cart(["book", "pen"])
print(len(c))
print("book" in c)
print("laptop" in c)

Remember: __contains__ is checked before falling back to a linear __iter__ scan for "in" — define it whenever a faster membership check is available.

See also: bool dunder · iterator protocol dunders · indexing dunders

__iter__ and __next__

coreintermediate

__iter__(self) makes an object iterable — for x in obj: calls it once for an iterator. __next__(self) makes an object an iterator, returning the next value each call and raising StopIteration when exhausted.

Think of it as

__iter__ hands out a fresh bookmark into a book; __next__ turns one page and reads it. A class is 'iterable' if it can hand out bookmarks (__iter__); an object is an 'iterator' if it IS a bookmark that knows how to advance itself (__next__). Many classes implement both on the same object — a self-iterator — but the two responsibilities are genuinely separate.

python
class Name:
    def __iter__(self):
        return self          # this object is also its own iterator

    def __next__(self):
        if done:
            raise StopIteration
        return next_value

What we're doing: Implement both dunders on a Countdown class, then show that two separate for loops over the same instance interfere with each other — because __iter__ returns self instead of a fresh iterator.

countdown.pypython
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

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


c = Countdown(3)
for n in c:
    print(n)

print(list(c))
5
__iter__ returns self — Countdown is both the iterable AND the iterator, a common but not universal pattern.
8
__next__ raises StopIteration once current reaches 0 — this is what tells the for loop to stop.
20
list(c) tries to iterate the SAME object again — but its internal state (current) was already exhausted by the first loop.
Output
3
2
1
[]

Why this works: The for loop correctly prints 3, 2, 1 by calling __next__ until StopIteration. list(c) afterward returns an empty list — not an error, but a silent surprise — because __iter__ returned self, and self.current is already 0 from the first loop. A fresh iterator would restart from the beginning; this self-iterator has no way to be 'rewound,' since there is only one current value shared by every iteration attempt.

Returning self from __iter__ when a class is meant to be iterated multiple times

Wrong

python
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self   # exhausted after ONE full iteration

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

c = Countdown(3)
print(list(c))   # [3, 2, 1]
print(list(c))   # [] — silently empty the second time!

Better

python
class Countdown:
    def __init__(self, start):
        self.start = start   # remember the ORIGINAL value

    def __iter__(self):
        return CountdownIterator(self.start)   # a fresh iterator every time

class CountdownIterator:
    def __init__(self, current):
        self.current = current

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

    def __iter__(self):
        return self

c = Countdown(3)
print(list(c))   # [3, 2, 1]
print(list(c))   # [3, 2, 1] again — a fresh iterator each time

What you see: The second for loop (or list()/sum() call) over the same object silently produces nothing, with no error to explain why — it looks like the data disappeared.

Why: A class where __iter__ returns self can only ever be iterated once, because the iteration state (self.current) lives directly on the object itself, not in a separate, disposable iterator. Splitting the iterable (Countdown, holds the original config) from the iterator (CountdownIterator, holds the in-progress state) is what allows the SAME iterable to be iterated independently, as many times as needed.

A for loop is exactly iter() once, then next() repeatedly

iter(obj)

called once, returns an iterator

next(iterator)

called repeatedly, one value per call

StopIteration

raised when exhausted — the loop ends cleanly

  1. iter(obj) — called once, returns an iterator
  2. next(iterator) — called repeatedly, one value per call
  3. StopIteration — raised when exhausted — the loop ends cleanly

Iterable vs. iterator — two related but different roles

Iterable vs. iterator — two related but different roles
RoleDefines
Iterable__iter__, returning an iterator (often not itself)
Iterator__next__, returning one value per call, raising StopIteration when done
Self-iterator (common)both — __iter__ returns self, __next__ advances state on self
for x in obj:calls iter(obj) once, then next() repeatedly until StopIteration

Together

python
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

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

for n in Countdown(3):
    print(n)

Remember: __iter__ returns an iterator (often self); __next__ returns one value or raises StopIteration. Returning self means the object can only be fully iterated once.

See also: iter next · container dunders · indexing dunders

__getitem__ and __setitem__

standardintermediate

__getitem__(self, key) defines what obj[key] returns; __setitem__(self, key, value) defines what obj[key] = value does. key can be an int, a string, or a slice object — the method decides what to do with whatever it receives.

Think of it as

__getitem__ is a class-defined vending machine slot — obj[key] presses the button labeled key and the method decides what comes out, whether key looks like a row number, a name, or a whole range. Without __setitem__, the machine only dispenses; adding it lets obj[key] = value restock a specific slot too.

python
class Name:
    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

What we're doing: Implement both dunders on a small key-value wrapper, and show a missing key raising KeyError by convention.

settings.pypython
class Settings:
    def __init__(self):
        self._data = {}

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value


s = Settings()
s["theme"] = "dark"
s["retries"] = 3

print(s["theme"])
print(s["retries"])
try:
    s["missing"]
except KeyError as e:
    print("KeyError:", e)
5
__getitem__ just forwards to the underlying dict — a KeyError from self._data[key] propagates naturally.
8
__setitem__ makes s["theme"] = "dark" work — without it, that line would raise TypeError.
13
s["theme"] = "dark" is exactly sugar for calling s.__setitem__("theme", "dark").
Output
dark
3
KeyError: 'missing'

Why this works: s["theme"] = "dark" and s["theme"] both work because __setitem__/__getitem__ are defined and simply delegate to the underlying dict — the class adds no new logic here, just a controlled interface around it. s["missing"] raises KeyError not because Settings defines any special error handling, but because self._data[key] on the underlying dict itself raises KeyError for a missing key, and __getitem__ never catches it.

Defining __getitem__ but not __setitem__, then being surprised assignment fails

Wrong

python
class ReadOnlyPoint:
    def __init__(self, x, y):
        self._coords = {"x": x, "y": y}

    def __getitem__(self, key):
        return self._coords[key]
    # no __setitem__!

p = ReadOnlyPoint(1, 2)
p["x"] = 100   # TypeError!

Better

python
class Point:
    def __init__(self, x, y):
        self._coords = {"x": x, "y": y}

    def __getitem__(self, key):
        return self._coords[key]

    def __setitem__(self, key, value):
        self._coords[key] = value   # now assignment works

p = Point(1, 2)
p["x"] = 100   # works

What you see: TypeError: 'ReadOnlyPoint' object does not support item assignment

Why: __getitem__ and __setitem__ are entirely independent hooks — defining one implies nothing about the other. A class with only __getitem__ is genuinely read-only by design (which can be the intended behaviour, as the name ReadOnlyPoint suggests), and Python raises a clear TypeError rather than silently ignoring the assignment.

obj[key] and obj[key] = value are two separate hooks

obj[key]

__getitem__ — read

obj[key] = value

__setitem__ — write

both defined

obj behaves like a real mutable container

  1. obj[key] — __getitem__ — read
  2. obj[key] = value — __setitem__ — write
  3. both defined — obj behaves like a real mutable container

Read vs. write, and what a missing key should raise

Read vs. write, and what a missing key should raise
SituationBehaviour
obj[key] (read)calls __getitem__(self, key)
obj[key] = value (write)calls __setitem__(self, key, value) — requires it to be defined
Sequence-style, bad indexconvention: raise IndexError
Mapping-style, missing keyconvention: raise KeyError

Together

python
class ReadOnlyPoint:
    def __init__(self, x, y):
        self._coords = {"x": x, "y": y}

    def __getitem__(self, key):
        return self._coords[key]

p = ReadOnlyPoint(1, 2)
print(p["x"], p["y"])
try:
    p["z"]
except KeyError as e:
    print("KeyError:", e)

Remember: __getitem__ and __setitem__ are independent — a class can be read-only. Raise IndexError (sequence-style) or KeyError (mapping-style) for a bad key.

See also: container dunders · iterator protocol dunders · slicing

Advertisement

Context managers and calling

Guaranteed setup and cleanup around a with block, and making an instance callable like a function.

__enter__ and __exit__

standardintermediate

__enter__(self) runs at the top of a with block and its return value becomes the as name. __exit__(self, exc_type, exc_value, traceback) always runs when the block ends — even if it raised — and can suppress the exception by returning True.

Think of it as

A with block is a guaranteed pair of bookends, not just a shortcut for indentation — __enter__ is 'open the file/lock/connection,' __exit__ is 'close it, no matter what happened in between.' Even if the code inside throws a wrench into the works, __exit__ still runs, the same way a building's fire exits work regardless of what set off the alarm.

python
class Name:
    def __enter__(self):
        return self                       # becomes the "as" name

    def __exit__(self, exc_type, exc_value, traceback):
        # cleanup here — always runs
        return False                      # do not suppress exceptions

What we're doing: Show __exit__ running even when the with block raises an exception, and confirm the exception still propagates afterward since __exit__ returns False.

resource.pypython
class Resource:
    def __enter__(self):
        print("opening")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print(f"closing — exception was: {exc_type}")
        return False


try:
    with Resource() as r:
        print("using it")
        raise ValueError("something broke")
except ValueError as e:
    print(f"caught outside: {e}")
2
__enter__ runs first, printing "opening" and returning self as the value bound to r.
6
__exit__ still runs even though the block raised — cleanup is not skipped just because something went wrong.
8
return False (or any falsy value) lets the exception continue propagating after __exit__ finishes.
Output
opening
using it
closing — exception was: <class 'ValueError'>
caught outside: something broke

Why this works: "closing" prints even though raise ValueError(...) happened inside the with block — this is __exit__'s entire purpose: guaranteed cleanup regardless of how the block ends. exc_type is <class 'ValueError'> rather than None, because __exit__ receives full details about whatever exception is in flight. Because __exit__ returns False, the ValueError is NOT suppressed — it continues propagating past the with statement and is caught by the surrounding try/except, exactly as it would without any context manager involved.

Returning a truthy value from __exit__ by accident, silently swallowing every exception

Wrong

python
class Resource:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("cleaning up")
        return True   # OOPS — this suppresses ALL exceptions!

with Resource() as r:
    raise ValueError("this vanishes silently")

print("execution continues here, no error visible at all")

Better

python
class Resource:
    def __exit__(self, exc_type, exc_value, traceback):
        print("cleaning up")
        return False   # let exceptions propagate, unless deliberately suppressing one type

What you see: The ValueError raised inside the with block disappears entirely — the print() line after it runs normally, as if nothing went wrong.

Why: __exit__ returning any truthy value (True, a nonempty string, a nonzero number) tells Python to suppress the exception — this is occasionally intentional (a context manager that deliberately swallows a specific, expected exception type), but returning True unconditionally swallows EVERY exception, including bugs that should have been visible. The safe default is False, suppressing nothing unless there is a deliberate, narrow reason to.

__exit__ runs no matter how the block ends

__enter__

setup — runs first

with block body

may complete, or raise

__exit__

cleanup — always runs

  1. __enter__ — setup — runs first
  2. with block body — may complete, or raise
  3. __exit__ — cleanup — always runs

What __exit__ receives, and what its return value means

What __exit__ receives, and what its return value means
SituationBehaviour
Block completes normallyexc_type/exc_value/traceback are all None
Block raises an exceptionexc_type/exc_value/traceback describe it
__exit__ returns Truethe exception is suppressed — swallowed, not re-raised
__exit__ returns None/False (default)the exception propagates normally after __exit__ finishes

Together

python
class Resource:
    def __enter__(self):
        print("opening")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("closing")
        return False   # do not suppress exceptions

with Resource() as r:
    print("using it")

Remember: __exit__ always runs, even on an exception — return a truthy value to deliberately suppress it, or False (the safe default) to let it propagate normally.

See also: hash dunder · iterator protocol dunders · scope and legb

__call__

standardintermediate

__call__(self, *args, **kwargs) lets an instance be called like a function — obj(1, 2) runs obj.__call__(1, 2). Common for objects that need to remember state between calls, like a running counter.

Think of it as

A callable object is a function with a memory — a plain function starts fresh every call, but an object implementing __call__ can carry state (self.count, self.config) between calls the way a closure does, while still being invoked with the same familiar obj(args) syntax. It looks like calling a function; it is really calling a method with extra steps hidden behind the parentheses.

python
class Name:
    def __call__(self, *args, **kwargs):
        return self.do_something(*args, **kwargs)

What we're doing: Build a configurable, stateful multiplier as a callable object, and show it being used exactly like a plain function while remembering how many times it has run.

multiplier.pypython
class Multiplier:
    def __init__(self, factor):
        self.factor = factor
        self.call_count = 0

    def __call__(self, value):
        self.call_count += 1
        return value * self.factor


double = Multiplier(2)
triple = Multiplier(3)

print(double(5))
print(triple(5))
print(double(10))
print(double.call_count)
print(callable(double))
6
__call__ makes instances of Multiplier callable — double(5) is exact sugar for double.__call__(5).
7
self.call_count persists between calls, something a plain function without a closure could not do this simply.
8
The actual multiplication uses self.factor, set once at construction — each instance remembers its own configuration.
Output
10
15
20
2
True

Why this works: double(5) and triple(5) produce different results from the same call syntax because each is a separate Multiplier instance with its own self.factor — double(...) is really double.__call__(...), so the method has full access to whatever state that particular instance holds. double.call_count is 2, not 3, because only double was called twice (5, then 10) — triple's calls are tracked entirely separately on its own instance.

Defining __call__ but instantiating the class fresh on every use, defeating the purpose of holding state

Wrong

python
class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self):
        self.count += 1
        return self.count

for _ in range(3):
    tick = Counter()   # a NEW Counter every iteration!
    print(tick())       # always prints 1 — state never accumulates

Better

python
tick = Counter()      # create ONCE, outside the loop
for _ in range(3):
    print(tick())       # 1, 2, 3 — state persists across calls

What you see: Every call prints 1, never advancing, even though Counter clearly implements state-tracking logic in __call__.

Why: __call__'s entire value over a plain function is that self persists between separate calls on the SAME instance — creating a fresh Counter() on every iteration means every __call__ runs on a brand-new self.count = 0, throwing away exactly the state that made a callable object worth reaching for in the first place. The object needs to be created once, outside whatever loop or repeated-call context is going to use it.

obj(args) is sugar for obj.__call__(args)

obj(args)

looks like calling a function

__call__(self, args)

runs, with access to self

state persists

self.anything survives to the next call

  1. obj(args) — looks like calling a function
  2. __call__(self, args) — runs, with access to self
  3. state persists — self.anything survives to the next call

Plain function vs. a callable object, for the same job

Plain function vs. a callable object, for the same job
ApproachState between calls
A plain functionnone — every call starts fresh unless using a closure or global
A class with __call__self.anything persists naturally between calls
callable(obj)True whenever obj's class defines __call__
obj(args)exact sugar for obj.__call__(args)

Together

python
class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self):
        self.count += 1
        return self.count

tick = Counter()
print(tick())
print(tick())
print(callable(tick))

Remember: __call__ makes obj(args) work as sugar for obj.__call__(args) — reach for it when an object needs to hold and mutate state across separate calls.

See also: closures · function objects · static methods

Advertisement