PYTHON

Python Dunder Methods: Syntax, Usage, and Examples

Python dunder methods are special methods with double underscores in their names, like __init__ and __str__. They let your objects work naturally with built-in Python behavior, such as printing, adding, comparing, and looping.


How to Use Dunder Methods in Python

Dunder methods live inside a class, and Python calls them automatically in specific situations. You can also call them manually, but most of the time you use the “normal” syntax and let Python do the rest.

Basic syntax

classPlaylist:
def__init__(self, name):
self.name = name

playlist = Playlist("Road Trip")

In this example, Python calls __init__ when you create a new object.

Example with a few common ones

classBook:
def__init__(self, title, pages):
self.title = title
self.pages = pages

def__str__(self):
returnf"{self.title} ({self.pages} pages)"

book = Book("Clean Code",464)
print(book)

That print(book) line triggers __str__ behind the scenes. Without __str__, you’d get a less helpful output like <__main__.Book object at 0x...>.

Dunder methods map to Python operators

A lot of operators and built-in functions rely on dunder methods.

  • len(obj) calls obj.__len__()
  • obj1 + obj2 calls obj1.__add__(obj2)
  • obj[key] calls obj.__getitem__(key)
  • obj == other calls obj.__eq__(other)

So, when you implement dunder methods, you’re basically teaching Python how your object should behave.


When to Use Dunder Methods

Dunder methods help most when you want your own classes to feel like built-in types. Here are a few common use cases.

1) When you want better string output for debugging

Printing an object should tell you something useful. Nobody wants to read memory addresses all day.

Adding __str__ and __repr__ makes your objects easier to inspect.

2) When you want custom objects to support operators

If your class represents something that has “math” behavior, like money, points, or measurements, operator support makes your code clean and readable.

Instead of writing:

total = price.add(tax)

You can write:

total = price + tax

3) When you want your objects to work with built-in functions

Built-ins like len(), iter(), sum(), sorted(), or membership checks (in) rely on dunder methods.

Adding the right methods lets your object work naturally in loops and conditions, like a list or dictionary.

4) When you want to control object creation and cleanup

Some dunder methods affect how objects are created and destroyed, like:

  • __new__ for customizing instance creation
  • __del__ for cleanup (rarely needed, but it exists)

Most projects won’t need these, but it helps to know they exist.


Examples of Dunder Methods in Python

Let’s look at practical examples that show how these methods make your classes feel “Pythonic,” without any weird hacks.

Example 1: Making an object printable with str

classEvent:
def__init__(self, title, location):
self.title = title
self.location = location

def__str__(self):
returnf"{self.title} in{self.location}"

event = Event("Meetup","Vienna")
print(event)

Output:

Meetup in Vienna

This is perfect for logs and quick prints.


Example 2: Using len so len() works

classTodoList:
def__init__(self, tasks):
self.tasks = tasks

def__len__(self):
returnlen(self.tasks)

todo = TodoList(["Reply to emails","Review PR","Write notes"])
print(len(todo))# 3

Now your object fits right into code that expects something “countable.”


Example 3: Using add for custom + behavior

Imagine a points system for a learning app.

classPoints:
def__init__(self, value):
self.value = value

def__add__(self, other):
return Points(self.value + other.value)

def__str__(self):
returnf"{self.value} points"

a = Points(50)
b = Points(25)

total = a + b
print(total)

Output:

75 points

That looks clean in code, and it still feels obvious what’s happening.


Example 4: Comparing objects with eq

classUser:
def__init__(self, username):
self.username = username

def__eq__(self, other):
returnself.username == other.username

u1 = User("mira")
u2 = User("mira")
u3 = User("leon")

print(u1 == u2)# True
print(u1 == u3)# False

Now equality checks compare the actual data you care about, not memory addresses.


Example 5: Making your object iterable with iter

If you want to loop over your object like a list, implement __iter__.

classShoppingCart:
def__init__(self, items):
self.items = items

def__iter__(self):
returniter(self.items)

cart = ShoppingCart(["bread","eggs","tea"])

for itemin cart:
print(item)

Output:

bread
eggs
tea

