So you’ve been learning Python and you’ve probably noticed some functions have double underscores, while others don’t:
Why is that?
What do they mean?
Can you change them?
Should you?
The reality is that these functions control much more of Python than most people realize. Use them correctly and your code feels clean and natural. But use them wrong, and things break in confusing ways.
That’s why in this guide, I’ll break down what these “magic” dunder methods are, how they work, and how to use them.
Sidenote: If you find any of this confusing, or simply want a deep dive into Python, check out Andrei's Python Coding course taken by 200,000+ people:
It’ll take you from an absolute beginner to understanding everything you need to be hired ASAP.
Alternatively, if you're already pretty good at Python and want to build some interesting and useful projects, why not check out my course on Python Automation:
It'll show you how to automate all of the boring or repetitive tasks in your life - and makes for some pretty standout portfolio projects as well!
With all that out of the way, let's get into this guide.
To be clear, dunder is just a shorter way of saying ‘double-underscore'. (And not to be confused with ‘Down under’ and kangaroos!).
Basically, any time you see something in Python with two underscores on each side like __init__, __len__, or __str__, this is what people mean when they say “dunder method.”
So what do these double underscores mean?
The double underscores are there to mark the methods whose names they surround as special, because these aren’t regular methods you write yourself.
In fact, they’re the code behind the code that makes Python work…
For example
When you write print("Hello world"), Python calls the string’s __str__ method to figure out what to show. Or when you add numbers with 5 + 10, it’s really the __add__ method doing its job.
So why should we care?
Well if we didn’t have dunder methods, we would need to manually write specific code each time for all of this.
Want to add two numbers?
Well, you’d have to spell it out every single time:
def add_numbers(a, b):
return a + b
print(add_numbers(5, 10)) # 15
Want to print text? Without __str__, Python wouldn’t know how to turn "Hello world" into something it could display:
def stringify(text):
return text
print(stringify("Hello world")) # Hello world
In the past, with some older programming languages, you would have to write these functions each time. And as you might imagine, that can get annoying pretty quickly!
That’s why some people call these dunder methods “magic methods” because everything just works ‘like magic’, and you don’t have to reinvent the basics or account for all those extra steps.
Instead, Python bakes all of this in for you. You just write your code the way you’d expect, and these dunder methods handle the heavy lifting in the background.
Now that you know what dunder methods are, let’s talk about the kinds of things they actually control.
The short version? Pretty much everything you already do in Python has a dunder method working behind the scenes.
Most of them fall into a few main groups:
Object setup methods control how something comes to life in Python when you create or copy objects
Representation methods decide how your object shows up when you print it, log it, or inspect it in a shell
Comparison methods handle equality - greater than, less than, and so on
Container and iteration methods make your objects behave like lists or dictionaries so you can loop over them or grab items by index
Arithmetic and operator methods cover the math side of things: adding, multiplying, dividing, etc
Special behavior methods give objects extra tricks, like making them callable as if they were functions or handling setup and cleanup when used in a with block
What most people don’t realize is that these methods aren’t just for Python’s built-in types, but you can use them in your own code too.
This can make your objects feel more natural to work with, as well as make them capable of things such as printing cleanly, comparing properly, looping neatly, or even acting like functions when you need them to.
Pretty cool, right?
So let’s go through these one by one, so you can see what they are, what they do, and how they work.
When you create something in Python, it doesn’t just pop into existence fully formed. Python runs special dunder methods behind the scenes that handle the setup. These are what bring your objects to life.
The two you’ll see most often are __new__ and __init__.
__new__ is the method that actually creates an object in memory. Think of it as Python carving out a spot for the new object. You usually don’t need to worry about this one, because Python employs it automatically unless you’re doing something more advanced.
__init__ is the one dunder method you’ve almost certainly seen already. This runs immediately after __new__, and its job is to initialize the object with whatever details you care about.
For example
Imagine you’re writing a program to track pets at a shelter.
Each dog needs to have a name when it's added to the shelter files, otherwise the program wouldn’t know how to refer to them.
That’s where __init__ comes in:
class Dog:
def __init__(self, name):
self.name = name
fido = Dog("Fido")
rex = Dog("Rex")
luna = Dog("Luna")
print(fido.name) # Fido
print(rex.name) # Rex
print(luna.name) # Luna
Python calls Dog.__new__ first to create the empty object, then it immediately calls Dog.__init__, which fills in the name you passed in.
Tl;DR
__new__ builds the object, and __init__ customizes it with whatever details matter for your program. Most of the time you’ll only ever write __init__, but it helps to know __new__ is always working just before it.
Whenever you print something in Python, you expect it to show up in a way that makes sense; i.e., if you print a string, then you expect to see the actual text. And if on the other hand you print a list, you expect to see the items inside square brackets.
The two main dunder methods that deal with this are __str__ and __repr__.
__str__ is what Python calls when you use print() on an object. Its job is to give you a nice, readable version of that object. Think of it as the “friendly” display.
__repr__ is what Python falls back on when you’re just inspecting something in a shell, and what developers often use when they need an unambiguous representation of an object. Its job is to be precise, even if the output it produces looks a little less polished.
For example
Going back to our pet shelter program, let’s say we want to print out one of the dogs' names. Without __str__ or __repr__, printing dog just gives us this weird memory address:
class Dog:
def __init__(self, name):
self.name = name
dog = Dog("Fido")
print(dog) # <__main__.Dog object at 0x0000023...>
See what I mean?
Dog object at 0x0000023
But, if we add a __str__, it then fixes the issue and uses the correct string:
class Dog:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Dog named {self.name}"
dog = Dog("Fido")
print(dog) # Dog named Fido
Simple!
And if we wanted a more technical version for debugging, we could also add a __repr__.
class Dog:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Dog named {self.name}"
def __repr__(self):
return f"Dog(name='{self.name}')"
dog = Dog("Fido")
print(dog) # Dog named Fido
dog # Dog(name='Fido')
TL;DR
__str__ is the user-friendly view, and __repr__ is the developer-friendly view.
You’ll typically define __str__ when you want your objects to print nicely, and sometimes you’ll define __repr__ too if you want more clarity when debugging.
A lot of the time when you’re coding, you’ll want to compare things so you can make decisions. Maybe you’re checking if two numbers are equal, seeing if one is bigger than another, or sorting a list.
That’s where comparison dunder methods come in, and the most common ones are:
__eq__ checks equality (==). You might use this to see if two dogs have the same ID
__lt__ checks less-than (<). You could use this to see which dog is younger
__gt__ checks greater-than (>). Useful for checking which one is older
__le__ and __ge__ covers the “or equal to” versions of these comparisons
Python already knows how to do this for its built-in types such as numbers, strings, and lists. But when you create your own objects, Python doesn’t know what “equal” or “less than” means in relation to these unless you tell it.
For example
Let’s say that when we uploaded our dogs’ information via our __init__ earlier, we saved both their names and their ages. Now we want to organize them by age.
Without a dunder method, Python has no idea what to do:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
fido = Dog("Fido", 3)
rex = Dog("Rex", 5)
print(fido < rex) # TypeError: '<' not supported between instances of 'Dog' and 'Dog'
But if we give Python a rule by adding __lt__, it suddenly works:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def __lt__(self, other):
return self.age < other.age
fido = Dog("Fido", 3)
rex = Dog("Rex", 5)
print(fido < rex) # True
See what’s happening?
When Python sees fido < rex, it also sees their respective ages and asks itself: “Is 3 less than 5?” Since that’s true, the result is True. This lets you sort or filter dogs based on their age.
Simple!
We can also define what “equal” means. Let’s say we want to check if two dogs are the same age:
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
fido = Dog("Fido", 3)
luna = Dog("Luna", 3)
print(fido == luna) # True
In this case, __eq__ tells Python to compare ages. Since both Fido and Luna are 3, the result is True.
But remember, we can use these comparisons for more than just numbers. For example, what if we want to organize all the dogs’ names alphabetically?
Well, we could use the exact same method (__lt__) but give it a different meaning.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def __lt__(self, other):
return self.name < other.name
dogs = [Dog("Rex", 5), Dog("Fido", 3), Dog("Luna", 2)]
sorted_dogs = sorted(dogs)
for dog in sorted_dogs:
print(dog.name)
# Fido
# Luna
# Rex
Here, __lt__ is no longer looking at ages. It’s comparing names instead:
def __lt__(self, other):
return self.name < other.name
When Python sorts the list, it checks things like: “Is 'Fido' less than 'Luna' alphabetically?” Since 'F' comes before 'L', it says yes. That’s why the dogs end up listed as Fido, Luna, and Rex in alphabetical order.
When you’re coding, you’ll often need to keep track of groups of things, not just one. That’s why Python has lists, dictionaries, and sets. These are called containers, because they hold multiple items inside them.
Most of the time, you don’t just care that you have a group, you actually need to work with it. But you might want to check how many items there are, look at them all one-by-one, or grab a specific item. And container dunder methods make that possible.
The main ones are:
__len__ tells Python how many items are in your object
__getitem__ lets you use square brackets ([]) to grab a specific item
__iter__ makes your object usable in a for loop
For example
Let’s say you just came in for your weekend shift at the shelter. The first things you want to know are:
How many dogs are now in the shelter
Which dog joined most recently, so you can make sure all the paperwork is set up (and give them a hug)
As well as fetching a full list of all current dogs
Here’s how container methods make that possible.
First of all, let’s give our shelter these three container methods so it works more like a list:
class Shelter:
def __init__(self, dogs):
self.dogs = dogs
def __len__(self):
return len(self.dogs)
def __getitem__(self, index):
return self.dogs[index]
def __iter__(self):
return iter(self.dogs)
shelter = Shelter(["Fido", "Rex", "Luna"])
Now we can use this to figure out the answers to our questions.
1. How many dogs are in the shelter?
When you call len(shelter), Python runs the __len__ method so we can see the total number of dogs:
print(len(shelter))
Output:
3
That tells you there are three dogs in the shelter right now.
2. Who was the latest dog to join?
With __getitem__ applied to our container, we can use square brackets ([]) just like with a list.
Here shelter[-1] means “give me the last dog in the list”, as this would then show us the last to join the list i.e. the newest doggo at the shelter.
print(shelter[-1])
Output:
Luna So Luna was the most recent arrival.
3. What’s the full list of dogs right now?
With __iter__ added, we can now run a for loop. This means we could then use it to scan through every dog in the shelter and see who is there, without writing extra code to step through manually.
for dog in shelter:
print(dog)
Fido
Rex
Luna
That way, you can see the full list of dogs to confirm who’s still there, and who might’ve been adopted.
Handy right?
TL;DR
Container methods let your objects act like collections. They power len(), [], and for loops, making your custom objects feel just as easy to use as Python’s built-in lists.
Sometimes you’ll want to do math with your objects, and Python gives us a few different dunder methods to handle this.
The main ones are:
__add__ runs when you use +
__sub__ runs when you use -
__mul__ runs when you use *
__truediv__ runs when you use /
As I said earlier, when it comes to numbers and strings, Python already knows what these operators mean. 2 + 3 gives 5, and "hi" + " there" gives "hi there".
But for your own objects, Python doesn’t know what “add” or “multiply” means unless you tell it.
For example
Let’s say each dog in our shelter eats 2kg of food a day, and we’d like to quickly figure out the total food needed each week, so that we can automatically place an order.
By default, Python won’t understand this:
class Shelter:
def __init__(self, dogs):
self.dogs = dogs
shelter = Shelter(["Fido", "Rex", "Luna"])
print(shelter * 2) # TypeError: unsupported operand type(s)
See the error?
That happens because Python doesn’t know how to use * with objects created from our Shelter class. So let’s give it a rule with __mul__:
class Shelter:
def __init__(self, dogs):
self.dogs = dogs
def __len__(self):
return len(self.dogs)
def __mul__(self, food_per_dog):
return len(self) * food_per_dog
shelter = Shelter(["Fido", "Rex", "Luna"])
print(shelter * 2) # 6
Here, when Python sees shelter * 2, it calls __mul__, which multiplies the number of dogs (len(self)) by food_per_dog. Since there are 3 dogs and each eats 2kg, the result is 6kgs required per day, or 42kgs per week.
Simple!
Arithmetic operators don’t just work with numbers, though. You can define them to do whatever makes sense for your objects.
For example
Suppose that two shelters merge, and we want to combine their dog lists. We can use __add__ to tell Python how to handle + in this specific case:
class Shelter:
def __init__(self, dogs):
self.dogs = dogs
def __add__(self, other):
return Shelter(self.dogs + other.dogs)
shelter1 = Shelter(["Fido", "Rex"])
shelter2 = Shelter(["Luna", "Bella"])
merged = shelter1 + shelter2
print(len(merged.dogs)) # 4
Here, shelter1 + shelter2 produces a brand-new Shelter with all four dogs, while behind the scenes Python calls __add__, which glues the two lists together
TL;DR
Arithmetic methods let your objects handle mathematical-type operations, numeric or otherwise. You just need to decide what +, -, *, or / means for your objects, so Python can handle them just like numbers.
Easy!
Not all dunder methods fit neatly into setup, representation, comparisons, containers, or arithmetic.
The most common ones you’ll come across outside those categories are __call__ and the context manager pair __enter__ / __exit__. Each of these unlocks very different behaviors, so let’s take them one at a time.
In Python, when you call a function, you’re basically telling Python to run it. You do that by writing the function’s name, followed by parentheses:
def greet():
return "Hello!"
print(greet()) # Hello!
The parentheses are the trigger. They tell Python, “go ahead and execute this”. Without them, nothing happens:
print(greet) # <function greet at 0x...>
So far, so good. But sometimes you might want an object that can hold data or settings, and also behave like a function when you call it.
Maybe:
A machine learning model object that stores its training settings, but also makes predictions when you call it
A text cleaner that saves its rules, but also processes strings when you call it
A caching tool that remembers old results, but still runs new calculations when you call it
Normally, objects don’t work like that. They’re just data holders, so if you try to “call” one like a function, Python gets confused:
dog = "Fido"
print(dog()) # TypeError: 'str' object is not callable
That’s where the __call__ method comes in. If you define this inside your class, you’re basically telling Python: “When this object is called with parentheses, here’s what to do”.
For example
Let’s say that you’re working on a small program to track your training for a marathon.
You log your distance in miles, but your friend uses kilometers, and you want an easy way to convert between the two whenever you need to share your results with them.
Sure, you could write a function like def miles_to_km(x): return x * 1.609, but that only works for one specific conversion. What if you want it the other way around? Or want to reuse the function for something else later on?
In scenarios like those you’d end up with a pile of separate functions.
To avoid that hassle however, you could simply make a Converter object with a __call__ method. It would store the conversion rate, and when you “call” it, it would also do the math:
class Converter:
def __init__(self, rate):
self.rate = rate
def __call__(self, value):
return value * self.rate
miles_to_km = Converter(1.609)
print(miles_to_km(10)) # 16.09
print(miles_to_km(25)) # 40.225
Handy right?
TL;DR
__call__ lets your objects act like functions. It’s how you make an object that both stores information and runs code when you call it.
When you open a file in Python, you’re basically saying: “Keep this file ready for me, because I want to use it”.
The thing is that once you’re done, you also have to close the file. because if you don’t, then the data might not save properly.
This means that the file can get locked so other programs can’t use it, and may also slowly waste system resources by simply being left open.
The trouble is, people forget. Or worse, something unexpected happens.
Perhaps your code crashes before it ever reaches the line that closes the file. And so now you’ve got a file left hanging open, which is one of those frustrating bugs that doesn’t show up until it causes a real problem later on.
All this is why Python has context managers.
A context manager is just a way of saying: “When I start using this thing, do some setup; and when I’m done, no matter what happens, clean it up properly.”
In our code, we use with for this. However, as you can guess, with only works because files (and lots of other objects) implement two special dunder methods:
__enter__ runs at the start, when the block begins
__exit__ runs at the end, when the block finishes. Even if an error happened in the middle
For example
Let’s say you’re keeping track of the shelter’s adoptions in a file. You open it, write a new dog’s name, and then close it:
f = open("adoptions.txt", "w")
f.write("Fido adopted!\n")
# If an error happens here, the file never gets closed!
f.close()
See the issue?
If your program crashes before f.close() runs, the file might stay open and the update might not be saved.
So here’s the safer version using with (so that we can apply __enter__ and __exit__ under the hood)
with open("adoptions.txt", "w") as f:
f.write("Fido adopted!\n")
# Even if an error happens here, the file will still close properly
This second version now guarantees that the file will be closed once the code in the block executes, even if it throws an error. That means your adoption record actually makes it into the file, and you don’t risk leaving it stuck open.
TL;DR
__enter__ and __exit__ are the backbone of context managers. They ensure that Python’s important cleanup happens for you, so you don’t lose your data or leave things hanging open.
Hopefully this guide has helped pull back the curtain and made dunder methods a little less mysterious.
Most of the time, you won’t be writing them yourself; you’ll just use the functions that rely on them, like len(), print(), or sorted(). But every so often, you’ll run into a situation where adding one makes sense.
That might mean teaching Python how to compare your objects, how to print them nicely, or even letting them act like functions. Knowing how these hooks work means that not only will you not be stuck when something doesn’t behave as expected, but that you’ll also have the tools to fix it.
As usual though, the best way to really understand a coding concept is to try it out! So go ahead and pick a couple of dunder methods, add them to a simple class in your Python code, and see how your objects change.
Don’t forget, if you want to learn more and dive deep into Python, then be sure to check out Andrei's Complete Python Developer course
It’ll take you from an absolute beginner and teach you everything you need to get hired ASAP and ace the tech interview.
This is the only Python course you need if you want to go from a complete Python beginner to getting hired as a Python Developer this year!
Alternatively, if you're already pretty good at Python and want to build some interesting and useful projects, why not check out my course on Python Automation:
It'll show you how to automate all of the boring or repetitive tasks in your life, and makes for some pretty stand out portfolio projects!
Plus, as part of your membership, you'll get access to both of these courses and others, and be able to join me and 1,000s of other people (some who are alumni mentors and others who are taking the same courses that you will be) in the ZTM Discord.
Ask questions, help others, or just network with other Python Developers, students, and tech professionals.
If you enjoyed Travis's post and want to get more like it in the future, subscribe below. By joining over 300,000 ZTM email subscribers, you'll receive exclusive ZTM posts, opportunities, and offers.
No spam ever, unsubscribe anytime
If you enjoyed this post, check out my other Python tutorials: