DSM
70 XP
30 minsIntermediate70 XP

Special (Dunder) Methods

print(order) shows <__main__.Order object at 0x7f3a2c1d>. len(order) crashes. order1 == order2 says False for identical orders. Your class WORKS, but it doesn't speak Python. Meanwhile len(df), df1 == df2, and print(df) all behave beautifully — because pandas implemented the same hooks you're about to learn.

What you'll learn

  • Write __repr__ so objects print honestly everywhere
  • Implement __eq__ for value equality (and know the is/== split)
  • Make objects sized, indexable, and iterable via container dunders
  • Recognize operator syntax as dunder dispatch (df['x'], a + b)
  • Know when NOT to define dunders — and when a dataclass does it for you

What

Special methods (dunders — double underscores) are the hooks Python's syntax calls: print → __repr__/__str__, == → __eq__, len() → __len__, in → __contains__, [] → __getitem__, + → __add__. Implement them and your objects plug into the language itself.

Why

Dunders are why Python feels consistent: ONE len() works on strings, lists, dicts, DataFrames — and can work on your classes. Debugging without __repr__ is archaeology; testing without __eq__ means assert failures you can't read. And understanding the protocol demystifies every library: df['col'] is just __getitem__.

Where it's used

Every domain class worth debugging (__repr__), every value-like class (__eq__), every collection-like class (__len__, __getitem__, __contains__), and reading ANY library source.

Where this runs in production

pandasdf['col'] IS a dunder

Bracket access on a DataFrame calls DataFrame.__getitem__; len(df) calls __len__; df == other builds a boolean frame via __eq__. The 'magic' API is protocol methods all the way down.

pathlibPaths that divide

Path('data') / 'sales.csv' works because Path defines __truediv__ — the stdlib redefining an operator to make file code readable.

pytestReadable assertion failures

assert result == expected produces useful diffs only because your objects define __eq__ and __repr__ — test quality is dunder quality.

Theory

The core ideas, in plain language.

The core idea: Python's syntax is a thin layer over method calls. len(x) calls x.__len__(); a == b calls a.__eq__(b); item in box calls box.__contains__(item); obj[key] calls obj.__getitem__(key). Built-in types implement these; your classes can too. You never call dunders directly (len(x), not x.__len__()) — you implement them so the SYNTAX works.
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point(x={self.x}, y={self.y})'

p = Point(3, 4)
print(p)          # Point(x=3, y=4)
print([p, p])     # [Point(x=3, y=4), Point(x=3, y=4)]

__repr__ is dunder #1 — implement it in every class you'll ever debug. The convention: return what a developer could paste to recreate the object. It shows up in print, f-strings, lists, error messages, and debuggers; without it, every log line is hex addresses.

Analogy: The universal wall socket

Python's syntax — len(), ==, in, [] — is a wall of standard sockets. Built-in types come with plugs already attached. Your class arrives with bare wires: the appliance works internally, but nothing connects to the wall. Each dunder you implement is attaching one standard plug: wire up __len__ and the len() socket accepts you; wire __eq__ and == flows. The genius of the socket standard: code that only knows the SOCKET (a function calling len(x)) works with every appliance ever made — including yours, the moment you attach the plug.

Key Concept
__eq__: value equality is yours to define

