
Python Special Methods Cheatsheet
Welcome, Python enthusiasts! If you’ve ever wondered how you can make your custom classes behave like built-in types—such as using len(obj)
, obj + other
, or even print(obj)
—the secret lies in Python’s special methods (also known as magic or dunder methods). These methods allow you to tap into Python’s core language features, making your code more expressive and intuitive. Let’s dive into the most essential special methods you should know, complete with examples and use cases.
Object Representation and Conversion
When you create a class, you’ll often want to define how instances are displayed or converted to other types. The __str__
and __repr__
methods are fundamental for this.
The __str__
method is called by str()
and print()
, and should return a human-readable string. The __repr__
method is called by repr()
and should return an unambiguous string that could ideally be used to recreate the object.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 4)
print(str(p)) # Output: Point(3, 4)
print(repr(p)) # Output: Point(x=3, y=4)
Additionally, you can define __bytes__
, __format__
, and __bool__
to control other conversion behaviors. For instance, __bool__
defines the truthiness of your object. If not defined, Python falls back to __len__
or considers the object True
.
class Account:
def __init__(self, balance):
self.balance = balance
def __bool__(self):
return self.balance > 0
acc = Account(100.0)
if acc:
print("Account has funds!") # This will print
Method | Called By | Purpose |
---|---|---|
__str__ |
str() , print() |
Human-readable string representation |
__repr__ |
repr() |
Unambiguous representation, often for debugging |
__format__ |
format() |
Custom formatting support |
__bytes__ |
bytes() |
Byte representation |
__bool__ |
bool() |
Boolean context behavior |
Key takeaways:
- Always define __repr__
for debugging clarity.
- Use __str__
for user-friendly output.
- Implement __bool__
if your object’s truthiness isn’t obvious.
Rich Comparison Methods
Comparison methods let your objects work with operators like ==
, !=
, <
, >
, etc. Each corresponds to a special method you can implement.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
return self.celsius == other.celsius
def __lt__(self, other):
return self.celsius < other.celsius
# Similarly, you can define __le__, __gt__, __ge__, __ne__
t1 = Temperature(20)
t2 = Temperature(30)
print(t1 < t2) # Output: True
print(t1 == t2) # Output: False
If you define __eq__
, it’s a good practice to also define __ne__
, though Python will use the negation of __eq__
by default if you don’t. For ordering, you only need to define __lt__
and __eq__
, and then use the @functools.total_ordering
decorator to fill in the rest.
Important methods:
- __eq__
for ==
- __ne__
for !=
- __lt__
for <
- __le__
for <=
- __gt__
for >
- __ge__
for >=
Arithmetic Operations
Arithmetic methods enable your objects to work with mathematical operators. You can define what happens when someone adds two instances of your class, multiplies them, and so on.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2
print(v3.x, v3.y) # Output: 4 6
v4 = v1 * 3
print(v4.x, v4.y) # Output: 3 6
There are also reflected methods (e.g., __radd__
) for when the object is on the right side of the operator, and in-place methods (e.g., __iadd__
) for operations like +=
.
Operation | Method | Reflected Method | In-place Method |
---|---|---|---|
Addition | __add__ |
__radd__ |
__iadd__ |
Subtraction | __sub__ |
__rsub__ |
__isub__ |
Multiplication | __mul__ |
__rmul__ |
__imul__ |
Division | __truediv__ |
__rtruediv__ |
__itruediv__ |
Floor Division | __floordiv__ |
__rfloordiv__ |
__ifloordiv__ |
Modulo | __mod__ |
__rmod__ |
__imod__ |
Exponentiation | __pow__ |
__rpow__ |
__ipow__ |
Pro tip: Implement reflected methods if your class might be used on the right side of an operation with a built-in type.
Container Emulation
To make your class act like a container (list, dict, etc.), you can implement methods like __len__
, __getitem__
, __setitem__
, and __delitem__
.
class Inventory:
def __init__(self):
self.items = []
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def __delitem__(self, index):
del self.items[index]
inv = Inventory()
inv.items = ['apple', 'banana', 'cherry']
print(len(inv)) # Output: 3
print(inv[1]) # Output: banana
inv[1] = 'blueberry'
print(inv[1]) # Output: blueberry
del inv[1]
print(len(inv)) # Output: 2
You can also implement __contains__
for the in
operator, and __iter__
to make your object iterable.
Essential methods for containers:
- __len__
for len(obj)
- __getitem__
for indexing and slicing
- __setitem__
for assignment to indices
- __delitem__
for deleting items
- __contains__
for in
checks
- __iter__
for iteration in loops
Attribute Access
Python provides special methods to customize attribute access. This is useful for creating proxy objects, lazy attributes, or dynamic attributes.
The __getattr__
method is called when an attribute is not found through normal lookup. __getattribute__
is called for every attribute access, so use it cautiously. __setattr__
intercepts all attribute assignments, and __delattr__
handles attribute deletion.
class LazyLoader:
def __init__(self):
self.loaded_data = None
def __getattr__(self, name):
if self.loaded_data is None:
self.loaded_data = self._load_data()
return getattr(self.loaded_data, name)
def _load_data(self):
# Simulate loading data
class Data:
value = 42
return Data()
loader = LazyLoader()
print(loader.value) # Output: 42 (loaded on first access)
Be careful with __setattr__
; you must avoid recursive calls by using super().__setattr__
or storing attributes in a different way.
Common use cases: - Lazy loading: Delay expensive operations until needed. - Validation: Check values before assigning attributes. - Proxy patterns: Forward attribute access to another object.
Callable Objects
By implementing __call__
, you can make your instances callable like functions. This is useful for objects that need to maintain state between calls.
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
counter = Counter()
print(counter()) # Output: 1
print(counter()) # Output: 2
This is often used in decorators, functors, or any time you want an object to be "function-like."
Context Managers
Context managers allow you to allocate and release resources precisely. You can create your own by implementing __enter__
and __exit__
.
class FileHandler:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
with FileHandler('test.txt', 'w') as f:
f.write('Hello, context manager!')
The __exit__
method handles exceptions too; if it returns True
, exceptions are suppressed.
Descriptors
Descriptors are a powerful feature that allows you to customize attribute access at a low level. They are classes that implement __get__
, __set__
, or __delete__
.
class PositiveNumber:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("Must be positive")
instance.__dict__[self.name] = value
class Product:
price = PositiveNumber()
def __init__(self, price):
self.price = price
p = Product(10) # OK
p.price = -5 # Raises ValueError
Descriptors are used internally for properties, class methods, and static methods.
Customizing Instance Creation
The __new__
method is responsible for creating a new instance, and it’s called before __init__
. It’s useful for immutable types or when you need to control object creation.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
print(a is b) # Output: True
Use __new__
sparingly; most classes don’t need it.
Pickling and Copying
To support object serialization with pickle
, implement __getstate__
and __setstate__
. For copying, define __copy__
and __deepcopy__
.
import pickle
class Data:
def __init__(self, value):
self.value = value
def __getstate__(self):
return self.value
def __setstate__(self, state):
self.value = state
d = Data(10)
serialized = pickle.dumps(d)
restored = pickle.loads(serialized)
print(restored.value) # Output: 10
Meta Programming
Metaclasses allow you to customize class creation. By defining __init_subclass__
in a base class, you can hook into subclass creation without full metaclasses.
class PluginBase:
plugins = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.plugins.append(cls)
class Plugin1(PluginBase):
pass
class Plugin2(PluginBase):
pass
print(PluginBase.plugins) # Output: [<class '__main__.Plugin1'>, <class '__main__.Plugin2'>]
This is simpler than using a metaclass and covers many use cases.
Summary Table of Common Special Methods
Category | Method | Purpose |
---|---|---|
Representation | __str__ |
Human-readable string |
__repr__ |
Unambiguous representation | |
Comparison | __eq__ |
Equality check |
__lt__ |
Less than | |
Arithmetic | __add__ |
Addition |
__sub__ |
Subtraction | |
Container | __len__ |
Length |
__getitem__ |
Indexing and slicing | |
Attribute | __getattr__ |
Attribute access when not found |
__setattr__ |
Attribute assignment | |
Callable | __call__ |
Make instance callable |
Context Manager | __enter__ |
Enter context |
__exit__ |
Exit context | |
Descriptor | __get__ |
Descriptor get |
__set__ |
Descriptor set |
Remember, special methods are the backbone of Python’s consistency and elegance. They let your objects integrate seamlessly with the language. Use them wisely to write more intuitive and Pythonic code. Happy coding!