Python Special Methods Cheatsheet

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!