This also makes your object compatible with functions like list(cart) and sum(...) (depending on the items).


Example 6: Using getitem for indexing and [] access

classTeam:
def__init__(self, members):
self.members = members

def__getitem__(self, index):
returnself.members[index]

team = Team(["Amina","Jonas","Sasha"])
print(team[0])# Amina
print(team[1])# Jonas

Now your object behaves like a sequence.


Learn More About Dunder Methods

There are a lot of dunder methods, so it helps to group them by what they do instead of trying to memorize them all like flashcards.

Common “display” methods: str and repr

  • __str__ is for user-friendly output, usually for print()
  • __repr__ is for developer-friendly output, often used in debugging
classMovie:
def__init__(self, title, year):
self.title = title
self.year = year

def__str__(self):
returnf"{self.title} ({self.year})"

def__repr__(self):
returnf"Movie(title={self.title!r}, year={self.year})"

m = Movie("Spirited Away",2001)
print(m)# Spirited Away (2001)
print([m])# [Movie(title='Spirited Away', year=2001)]

That !r uses repr() formatting automatically, which is super handy for debugging.


Container behavior: len, contains, iter

Want your class to feel like a collection? These methods get you there.

  • __len__ lets len(obj) work
  • __contains__ controls value in obj
  • __iter__ makes your object loopable

Here’s a quick example using __contains__:

classAllowedCountries:
def__init__(self, countries):
self.countries =set(countries)

def__contains__(self, item):
return item.lower()inself.countries

allowed = AllowedCountries(["montenegro","croatia","austria"])
print("croatia"in allowed)# True
print("france"in allowed)# False


Math and operators: add, sub, mul

Operator methods let you write simple code that reads nicely.

classHours:
def__init__(self, value):
self.value = value

def__mul__(self, other):
return Hours(self.value * other)

def__str__(self):
returnf"{self.value}h"

work = Hours(2)
print(work *3)# 6h

You can also implement “reverse” operator methods like __radd__ so that 10 + obj works too, not just obj + 10.


Attribute access: getattr and setattr

These methods let you control what happens when someone reads or sets attributes. They can be powerful, but they’re also easy to mess up if you don’t keep the logic simple.

A small example:

classConfig:
def__init__(self, values):
self.values = values

def__getattr__(self, name):
returnself.values.get(name)

cfg = Config({"theme":"dark","language":"en"})
print(cfg.theme)# dark
print(cfg.language)# en
print(cfg.missing)# None

Python calls __getattr__ only if the attribute doesn’t exist normally, which makes it safer than overriding everything.


Context managers: enter and exit

Context managers power the with statement. Files use this pattern:

withopen("notes.txt")as f:
    text = f.read()

You can build your own too, like a timer:

import time

classTimer:
def__enter__(self):
self.start = time.time()
returnself

def__exit__(self, exc_type, exc_value, traceback):
        end = time.time()
print(f"Time: {end - self.start:.3f}s")

with Timer():
    total =sum(range(1_000_000))

This pattern is great for resource cleanup and for measuring performance without cluttering your code.


A quick note on init vs new

  • __new__ creates the instance
  • __init__ initializes it

Most classes only need __init__. __new__ comes up when you subclass immutable types (like str or tuple) or when you need custom instance creation logic.


Common mistakes to avoid

1) Returning the wrong type

If __add__ returns a raw number sometimes and your class other times, your object becomes unpredictable. Try to return consistent types.

2) Forgetting to handle unsupported comparisons

Comparing to unrelated types can cause confusing bugs. Returning NotImplemented is often the right move.

Example:

def__eq__(self, other):
ifnotisinstance(other, User):
returnNotImplemented
returnself.username == other.username

3) Going wild with magic

Dunder methods are powerful, but overusing them can make your code hard to follow. If a class behaves in surprising ways, someone will eventually call it “cursed” in a code review.


Summary

Dunder methods are special class methods that let your objects plug into Python’s built-in behavior, like printing, comparing, looping, indexing, and using operators. Add them when you want your custom classes to behave more like standard Python types, and keep the behavior clear so your code stays easy to read and maintain.