Skip to contentSkip to navigation
MODULE 09 · LESSON 06

Special Dunder Methods

Implement Pythonic operator overloading, developer string representations with __repr__, and length checks.

Core Learning Milestones:
__repr__ vs __str__
Developer vs user-facing
__len__ Contract
Non-negative int, or error
Operator Overloading
__add__, __eq__, and friends
NotImplemented
Never raise TypeError directly
Estimated Time
30 Minutes
Comprehensive deep dive
Difficulty Level
Advanced
Builds on Inheritance
Knowledge Check
15 Questions
+35 XP Potential
01 / Foundations

Python's Data Model: A Secret Handshake With Built-ins

You never call .size() or .toString() on a Python object. Instead, len(obj), str(obj) and obj + other all silently dispatch to double-underscore "dunder" methods defined on the class: type(obj).__len__(obj), type(obj).__str__(obj), type(obj).__add__(obj, other).

Implement these protocols correctly and your custom class behaves indistinguishably from a built-in list, dict, or int.

Raising TypeError from __eq__
def __eq__(self, other):
raise TypeError(...) # crashes on 'x == 5'
Returning NotImplemented
def __eq__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
Think of it like this:

Dunder methods are like standardized electrical outlets — Python's built-in functions are the appliances, and every object that implements the right "plug shape" (__len__, __add__) just works, no matter who built it.

Full Contract in Action

Hooking into Python Protocols

Run the snippet below to see how str(), repr(), and list display each call a different dunder method:

dunder_protocols.py — Press 'Run Code' to execute in your browser
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = round(float(amount), 2)
        self.currency = currency

    # Unambiguous developer representation
    def __repr__(self):
        return f"Money({self.amount}, '{self.currency}')"

    # Human-friendly string formatting
    def __str__(self):
        return f"${self.amount:.2f} {self.currency}"

m = Money(49.95)
print("str(m):", str(m))
print("repr(m):", repr(m))
print("Interactive REPL output:", [m])  # Lists use repr() for items!
Dunder Methods Glossary: Core Concepts
__repr__Developer

Unambiguous debug representation; always implement this first since str() falls back to it.

__str__End-user

Clean, readable text for print() and str() — the polished face of your object.

__len__Sizing

Must return a non-negative int; also powers automatic truthiness via len(obj) == 0.

NotImplementedSingleton

The safe return value from a binary operator method when the other operand's type is unsupported.

02 / Protocol Dispatch Explorer

See What Each Built-in Actually Calls

Click through repr, str, and operator dispatch to see the exact method Python invokes:

repr(obj) -> __repr__
def __repr__(self): return f"Money({self.amount}, {self.currency!r})"

Written for developers: unambiguous, and ideally valid Python that could recreate the object. Implement this one first — str() falls back to it.

03 / Dunder Protocol Reference

Representation, Sizing, and Operator Protocols

These three families cover nearly every dunder method you'll write in application code.

String representation

__repr__(self)

Developer-facing, unambiguous. Used by REPL, debuggers, and containers displaying items (e.g. inside a list).

__str__(self)

User-facing, readable. Used by str() and print(); falls back to __repr__ if not defined.

Sizing & truthiness

__len__(self) -> int

Must return a non-negative integer. len() calls this directly; a negative value raises ValueError, a non-integer raises TypeError.

__bool__(self) -> bool

Controls truthiness directly. If absent, Python falls back to __len__() == 0 meaning False.

Operator overloading

__add__(self, other)

Handles a + b. Return NotImplemented for incompatible types, never raise TypeError directly.

__eq__(self, other)

Handles a == b. Same NotImplemented rule applies so Python can try the other operand's __eq__.

04 / Hands-on Examples

Worked Hands-on Walkthroughs

Progress step-by-step from basic string protocols through fallback behavior to full operator overloading:

Walkthrough 01 · FoundationLevel 1 · Foundation

Hooking into Python Protocols

Representation
Problem Scenario

print and str() invoke __str__, while container displays and interactive shells invoke __repr__.

