In the last chapter, our Spacecraft class had two methods that looked a little strange: __init__ and __str__. Those double underscores on both sides aren’t a typo, and they aren’t decoration. They’re Python’s way of marking a method as special. Programmers call these dunder methods - short for "Double UNDERscore" - and this chapter is all about what they are and why they matter.
Any method whose name starts and ends with two underscores, like __init__ or __str__, is a dunder method. You don’t invent these names yourself - Python has a fixed list of them, and each one has a specific job. What makes them special is that you almost never call them directly. Instead, Python calls them for you, automatically, in response to something else you do to the object - creating it, printing it, comparing it, and so on.
Think of dunder methods as hooks. You write the hook, and Python promises to pull it at just the right moment.
You met this one last chapter, but let’s call it out by name: __init__ is called automatically the instant you create a new object. It’s where you set up all of an object’s starting values.
class Spacecraft:
def __init__(self, name, topspeed):
self.name = name
self.topspeed = topspeed
self.current_speed = 0
enterprise = Spacecraft("Enterprise", 9.9)
print(enterprise.name) # -> Enterprise
print(enterprise.topspeed) # -> 9.9Notice we didn’t have to set enterprise.name = "Enterprise" on a separate line afterward, like we did before __init__ showed up. Passing "Enterprise" and 9.9 straight into Spacecraft(…) hands them right to __init__, which tucks them away on self. __init__ is a dunder, but it’s such a common one you’ll write it in almost every class you ever build.
You also met __str__ last chapter. It’s the hook that Python pulls whenever your object needs to be turned into a readable string - most commonly, when you print() it.
class Spacecraft:
def __init__(self, name, topspeed):
self.name = name
self.topspeed = topspeed
def __str__(self):
return "Starship " + self.name
enterprise = Spacecraft("Enterprise", 9.9)
print(enterprise)
# -> Starship EnterpriseWithout __str__, print(enterprise) would spit out something ugly and unhelpful, like <__main__.Spacecraft object at 0x103013100>. With it, you decide exactly what shows up.
By default, Python considers two objects equal only if they are literally the same object in memory - not just similar, but the exact same one.
enterprise = Spacecraft("Enterprise", 9.9)
enterprise_copy = Spacecraft("Enterprise", 9.9)
print(enterprise == enterprise_copy) # -> False, even though they look identical!That’s often not what we want. If two spacecraft have the same name, we probably want == to say they’re equal. That’s what __eq__ is for.
class Spacecraft:
def __init__(self, name, topspeed):
self.name = name
self.topspeed = topspeed
def __eq__(self, other):
return self.name == other.name
enterprise = Spacecraft("Enterprise", 9.9)
enterprise_copy = Spacecraft("Enterprise", 9.9)
print(enterprise == enterprise_copy) # -> True now!__eq__ takes two things to compare: self (the object on the left of ==) and other (the object on the right). Whatever expression you return becomes the answer to "are these equal?"
Remember len(), from way back in the Built-in Functions chapter? It works on strings, lists, dicts, and sets - and it can work on your own classes too, if you define __len__.
class Fleet:
def __init__(self, ships):
self.ships = ships
def __len__(self):
return len(self.ships)
home_fleet = Fleet(["Enterprise", "Constellation", "Defiant"])
print(len(home_fleet)) # -> 3Whatever number __len__ returns is exactly what len() hands back to whoever calls it. This is a small example of a bigger idea: dunder methods are how your own classes get to plug into Python’s built-in functions and operators, instead of only working with Python’s built-in types.
That same idea applies to operators, not just functions. __add__ controls what happens when your object is used on the left side of a +.
class Fleet:
def __init__(self, ships):
self.ships = ships
def __add__(self, other):
return Fleet(self.ships + other.ships)
def __len__(self):
return len(self.ships)
home_fleet = Fleet(["Enterprise", "Constellation"])
reserve_fleet = Fleet(["Defiant"])
combined_fleet = home_fleet + reserve_fleet
print(len(combined_fleet)) # -> 3Without __add__, writing home_fleet + reserve_fleet would just crash with an error, since Python has no idea how to "add" two Fleet objects together. With it, you decide what + should mean for your own class.
| Dunder Method | Gets Called When… | Common Use |
|---|---|---|
|
An object is created |
Setting up starting values |
|
The object is printed, or turned into a string |
A friendly, readable description |
|
Two objects are compared with |
Deciding what "equal" means for your class |
|
|
Reporting some meaningful "size" |
|
Two objects are combined with |
Deciding what "adding" means for your class |
|
Tip
|
|
Here’s the big idea to walk away with: dunder methods are the bridge between your own custom classes and all the built-in syntax and functions you’ve already learned throughout this book - print(), len(), ==, +, and plenty more we haven’t shown here. You’re not learning a pile of odd, unrelated tricks. You’re learning the one mechanism Python uses, over and over, to let your objects behave like they belong in the language, right alongside strings, lists, and dictionaries.
You won’t need most dunder methods most of the time. But __init__ and __str__ will show up in nearly every class you write from here on out, so make sure those two, at least, feel like old friends.