
Magic methods are special methods whose names start and end with two underscores. Python calls them for ordinary operations such as creating an object, printing it, asking for its length, or calling it like a function. You may also hear them called dunder methods.
You usually do not call these methods directly. You write the method that matches the behavior you want, then use normal Python syntax and let Python make the call.
The useful way to learn them is to start with the syntax you want your object to support. If print(playlist) should be readable, look at __str__. If len(playlist) should mean something, look at __len__. You do not need to memorise a catalogue of every dunder method before writing a class.
Python calls __init__ after it creates a new instance. Put the initial instance attributes there.
class Playlist:
def __init__(self, songs):
self.songs = list(songs)
playlist = Playlist(["Intro", "Signal"])
print(playlist.songs)The list(songs) call makes a new list for the object. The initializer prepares the object; it does not return the object itself.
str(value) and print(value) prefer __str__, which should be readable to a person. repr(value) is meant for debugging and should contain enough detail to identify the value clearly.
class Playlist:
def __init__(self, songs):
self.songs = list(songs)
def __str__(self):
return ", ".join(self.songs)
def __repr__(self):
return f"Playlist({self.songs!r})"
playlist = Playlist(["Intro", "Signal"])
print(playlist)
print(repr(playlist))The readable form is Intro, Signal. The representation is Playlist(['Intro', 'Signal']). A __repr__ result does not have to be executable, but showing the important state makes debugging much less frustrating.
Define __len__ when your object has a meaningful size. Python expects the method to return a non-negative integer.
class Playlist:
def __init__(self, songs):
self.songs = list(songs)
def __len__(self):
return len(self.songs)
playlist = Playlist(["Intro", "Signal"])
print(len(playlist))The call to len(playlist) is Python's friendly syntax for asking the object for playlist.__len__().
The same idea applies to comparisons. If two objects represent the same value, __eq__ can define what first == second means:
class Song:
def __init__(self, title):
self.title = title
def __eq__(self, other):
if not isinstance(other, Song):
return NotImplemented
return self.title == other.title
print(Song("Signal") == Song("Signal"))Without __eq__, two separate Song objects compare by identity, even when their attributes match. Returning NotImplemented for an unrelated type lets Python handle that comparison instead of pretending every object is comparable to a Song.
Functions are objects too. A regular object can act like a function when its class defines __call__.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
print(double(5))double(5) calls double.__call__(5), so the object remembers its factor between calls. This pattern is useful when a callable needs configuration or state.
Another common dunder is __iter__, which lets an object participate in a for loop. For a playlist, iteration should expose the songs in the same order a caller expects:
class Playlist:
def __init__(self, songs):
self.songs = list(songs)
def __iter__(self):
return iter(self.songs)
playlist = Playlist(["Intro", "Signal"])
for song in playlist:
print(song)These methods make an object fit a familiar Python protocol. That is the real benefit: callers can use normal language features instead of learning a private API for every class.
My caveat is to add a magic method only when normal syntax becomes clearer. A custom __str__ often helps immediately. A pile of clever dunders can make a class feel like it is fighting Python instead of working with it. Keep return types and failure behavior unsurprising: __len__ should return a non-negative integer, __str__ should return a string, and an operator should reject unsupported values clearly.
