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!