Executable Solution
Hooking into Python Protocols
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount = round(float(amount), 2)
        self.currency = currency

    # Unambiguous developer representation
    def __repr__(self):
        return f"Money({self.amount}, '{self.currency}')"

    # Human-friendly string formatting
    def __str__(self):
        return f"${self.amount:.2f} {self.currency}"

m = Money(49.95)
print("str(m):", str(m))
print("repr(m):", repr(m))
print("Interactive REPL output:", [m])  # Lists use repr() for items!
Step-by-Step Explanation

print and str() invoke __str__, while container displays and interactive shells invoke __repr__.

Walkthrough 02 · Building UpLevel 2 · Applied

Demonstrating Fallback Behavior and Unambiguous Output

Fallback
Problem Scenario

Because __str__ was omitted, Python seamlessly routed str(node) to __repr__.

Executable Solution
Demonstrating Fallback Behavior and Unambiguous Output
class ServerNode:
    def __init__(self, hostname, ip):
        self.hostname = hostname
        self.ip = ip

    def __repr__(self):
        return f"ServerNode(hostname='{self.hostname}', ip='{self.ip}')"

node = ServerNode("web-01", "10.0.0.1")

# Fallback: __str__ is not defined, so str() uses __repr__()
print("str() fallback:", str(node))
print("repr():", repr(node))
print("Are str and repr identical here?", str(node) == repr(node))
Step-by-Step Explanation

Because __str__ was omitted, Python seamlessly routed str(node) to __repr__.

Walkthrough 03 · MasteryLevel 3 · Architecture

Arithmetic Overloading with Rich Comparison and NotImplemented

Operators
Problem Scenario

Returning NotImplemented in __eq__ allows Python to compare against incompatible types gracefully, returning False instead of crashing.

Executable Solution
Arithmetic Overloading with Rich Comparison and NotImplemented
class Vector2D:
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

    def __add__(self, other):
        if not isinstance(other, Vector2D):
            return NotImplemented  # Let Python try other.__radd__ or raise TypeError
        return Vector2D(self.x + other.x, self.y + other.y)

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

v1 = Vector2D(2, 5)
v2 = Vector2D(3, 1)

print("v1 + v2:", v1 + v2)
print("v1 == Vector2D(2, 5):", v1 == Vector2D(2, 5))
print("v1 == 'invalid':", v1 == "invalid")  # Returns False gracefully!
Step-by-Step Explanation

Returning NotImplemented in __eq__ allows Python to compare against incompatible types gracefully, returning False instead of crashing.

05 / Traps to Avoid

Common Traps & How to Avoid Them

Study the mistakes and clean fixes below before writing your own dunder methods:

!Trap 1: Syntax or Name Typos in Dunder Methods & Magic Protocols
Common Trap
The Bug (Avoid)Buggy Code
# Mistyped variable or missing closing parenthesis result = dunder_methods___magic_protocols_val
What Happens:
NameError: name is not defined
The Clean FixCorrected Code
# Declare and verify variables before execution val = 10 print("Defined correctly:", val)
Recommended Practice:
Verify identifiers, avoid mutable defaults, and always bound loops that acquire resources.
Why It Happens

Beginners frequently encounter typos or forgotten definitions before using an identifier.

!Trap 2: Unintended State Mutation or Scope Leakage
Subtle Trap
The Bug (Avoid)Buggy Code
# Modifying shared state or unexpected variable shadowing def update_state(data=[]): data.append(1) return data
What Happens:
Default mutable argument accumulates values across separate function calls.
The Clean FixCorrected Code
def update_state(data=None): if data is None: data = [] data.append(1) return data
Recommended Practice:
Verify identifiers, avoid mutable defaults, and always bound loops that acquire resources.
Why It Happens

Default arguments in Python are evaluated once at function definition time, not at invocation time.

!Trap 3: Resource Leaks & High-Scale Performance Regressions
Architecture Trap
The Bug (Avoid)Buggy Code
# Opening resources in a tight loop without closing or bounding memory files = [open(f"log_{i}.txt", "w") for i in range(5000)]
What Happens:
OSError: [Errno 24] Too many open files (file descriptor exhaustion)
The Clean FixCorrected Code
for i in range(5): with open(f"log_{i}.txt", "w") as f: f.write("Safe")
Recommended Practice:
Verify identifiers, avoid mutable defaults, and always bound loops that acquire resources.
Why It Happens