By default, == on your objects means IDENTITY (same object — inherited from `object`, it's `is` in disguise). Two Point(3, 4)s compare unequal until you define __eq__ saying which FIELDS make two instances 'the same'. The disciplined shape: check the type first (return NotImplemented for foreign types — letting Python try the other side or fall back cleanly), then compare the fields as a tuple. Bonus fact with consequences: objects defining __eq__ lose hashability by default — deliberately, since equal things must hash equal (the dict-key rule from Data Structures).

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

    def __repr__(self):
        return f'Point({self.x}, {self.y})'

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

print(Point(3, 4) == Point(3, 4))   # True — value equality
print(Point(3, 4) == 'hello')       # False — NotImplemented fallback

Tuple comparison does the multi-field work (the trick from Tuples). NotImplemented (a special value, not an exception!) tells Python 'I don't know this type' — it then asks other.__eq__ and ultimately falls back to identity, so mixed comparisons stay False instead of crashing.

Key Concept
The container protocol: act like a collection

Three dunders make a class feel like a list/dict: __len__ (len(box), and truthiness — empty means falsy!), __getitem__ (box[0] or box['key']), __contains__ (x in box). Implementing __getitem__ with integer indexes even makes your object ITERABLE — for loops just call [0], [1], [2]... until IndexError. This is duck typing's engine: sorted(), list(), sum(), and for care only that the right dunders exist, not what class you are.

Watch out
Don't hand-write what dataclasses generate

For value-holding classes, @dataclass writes __init__, __repr__, and __eq__ FOR you from field declarations: @dataclass class Point: x: int; y: int — done, all three correct. Hand-write dunders when you need behavior dataclasses don't generate (containers, operators, custom equality). Also: keep dunders unsurprising. A __len__ that returns anything but 'how many items', or an __eq__ that mutates, breaks the universal expectation every reader carries — the whole value of the protocol is that nobody has to check.

Visual Learning

See the concept, then explore it.

Syntax → dunder dispatch

Five everyday syntaxes and the hooks they secretly call. Click each pair.

Worked Examples

Watch it built up, one line at a time.

Very Easy__repr__: from hex to honest

The two-minute upgrade every class deserves.

Step 1 of 1

The convention: look like the constructor call. Now logs, lists, and debuggers all show real information. Without this, print(s) is <__main__.Sensor object at 0x...> — useless at 3am.

Code
01class Sensor:
02 def __init__(self, sid, unit):
03 self.sid = sid
04 self.unit = unit
05 
06 def __repr__(self):
07 return f"Sensor(sid='{self.sid}', unit='{self.unit}')"
08 
09s = Sensor('T-101', 'celsius')
10print(s)
11print([s])
Output
Sensor(sid='T-101', unit='celsius')
[Sensor(sid='T-101', unit='celsius')]
Practice Coding

Your turn — write the code.

Your task

Upgrade the Playlist from Classes & Objects into a native-feeling object: __repr__ as Playlist('<name>', tracks=<n>); __len__ (track count); __getitem__ (i-th title); __contains__ (is a title in the playlist?); __eq__ (same name AND same track list, NotImplemented for foreign types). Demo lines are provided.

Expected output
Playlist('Focus', tracks=2)
2 Deep Flow
True False
True False

Write your solution in the editor on the right, then hit Run.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

What does len(obj) actually do?

Without defining __eq__, what does == compare on your class?

Medium0/3 solved

In __eq__, why return NotImplemented (not False) for foreign types?

ScenarioA production incident log shows: 'ERROR processing <__main__.Order object at 0x7f2b431dfd60>' — repeated 400 times with different addresses. The on-call engineer can't tell which orders failed.

What's the one-method fix, and what should it return?

Class Tag: __init__(name); __repr__ as Tag('<name>'); __eq__ comparing lowercase names (Tag('SQL') == Tag('sql') is True), NotImplemented for non-Tags. Print Tag('SQL'), Tag('SQL') == Tag('sql'), and Tag('SQL') == 'sql'. Expected: Tag('SQL') True False

Normalized equality:__eq__ compares .lower() names — case-insensitive by design
Foreign types negotiate:Tag vs string returns NotImplemented → False, no crash
Hard0/2 solved

A class defines __len__ returning 0 when empty. What does `if my_obj:` do with no __bool__ defined?

Class Basket: holds (name, price) via add(); __len__; __contains__ checking NAMES; __add__ merging two baskets into a NEW one (items concatenated); __repr__ as Basket(items=<n>). Demo: a has mug(14.0) and tee(22.5); b has cap(9.0); c = a + b. Print c, len(c), 'cap' in c, 'hat' in c. Expected: Basket(items=3) 3 True False

Pure __add__:a + b returns a NEW Basket; a and b are unchanged
Name-based membership:__contains__ unpacks tuples and checks names only
Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

What are special methods, and why are they central to Python's design?

Show model answer

Special ('dunder') methods are the named hooks Python's syntax dispatches to: len(x) → x.__len__(), a == b → a.__eq__(b), obj[k] → __getitem__, x in c → __contains__, a + b → __add__, print → __str__/__repr__. Their centrality is uniformity: there's ONE len() spelled identically for strings, lists, dicts, DataFrames, and your classes — because it's protocol, not per-type methods (contrast .size(), .length, .count() proliferation in other languages). This is also duck typing's engine: generic code — for loops, sorted, sum, min — is written against dunders, so implementing the protocol plugs a brand-new type into decades of existing code with zero registration. The convention half matters as much as the mechanics: you IMPLEMENT dunders but never call them directly (len(x), not x.__len__()), and a dunder that betrays its universal meaning — a mutating __eq__, a __len__ that isn't a count — damages the very predictability the protocol exists to provide.

Which dunders do you implement by default on a domain class, and what does each buy?

Show model answer

__repr__ always — constructor-style output (Order(id='A-101', total=49.99)) that shows up in print, f-strings, container displays, debuggers, and crucially in error messages and logs; the cost of skipping it is hex-address archaeology during incidents. __eq__ on any value-like class — field-based equality (tuple comparison, isinstance guard, NotImplemented for foreign types) — which is what makes assert result == expected work in tests; without it, == is identity and equal-looking objects compare False. Then by role: container-likes get __len__ (also truthiness), __getitem__ (also iteration via the index protocol), __contains__; sortable domain objects get __lt__ (unlocks sorted/min/max, with functools.total_ordering deriving the rest); arithmetic-bearing domains (Money, Vector) get pure __add__-style operators with domain guards. Two caveats I'd volunteer: defining __eq__ removes default hashability (equal objects must hash equal — so add __hash__ or accept unhashable), and for plain value holders @dataclass generates __init__/__repr__/__eq__ correctly from field declarations — hand-writing those three is usually wasted review surface.

Explain how df['col'], len(df), and `if df_slice:` connect to the dunder protocol — and why that pandas truthiness check famously raises.

Show model answer

Each is dispatch: df['col'] calls DataFrame.__getitem__('col') on a column-oriented store (a dict-of-lists industrialized); len(df) calls __len__ returning the row count; printing calls __repr__ (that neat table IS a repr). Understanding this de-magics the API — bracket syntax on ANY library object is someone's __getitem__, so you can read the source rather than memorize behaviors. The truthiness case is the instructive one: `if df_slice:` would normally fall back to __len__ or __bool__, but pandas deliberately implements __bool__ to RAISE ('The truth value of a DataFrame is ambiguous') — because a data structure holding many boolean comparisons has no single honest truth value: did you mean .empty, .any(), or .all()? It's a protocol lesson in itself: when every default answer would mislead, the most user-respecting implementation of a dunder is a loud, specific error pointing to the precise alternatives. The same judgment applies to your own classes: implement the protocol where the semantics are unambiguous, and refuse it loudly where they aren't.

Common Mistakes to Avoid

1) Shipping classes without __repr__ — every log line becomes a hex address. 2) Forgetting __eq__ on value objects — tests compare identity and lie. 3) Returning False instead of NotImplemented for foreign types — kills the two-way negotiation. 4) Calling dunders directly (x.__len__()) — the built-ins (len(x)) are the API. 5) Operators that mutate — a + b must return NEW; surprise mutation in an operator is unforgivable. 6) Hand-writing __init__/__repr__/__eq__ on plain value holders — @dataclass generates all three. 7) Surprising semantics — __len__ that isn't a count breaks every reader's assumptions.

