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
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
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.
Path('data') / 'sales.csv' works because Path defines __truediv__ — the stdlib redefining an operator to make file code readable.
assert result == expected produces useful diffs only because your objects define __eq__ and __repr__ — test quality is dunder quality.
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.
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.
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 fallbackTuple 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.
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.
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.
Five everyday syntaxes and the hooks they secretly call. Click each pair.
The two-minute upgrade every class deserves.
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.
Sensor(sid='T-101', unit='celsius') [Sensor(sid='T-101', unit='celsius')]
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.
Playlist('Focus', tracks=2)
2 Deep Flow
True False
True FalseWrite your solution in the editor on the right, then hit Run.
What does len(obj) actually do?
Without defining __eq__, what does == compare on your class?
In __eq__, why return NotImplemented (not False) for foreign types?
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
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
What are special methods, and why are they central to Python's design?
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?
__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.
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.
Run your code to see the output here.