Failing to release system resources leads to process termination under heavy production workloads.

06 / Real-World Systems

Real-World Systems in Action

See how scripts, cloud microservices, and tensor libraries all lean on Python's Data Model:

Developer ProductivityFoundation Script

Local Developer Automation Tool

SYSTEM #1
Organization Context

Individual developers and IT administrators automate everyday repetitive workflows.

Why This Pattern Works Here

A clean standalone script providing fast, local automation with zero dependencies.

Production Code Wrapper
Local Developer Automation Tool
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages
    def __len__(self):
        return self.pages

book = Book("Fluent Python", 800)
print("Length of book:", len(book))
Cloud MicroservicesScaled Service · Applied

Backend API Service Integration

SYSTEM #2
Organization Context

SaaS platforms run backend microservices processing requests concurrently.

Why This Pattern Works Here

A modular service handler integrating validation and structured responses.

Production Code Wrapper
Backend API Service Integration
# Service integration layer
def handle_request(payload: dict) -> dict:
    return {"status": "success", "processed": True}
Distributed SystemsEnterprise · Architecture

High-Throughput Cloud Architecture

SYSTEM #3
Organization Context

Data science frameworks like PyTorch and NumPy overload arithmetic dunder methods (__add__, __matmul__) to enable tensor math.

Why This Pattern Works Here

Data science frameworks like PyTorch and NumPy overload arithmetic dunder methods (__add__, __matmul__) to enable tensor math.

Production Code Wrapper
High-Throughput Cloud Architecture
# High-resilience production pattern
import time

def process_stream(events: list) -> int:
    processed = 0
    for event in events:
        if event:
            processed += 1
    return processed
Key Takeaways

Key Points & Concept Review

  • Dunder methods hook your classes into Python's native syntactic operators and built-in functions.
  • Always implement __repr__ first: it serves as the foundation for debugging and the fallback for str().
  • __str__ provides human-friendly formatting for print(), while __repr__ provides unambiguous technical representation.
  • __len__ must return a non-negative integer and serves as the automatic fallback for truth value testing.
  • Return NotImplemented (never raise TypeError) from binary operators when operand types are incompatible.
  • Classes implementing data model protocols act indistinguishably from built-in Python primitives.
Study Guide & References

Frequently Asked Questions & Reference Hub

FAQ & References
Growth Mindset: The Data Model Rewards Pattern Recognition

Once __repr__ vs __str__ clicks, every other dunder method — __add__, __eq__, __getitem__ — follows the exact same "the operator dispatches to a method" pattern. You're not memorizing dozens of rules, just one idea applied dozens of times.

Related Built-in References Right Here
PyLabs Library Docs
Ready for Action

You've Mastered Python's Data Model!

You understand string representation protocols, sizing, and safe operator overloading. Now lock in your knowledge with hands-on practice.

Hands-on Code Challenge

Implement __add__ and __repr__ on a custom Point class.

+40 XP Reward

Earn experience points toward your developer rank as soon as you pass.

~3 Minutes to Complete

Bite-sized knowledge check with instant explanations for every question.

Worked example

Provide custom string representations for user display.

Example
class Coin:
    def __str__(self): return "Gold Coin"
print(str(Coin()))

How it works, step by step

  1. __str__ provides human-friendly output for str() and print().

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

Special Dunder Methods practice

Sign-in required

Create a 2D Point class with __init__(self, x, y). Implement __add__(self, other) so that adding two points returns a new Point(self.x + other.x, self.y + other.y), and implement __repr__(self) returning f"Point({self.x}, {self.y})". Add p1 = Point(1, 2) and p2 = Point(3, 4), and print: Point(4, 6).

Coding Practice is Locked

Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.

Practice quiz

Check yourself

15 questions with explanations after submission, so you can correct course as you go.

Special Dunder Methods quiz

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/6 complete
Module cheat sheet