Ask the AI Tutor

Try these prompts in the AI Tutor panel: • 'Quiz me: which dunder does each of these ten syntaxes call?' • 'Walk me through the NotImplemented negotiation between two classes.' • 'Show me the same class hand-written vs @dataclass.' • 'Design dunders for a DateRange class with me — which fit, which don't?' • 'Interview mode: ask me why pandas raises on `if df:` and grade my answer.'

Glossary

Special/dunder method — double-underscore hook Python syntax dispatches to. __repr__ — developer-facing representation (constructor-style). __str__ — user-facing; print falls back to __repr__. __eq__ — defines ==; default is identity. NotImplemented — 'I can't compare this type; ask the other side' (a value, not an error). __len__ — powers len() and default truthiness. __getitem__ — powers obj[key] and index-based iteration. __contains__ — powers in. __add__/__lt__ — power + and < (sorted needs only __lt__). Protocol/duck typing — generic code written against dunders, not classes. @dataclass — generates __init__/__repr__/__eq__ from fields.

Recommended Resources

• Docs: the Data Model chapter of the Python reference — the complete dunder catalog (skim section 3.3). • Read: the dataclasses module docs — see exactly which dunders it writes for you. • Practice: add __repr__ and __eq__ to every class you built this module, then re-run your old code and watch prints and comparisons improve. • Next in DSM: OOP complete! Your setters raise ValueError — but what IS raising, who catches it, and how do robust programs recover? The Error Handling module begins with Understanding Errors & Exceptions.

Recap

✓ Python syntax is dunder dispatch: len → __len__, == → __eq__, [] → __getitem__, in → __contains__. ✓ __repr__ on every class — constructor-style, for logs, tests, and debuggers. ✓ __eq__ defines value equality: isinstance guard, tuple comparison, NotImplemented for strangers. ✓ __len__ + __getitem__ + __contains__ = container feel, truthiness, and free iteration. ✓ Operators stay pure and domain-honest: Money + Money returns new, guards currencies. ✓ @dataclass generates the value-object trio; hand-write only what has real behavior. Next up: Understanding Errors & Exceptions. You've been RAISING ValueError from setters and reading tracebacks all course — the Error Handling module makes you fluent: what exceptions are, how they travel, and how programs survive them.

scratchpad — preview this lesson's challenge anytime
playlist_native.pyPython
Ready
Output

Run your code to see the output here.