Python magic methods (also called dunder methods) like __str__, __repr__, __len__, __eq__, and __getitem__ are the hooks that let your own classes plug into built-in functions such as print(), len(), and the comparison operators. This reference walks through each one with tested examples.
“In Python, the special method names allow your objects to implement, support, and interact with basic language constructs.”
Luciano Ramalho, Fluent Python
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 13 minutes
When you call len(my_list), Python quietly calls my_list.__len__() for you. When you write print(obj), Python calls obj.__str__(). These special methods are called magic methods, or dunder methods, because they are wrapped in double underscores (“double under” shortened to “dunder”). They are the hooks that let your own classes behave like the built-in types you already know. Define the right ones and your objects work with for loops, print(), len(), the comparison operators, and more.
Think of magic methods like the power sockets in your house. The wall socket is a standard shape, so any appliance with a matching plug just works. You do not rewire the wall for each new device. Python defines the standard “sockets” (call len(), use in, loop with for), and your job is to give your class the matching “plug” (__len__, __contains__, __iter__). So instead of inventing method names like get_length() or to_string(), you implement __len__ and __str__, and your object snaps right into Python’s own syntax. This post is a quick reference: every method below is run on Python 3.14.6 and the output is the real thing.
Table of Contents
Magic Methods Cheat Sheet
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram groups Python’s magic methods by the job they do: creation (__init__, __new__), string representation (__str__, __repr__), comparison (__eq__, __lt__), container behavior (__len__, __getitem__), and cleanup (__del__). Each group is a different way your objects hook into Python’s built-in operations. The table below is the quick lookup: find the method, see what triggers it, and read what it is for.
| Method | Triggered By | Purpose |
|---|---|---|
__init__ | MyClass() | Initialize new object |
__str__ | print(obj), str(obj) | Human-readable string |
__repr__ | repr(obj), REPL display | Developer-readable string |
__len__ | len(obj) | Object length |
__eq__ | obj == other | Equality comparison |
__lt__ | obj < other | Less-than comparison |
__getitem__ | obj[key] | Indexing / subscript |
__setitem__ | obj[key] = value | Assignment by index |
__contains__ | x in obj | Membership test |
__iter__ | for x in obj | Make object iterable |
__bool__ | if obj: | Truthiness |
__hash__ | hash(obj), dict keys | Hash value |
__str__ vs __repr__: The Two String Methods
Start with the two you will use the most: __str__ and __repr__. Both return a string for your object, but they answer different questions. __str__ answers “how do I show this to a user?” and __repr__ answers “how do I show this to a developer who is debugging?” Think of how you introduce yourself: at a party you just say “I’m Aditi”, but on a passport form you write your full legal name, exactly as registered. __str__ is the party introduction, __repr__ is the passport entry. Here is one class that defines both so you can see the split.
📄 str_vs_repr.py: human-readable vs developer-readable
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name} - Rs.{self.price}"
def __repr__(self):
return f"Product('{self.name}', {self.price})"
p = Product("Python Book", 699)
# __str__ is for users (print, str(), f-strings)
print(p) # Uses __str__
print(f"Item: {p}") # Uses __str__
# __repr__ is for developers (REPL, debugging, containers)
print(repr(p)) # Uses __repr__
print([p]) # Lists use __repr__ for elements
▶ Output
Python Book - Rs.699
Item: Python Book - Rs.699
Product('Python Book', 699)
[Product('Python Book', 699)]
What happened here: print(p) and the f-string both picked __str__, so the user sees the friendly “Python Book – Rs.699”. But repr(p) and putting p inside a list both picked __repr__, so a developer sees “Product(‘Python Book’, 699)”. That is the rule of thumb: __repr__ should look like the Python code you would type to recreate the object. One tip that saves a lot of frustration: if you only write one of the two, write __repr__. Python falls back to it when __str__ is missing, but never the other way around.
Comparison Methods: __eq__, __lt__, __le__
Want ==, <, sorted(), and max() to work on your objects? Teach them how to compare. It works like a school merit list: once the rule is written down (higher GPA ranks first), any teacher can sort the whole class without asking you. __eq__ and __lt__ are you writing that rule down for Python. And you do not have to write all six comparison methods by hand. Define equality and one ordering, then let a decorator fill in the rest. Below, four students named Rahul, Anvi, Prathamesh, and Niranjan get compared and sorted by GPA.
📄 comparison.py: making objects comparable
from functools import total_ordering
@total_ordering # Generates __le__, __gt__, __ge__ from __eq__ + __lt__
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __eq__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.gpa == other.gpa
def __lt__(self, other):
if not isinstance(other, Student):
return NotImplemented
return self.gpa < other.gpa
def __repr__(self):
return f"Student('{self.name}', {self.gpa})"
students = [
Student("Rahul", 3.8),
Student("Anvi", 3.5),
Student("Prathamesh", 3.9),
Student("Niranjan", 3.8),
]
print(f"Rahul == Niranjan? {students[0] == students[3]}")
print(f"Anvi < Rahul? {students[1] < students[0]}")
print(f"Sorted: {sorted(students)}")
print(f"Top student: {max(students)}")
▶ Output
Rahul == Niranjan? True
Anvi < Rahul? True
Sorted: [Student('Anvi', 3.5), Student('Rahul', 3.8), Student('Niranjan', 3.8), Student('Prathamesh', 3.9)]
Top student: Student('Prathamesh', 3.9)
What happened here: __eq__ makes == work and __lt__ makes < work. The @total_ordering decorator looks at those two and writes <=, >, and >= for you, so sorted() and max() understand our Student class without any extra code. One detail people miss: when the other object is not a Student, we return NotImplemented (a value), we do not raise it. That return is a polite “I do not know how to compare these, you try”, and Python then asks the other object. Raise an error instead and you slam that door shut.
Container Methods: __len__, __getitem__, __contains__
This group is where Python magic methods really pay off. Implement a handful of them and your class starts acting like a built-in list: len() counts it, square brackets index into it, in checks membership, and for loops over it. Anyone using your class never has to learn a new Application Programming Interface (API), because they already know how Python sequences behave.
📄 container.py: making objects behave like containers
class Playlist:
def __init__(self, name):
self.name = name
self._songs = []
def add(self, song):
self._songs.append(song)
return self
def __len__(self):
return len(self._songs)
def __getitem__(self, index):
return self._songs[index]
def __contains__(self, song):
return song in self._songs
def __iter__(self):
return iter(self._songs)
def __repr__(self):
return f"Playlist('{self.name}', {len(self)} songs)"
playlist = Playlist("Coding Jams")
playlist.add("Lo-Fi Beat #1").add("Synthwave Night").add("Chill Hop Mix")
print(f"Songs: {len(playlist)}")
print(f"First: {playlist[0]}")
print(f"Last: {playlist[-1]}")
print(f"Has 'Synthwave Night'? {'Synthwave Night' in playlist}")
print("All songs:")
for song in playlist:
print(f" - {song}")
▶ Output
Songs: 3 First: Lo-Fi Beat #1 Last: Chill Hop Mix Has 'Synthwave Night'? True All songs: - Lo-Fi Beat #1 - Synthwave Night - Chill Hop Mix
What happened here: Four methods did a lot of work. __len__ let len(playlist) report 3. __getitem__ made playlist[0] and even playlist[-1] work, because it just forwards the index straight to the inner list. __contains__ powered the in check, and __iter__ let the for loop walk through every song. Think of it like a music app: you do not care how the playlist stores songs inside, you just tap next, scroll, and search. Your Playlist class gives Python the same familiar controls, so it feels like a built-in sequence even though you wrote it yourself.
__bool__ and __hash__
Two more methods worth knowing. __bool__ decides what your object means in a yes-or-no context, like if obj:. It is like the lights in a shop window: one glance from the street tells you whether the shop is open, no need to walk in and count the shelves. __hash__ decides whether your object can be a dictionary key or live in a set. The example below shows __bool__ in action; we cover the __hash__ catch right after it in Common Mistakes.
📄 bool_hash.py: truthiness with __bool__
class Inventory:
def __init__(self):
self.items = {}
def add(self, item, quantity):
self.items[item] = self.items.get(item, 0) + quantity
def __bool__(self):
return len(self.items) > 0
def __len__(self):
return sum(self.items.values())
inv = Inventory()
print(f"Empty inventory is truthy? {bool(inv)}")
inv.add("Laptop", 5)
inv.add("Mouse", 20)
print(f"Stocked inventory is truthy? {bool(inv)}")
print(f"Total items: {len(inv)}")
if inv:
print("Inventory has stock!")
▶ Output
Empty inventory is truthy? False Stocked inventory is truthy? True Total items: 25 Inventory has stock!
What happened here: An empty Inventory came back False from bool(inv), and once we added stock it came back True. That is __bool__ at work: it lets if inv: read like plain English (“if the inventory has anything in it”). One thing to know even if you never write __bool__: when you define __len__, Python already uses it for truthiness, treating length 0 as falsy. So if your object has a sensible length, you often get truthiness for free and only add __bool__ when you want different logic.
Common Mistakes
Mistake 1: Writing __str__ but skipping __repr__
If you are only going to write one of the two, write __repr__. Python uses it as a fallback when __str__ is missing, but it will not run the other way around. Lists and dicts also use __repr__ for the items inside them, so an object with only __str__ still prints the ugly default <__main__.Thing object at 0x...> the moment you drop it into a list.
Mistake 2: Adding __eq__ but forgetting __hash__
The moment you define __eq__, Python quietly sets __hash__ to None, and your objects can no longer be dictionary keys or set members. It is not being mean. The rule is that two objects that are equal must share the same hash, and Python cannot guess your new hash logic, so it disables the broken default. If you need hashing, write __hash__ yourself (a common choice is to hash a tuple of the same fields you compare in __eq__).
Mistake 3: Raising TypeError instead of returning NotImplemented
Inside a comparison method, return NotImplemented (the built-in value, not the exception) when the other object is a type you do not understand. That return tells Python to go ask the other object instead. If you raise TypeError yourself, you cut off that second chance, and comparisons that should have worked through the other operand suddenly fail.
Best Practices
- DO add
__repr__to every class. It is the single most useful magic method, and future-you debugging at 2am will thank you. - DO reach for
@total_orderingwhen you want ordering. Define just__eq__and__lt__, and let it write the other three. - DO return
NotImplementedfrom comparison methods when the other type is not yours. - DON’T bolt on magic methods that make no sense for your type. A User is not a container, so do not give it
__len__just because you can. - DON’T add
__eq__without deciding what happens to__hash__.
Conclusion
Python magic methods are the standard plugs that let your classes snap into the language’s built-in sockets. __str__ and __repr__ control how an object prints, __eq__ and __lt__ let it be compared and sorted, and __len__ and __getitem__ let it work with len() and square brackets. Pick the ones that genuinely fit your type, skip the ones that do not, and your custom class starts to feel like it shipped with Python.
So far the methods just answered questions: how long are you, are you equal, are you truthy. Next you get to make your objects do arithmetic. In the operator overloading tutorial, you will overload +, -, and * so expressions like vector_a + vector_b mean exactly what you want. And if you want to browse every topic in one place, head to the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Write a
Pointclass with both__str__(something friendly like “(3, 4)”) and__repr__(something like “Point(3, 4)”). Print it, then put it in a list and notice which one Python uses. - Exercise 2: Build a
Vectorclass that supports==via__eq__andlen()via__len__. Try sorting a list of vectors after adding__lt__and@total_ordering. - Exercise 3: Build a
Matrixclass that supports[]indexing via__getitem__, iteration via__iter__, and clean printing via__repr__.
Frequently Asked Questions
What are magic methods in Python?
Python magic methods (also called dunder methods) are special methods wrapped in double underscores, like __init__, __str__, and __len__. Python’s built-in operations call them for you: len(obj) calls __len__ and print(obj) calls __str__. They let your own objects behave like built-in types.
What is the difference between __str__ and __repr__?
__str__ is for end users (human-readable). __repr__ is for developers (unambiguous, ideally valid Python that recreates the object). print() uses __str__, the REPL uses __repr__. If you only define one, define __repr__, because Python falls back to it when __str__ is missing.
What does __getitem__ do in Python?
__getitem__ enables indexing: obj[key] calls obj.__getitem__(key). Implement it to make your objects subscriptable. It also enables iteration when __iter__ is not defined, because Python then calls __getitem__ with 0, 1, 2 and so on until it hits IndexError.
What is @total_ordering in Python?
@total_ordering (from functools) auto-generates missing comparison methods. Define just __eq__ and one of __lt__/__gt__/__le__/__ge__, and the decorator fills in the rest. It saves you from writing 4 nearly-identical methods.
Why does implementing __eq__ break hashing?
Python requires that objects which are equal (a == b) must have the same hash (hash(a) == hash(b)). Since a custom __eq__ changes equality rules, Python sets __hash__ = None to prevent broken hash behavior. Implement __hash__ explicitly if you need hashability.
What is the difference between NotImplemented and NotImplementedError?
NotImplemented is a singleton value you return from a comparison method to signal ‘I cannot handle this type, so try the other operand.’ NotImplementedError is an exception you raise inside abstract methods. Never raise NotImplemented and never return NotImplementedError. They are completely different things.
Interview Questions on Python Magic Methods
Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.
Q: You added __str__ to your Order class, but when you print a list of orders you still see <__main__.Order object at 0x7f2a…>. Why, and what do you fix?
Containers like lists and dicts use __repr__ for the elements inside them, not __str__. Since Order defines only __str__, the list falls back to the default object repr with the memory address. The fix is to define __repr__. As a general habit, write __repr__ first, because Python uses it as the fallback for __str__ but never the reverse.
Q: Your team stores custom objects as dictionary keys. After someone mutates an attribute on one of those objects, lookups for it start failing intermittently. What is going on?
The class hashes on a mutable field. A dict computes the hash when the key is inserted and uses it to pick a bucket. If the object is mutated afterwards, hash(obj) now returns a different value, so lookups search the wrong bucket and miss the entry even though the key is “in” the dict. The rule: only hash on fields that never change, or make the object effectively immutable (frozen dataclasses do exactly this).
Q: When Python evaluates a == b and a and b are instances of two different classes, what exactly happens under the hood?
Python first calls a.__eq__(b). If that returns NotImplemented, Python tries the reflected call b.__eq__(a). If both return NotImplemented, Python falls back to identity comparison (a is b) instead of raising an error. One special case: if type(b) is a subclass of type(a), Python asks b first, so the more specific class gets priority.
Q: A class defines __iter__ but not __contains__. Does “x in obj” still work, and what is the performance cost?
Yes, it works. When __contains__ is missing, Python falls back to iterating the object with __iter__ and comparing each element with == until it finds a match. That fallback is O(n) every time. If your class wraps something with fast membership checks, like a set or a dict, implement __contains__ to delegate to it and the check drops to O(1).
Q: Your HTTP client wraps responses in a custom Response class, and “if response:” skips the success branch even though the request returned 200. What do you check first?
Check how the class computes truthiness. Python first looks for __bool__, then falls back to __len__ (length 0 means falsy), and only defaults to True if neither exists. A Response with a __len__ that returns 0 for an empty body, or a __bool__ keyed to the wrong condition, will be falsy despite a 200 status. Fix it by defining __bool__ on the actual success condition, such as 200 <= self.status < 300.
Q: The convention says __repr__ should return valid Python that recreates the object. What do you do when that is impractical, like for a database connection?
Use the angle-bracket convention: return something like <DBConnection host='db1' state='open'>. The angle brackets deliberately make it invalid Python, which signals to readers that the object cannot be recreated from the string. The real goal of __repr__ is to be unambiguous and useful in logs and debuggers, so include the fields that identify the object’s state, not every attribute.
Want more? the official Python documentation documents everything this post could not fit.
Related Posts
Previous: Python: Abstract Classes & Interfaces (ABC Module)
Next: Python: Operator Overloading and Custom Object Behavior
Series Home: Python + AI/ML Tutorial Series

No comment