Python: Magic Methods (__str__, __repr__, __len__, __eq__)

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.

Magic Methods Cheat Sheet

Cleanup__del__ selfDestructor rare__exit__cleanup__enter__with statementContainer Behavior__len__len obj__iter__for x in obj__getitem__obj key__contains__x in objString Representation__str__ selfHuman-readableprint obj,str obj__repr__ selfDev-readablerepr obj,REPL displayComparison__eq__ ==__le__ <=__lt__ <__gt____hash__dict keys, setsObject Creation__new__ clsAllocatememory__init__ selfInitializeattributesPython Magic Methods: Dunder Methods Grouped by Job, from Creation to Cleanup

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.

MethodTriggered ByPurpose
__init__MyClass()Initialize new object
__str__print(obj), str(obj)Human-readable string
__repr__repr(obj), REPL displayDeveloper-readable string
__len__len(obj)Object length
__eq__obj == otherEquality comparison
__lt__obj < otherLess-than comparison
__getitem__obj[key]Indexing / subscript
__setitem__obj[key] = valueAssignment by index
__contains__x in objMembership test
__iter__for x in objMake object iterable
__bool__if obj:Truthiness
__hash__hash(obj), dict keysHash 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_ordering when you want ordering. Define just __eq__ and __lt__, and let it write the other three.
  • DO return NotImplemented from 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

  1. Exercise 1: Write a Point class 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.
  2. Exercise 2: Build a Vector class that supports == via __eq__ and len() via __len__. Try sorting a list of vectors after adding __lt__ and @total_ordering.
  3. Exercise 3: Build a Matrix class 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.

Previous: Python: Abstract Classes & Interfaces (ABC Module)

Next: Python: Operator Overloading and Custom Object Behavior

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *