Magic Methods
Python's magic methods are methods that are defined in classes and are used to provide functionality for certain built-in operations. These methods start and end with double underscores, such as __init__ and __add__.
Creation
def __new__(cls, ...) # Constructor - first called
def __init__(self, ...) # Initializer
def __del__(self) # Destructor - rarely used
Representation
def __str__(self) # Nice display for users
def __repr__(self) # Development representation
def __doc__(self) # Get docstring
def __bytes__(self) # Bytes representation
def __hash__(self) # Hash function for dictionaries
Example:
class Product:
def __init__(self, a, name):
self.a = a
self.name = name
def __str__(self) -> str:
return f"Product: {self.name}"
def __repr__(self) -> str:
return f':{self.name}::'
def __hash__(self) -> int:
return hash(self.name)
Attribute Access
def __getattr__(self, name) # Called when attribute doesn't exist
def __setattr__(self, name, val) # Called when attribute is set
def __delattr__(self, name) # Called when attribute is deleted
def __getattribute__(self, name) # Called on every attribute access
Numeric Operations
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __mul__(self, other):
return Complex(
self.real * other.real - self.imag * other.imag,
self.real * other.imag + self.imag * other.real
)
def __truediv__(self, other):
denom = other.real**2 + other.imag**2
return Complex(
(self.real*other.real + self.imag*other.imag) / denom,
(self.imag*other.real - self.real*other.imag) / denom
)
Operator Chaining
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __mul__(self, other):
return self.x * other.x + self.y * other.y
a = Vector(1, 2)
b = Vector(3, 4)
c = Vector(5, 6)
result = a * b + c * b
print(result) # 17
In-Place Operators
class RunningAverage:
def __init__(self):
self.count = 0
self.total = 0
def __iadd__(self, other):
self.count += 1
self.total += other
return self
def __float__(self):
return self.total / self.count
avg = RunningAverage()
avg += 1
avg += 2
avg += 3
print(float(avg)) # 2.0
Containers
class Deck:
ranks = [str(n) for n in range(2, 11)] + ["J", "Q", "K", "A"]
suits = ["hearts", "diamonds", "clubs", "spades"]
def __init__(self):
self.cards = [f"{rank} of {suit}"
for suit in self.suits
for rank in self.ranks]
def __len__(self):
return len(self.cards)
def __getitem__(self, index):
return self.cards[index]
def __contains__(self, card):
return card in self.cards
my_deck = Deck()
print(len(my_deck)) # 52
print("10 of hearts" in my_deck) # True
Comparison Methods
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
def __le__(self, other):
return self.age <= other.age
def __repr__(self):
return f"Person({self.name}, {self.age})"
p1 = Person("Alice", 30)
p2 = Person("Bob", 25)
print(p1 == p2) # False (different ages)
print(p1 < p2) # False (30 > 25)
print(p1 <= p2) # False
Callable Objects
Make an instance callable like a function:
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
doubler = Multiplier(2)
tripler = Multiplier(3)
print(doubler(10)) # 20
print(tripler(10)) # 30
print(callable(doubler)) # True
Asynchronous Magic Methods
For async programming, classes can define __aiter__, __anext__, and __await__ to work with async for and await:
import asyncio
class AsyncCounter:
def __init__(self, limit):
self.limit = limit
self.current = 0
async def __aiter__(self):
return self
async def __anext__(self):
if self.current >= self.limit:
raise StopAsyncIteration
value = self.current
self.current += 1
return value
async def main():
counter = AsyncCounter(5)
async for num in counter:
print(num) # 0, 1, 2, 3, 4
asyncio.run(main())
Metaprogramming with __slots__ and __new__
Use __slots__ to restrict attribute creation and save memory, and override __new__ for custom instance creation:
# __slots__ restricts which attributes can be set on an instance
# and saves memory by not creating __dict__ for each instance
class Point:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y) # 1 2
# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
Custom instance creation with __new__:
# Singleton pattern using __new__
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True (same instance)
Validate arguments in __new__ before creation:
class PositiveNumber:
def __new__(cls, value):
if value <= 0:
raise ValueError("Value must be positive")
instance = super().__new__(cls)
instance.value = value
return instance
n = PositiveNumber(10)
print(n.value) # 10
# NegativeNumber(-5) # ValueError
Copy Behavior
Customize shallow and deep copying with __copy__ and __deepcopy__:
import copy
class CustomList:
def __init__(self, items):
self.items = items
def __copy__(self):
# Shallow copy - same reference to inner list
return CustomList(self.items)
def __deepcopy__(self, memo):
# Deep copy - new list with copied contents
return CustomList(copy.deepcopy(self.items, memo))
original = CustomList([1, [2, 3]])
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original.items[1].append(4)
print(shallow.items[1]) # [2, 3, 4] (affected)
print(deep.items[1]) # [2, 3] (not affected)
Serialization with Pickle
Customize pickle behavior with __reduce__, __getstate__, and __setstate__:
import pickle
class CustomData:
def __init__(self, data):
self.data = data
self._computed = "expensive computation"
def __getstate__(self):
# Only save data, not _computed
return {'data': self.data}
def __setstate__(self, state):
# Restore state and recompute expensive parts
self.data = state['data']
self._computed = "recomputed on load"
obj = CustomData("important")
serialized = pickle.dumps(obj)
loaded = pickle.loads(serialized)
print(loaded.data) # important
print(loaded._computed) # recomputed on load
Custom Formatting
Customize string formatting with __format__:
class Distance:
def __init__(self, meters):
self.meters = meters
def __format__(self, format_spec):
if format_spec == 'km':
return f"{self.meters / 1000:.2f} km"
elif format_spec == 'cm':
return f"{self.meters * 100:.2f} cm"
else:
return f"{self.meters:.2f} m"
d = Distance(1234.5)
print(format(d)) # 1234.50 m
print(format(d, 'km')) # 1.23 km
print(format(d, 'cm')) # 123450.00 cm
Subclass Creation Hook
Use __init_subclass__ to customize subclass behavior:
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
# Validate or modify subclasses automatically
if hasattr(cls, 'required_method'):
cls._validated = True
else:
raise TypeError(
f"{cls.__name__} must implement required_method"
)
# class Invalid(Base): # Raises TypeError
# pass
class Valid(Base):
def required_method(self):
pass
print(Valid._validated) # True
Class Subscription
Use __class_getitem__ to customize class[] syntax:
class Vector:
def __class_getitem__(cls, item):
return f"Vector of {item}"
class Matrix:
def __class_getitem__(cls, item):
if isinstance(item, tuple):
return f"{item[0]}x{item[1]} Matrix"
return f"Matrix of {item}"
print(Vector[3]) # Vector of 3
print(Matrix[3, 3]) # 3x3 Matrix
print(Matrix["float"]) # Matrix of float
Class Namespace with __prepare__
Use __prepare__ in a metaclass to customize the class namespace before attributes are added:
from collections import OrderedDict
class OrderedMeta(type):
def __prepare__(mcs, name, **kwargs):
# Return OrderedDict to preserve definition order
return OrderedDict()
def __new__(mcs, name, bases, namespace):
# Store field order in class
cls = super().__new__(mcs, name, bases, namespace)
cls._field_order = list(namespace.keys())
return
class Person(metaclass=OrderedMeta):
name = ""
age = 0
email = ""
print(Person._field_order) # ['name', 'age', 'email']
Custom isinstance/issubclass Checks
Use __instancecheck__ and __subclasscheck__ to customize type checking:
class Meta(type):
def __instancecheck__(cls, instance):
# Treat dict-like objects as instances of CustomDict
return hasattr(instance, 'get') and hasattr(instance, 'keys')
def __subclasscheck__(cls, subclass):
# Any class with 'data' attribute is considered subclass
return hasattr(subclass, 'data') or hasattr(subclass, '_data')
class CustomDict(metaclass=Meta):
pass
print(isinstance({"a": 1}, CustomDict)) # True (dict has get/keys)
print(isinstance([1], CustomDict)) # False
class DataWrapper:
data = 42
print(issubclass(DataWrapper, CustomDict)) # True
Custom dir() Output
Use __dir__ to customize what shows up when calling dir() on an object:
class Config:
def __init__(self):
self.debug = True
self.version = "1.0"
self._secret = "password"
def __dir__(self):
# Return public attributes, filter out private
return [k for k in vars(self) if not k.startswith('_')]
config = Config()
print(dir(config)) # ['debug', 'version']
Destructor with __del__
Use __del__ for cleanup when an object is garbage collected (use sparingly):
class FileHandler:
def __init__(self, filename):
self.filename = filename
self.file = open(filename, 'w')
print(f"Opened {filename}")
def __del__(self):
# Cleanup when object is garbage collected
if not self.file.closed:
self.file.close()
print(f"Closed {self.filename}")
f = FileHandler("test.txt")
del f # Triggers __del__
# Output: Opened test.txt
# Closed test.txt
Pattern Matching with __match_args__
Use __match_args__ to support structural pattern matching (Python 3.10+):
class Point:
__match_args__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(1, 2)
match point:
case Point(x=0, y=0):
print("Origin")
case Point(x=1, y=2):
print("Found it!")
case Point(x, y):
print(f"Other point: {x}, {y}")
# Output: Found it!