Senior Software Engineer Edition

    Last Updated: November 2025 | Python 3.10+ Focus

    Table of Contents

    1. Introduction
    2. Object Lifecycle Magic Methods
    3. Comparison Magic Methods
    4. Arithmetic Magic Methods
    5. Unary Operators and Functions
    6. Type Conversion Magic Methods
    7. Container Magic Methods
    8. Attribute Access Magic Methods
    9. Callable Objects
    10. Context Managers
    11. Descriptor Protocol
    12. Copying and Pickling
    13. String Representation
    14. Advanced Magic Methods
    15. Performance Optimization
    16. Type Hints & Protocols
    17. Metaclass Patterns
    18. Async Magic Methods
    19. Memory Management & Slots
    20. Pattern Matching (Python 3.10+)
    21. Production Best Practices

    Introduction

    Magic methods (also called dunder methods, from “double underscore”) are special methods in Python that allow you to define how objects of your class behave with built-in operations. They are always surrounded by double underscores (e.g., __init__, __str__).

    Why Use Magic Methods?

    • Intuitive Syntax: Make your objects work with Python’s built-in operators (+, -, *, [], etc.)
    • Pythonic Code: Write code that feels natural and follows Python conventions
    • Integration: Seamlessly integrate custom objects with Python’s standard library
    • Operator Overloading: Define custom behavior for operators
    • Protocol Implementation: Enable duck typing and structural subtyping
    • Performance: Optimize object behavior at the C level

    Senior Engineer Considerations

    Type Safety: Always use type hints with magic methods for better IDE support and static analysis:

    from typing import Self
    from collections.abc import Iterator
    
    class Vector:
        def __init__(self, x: float, y: float) -> None:
            self.x = x
            self.y = y
    
        def __add__(self, other: Self) -> Self:
            return self.__class__(self.x + other.x, self.y + other.y)
    Python

    Performance Impact: Magic methods are called frequently. Profile before optimizing:

    import timeit
    
    # Test performance of magic methods
    setup = "class Point: ..."
    code = "p1 + p2"
    time = timeit.timeit(code, setup=setup, number=1000000)
    Python

    Protocol-Oriented Design: Use typing.Protocol for structural subtyping:

    from typing import Protocol
    
    class Addable(Protocol):
        def __add__(self, other: 'Addable') -> 'Addable': ...
    
    def combine(a: Addable, b: Addable) -> Addable:
        return a + b  # Type-safe without inheritance
    Python

    Object Lifecycle Magic Methods

    __new__(cls, *args, **kwargs)

    Controls the creation of a new instance. Called before __init__. Rarely used unless you need to control instance creation (e.g., implementing singleton pattern).

    Thread-Safe Singleton Pattern (Production-Ready):

    import threading
    from typing import Optional, Any
    
    class Singleton:
        _instance: Optional['Singleton'] = None
        _lock: threading.Lock = threading.Lock()
        _initialized: bool = False
    
        def __new__(cls, *args: Any, **kwargs: Any) -> 'Singleton':
            if cls._instance is None:
                with cls._lock:
                    # Double-checked locking pattern
                    if cls._instance is None:
                        cls._instance = super().__new__(cls)
            return cls._instance
    
        def __init__(self, value: int) -> None:
            # Prevent re-initialization
            if not self._initialized:
                with self._lock:
                    if not self._initialized:
                        self.value = value
                        self._initialized = True
    
    # Test
    s1 = Singleton(10)
    s2 = Singleton(20)
    print(s1 is s2)  # True - same instance
    print(s1.value)  # 10 - not re-initialized
    Python

    Immutable Objects with __new__ (like int, str):

    from typing import Any
    
    class PositiveInt(int):
        """An integer that must be positive."""
    
        def __new__(cls, value: int) -> 'PositiveInt':
            if value <= 0:
                raise ValueError(f"Value must be positive, got {value}")
            instance = super().__new__(cls, value)
            return instance
    
        # No __init__ needed - immutable types use __new__
    
    num = PositiveInt(5)
    print(num + 3)  # 8
    # PositiveInt(-5)  # Raises ValueError
    Python

    Object Pooling Pattern (Flyweight):

    from typing import Dict, Any
    import weakref
    
    class Flyweight:
        """Memory-efficient object pooling for immutable objects."""
        _pool: Dict[tuple, 'Flyweight'] = weakref.WeakValueDictionary()
    
        def __new__(cls, *args: Any) -> 'Flyweight':
            key = args
            if key not in cls._pool:
                instance = super().__new__(cls)
                cls._pool[key] = instance
            return cls._pool[key]
    
        def __init__(self, x: int, y: int) -> None:
            # Only initialize if not already done
            if not hasattr(self, 'x'):
                self.x = x
                self.y = y
    
    # Reuses instances for same arguments
    p1 = Flyweight(1, 2)
    p2 = Flyweight(1, 2)
    print(p1 is p2)  # True - same object
    Python

    __init__(self, *args, **kwargs)

    Initializes a newly created instance. This is the constructor you use most often.

    Type-Safe Initialization with Validation:

    from typing import Optional
    import re
    
    class Person:
        """Person class with validated initialization."""
    
        def __init__(self, name: str, age: int, email: Optional[str] = None) -> None:
            # Validation before assignment
            if not isinstance(name, str) or not name.strip():
                raise TypeError("Name must be a non-empty string")
            if not isinstance(age, int) or age < 0:
                raise ValueError("Age must be a non-negative integer")
            if email and not re.match(r'^[\w.-]+@[\w.-]+\.\w+$', email):
                raise ValueError(f"Invalid email format: {email}")
    
            self._name = name
            self._age = age
            self._email = email
    
        @property
        def name(self) -> str:
            return self._name
    
        @property
        def age(self) -> int:
            return self._age
    
    person = Person("Alice", 30, "alice@example.com")
    Python

    Alternative: Use Dataclasses for Less Boilerplate (Recommended):

    from dataclasses import dataclass, field
    from typing import Optional, ClassVar
    
    @dataclass(slots=True, frozen=False)  # slots for memory efficiency
    class Person:
        """Modern Person implementation with dataclass."""
        name: str
        age: int
        email: Optional[str] = None
        _id_counter: ClassVar[int] = 0
        id: int = field(init=False)
    
        def __post_init__(self) -> None:
            """Called after __init__ for validation."""
            if self.age < 0:
                raise ValueError("Age must be non-negative")
            Person._id_counter += 1
            object.__setattr__(self, 'id', Person._id_counter)
    
    person = Person("Alice", 30)
    print(person.id)  # Auto-assigned ID
    Python

    __del__(self)

    Called when an object is about to be destroyed. Use with caution as timing is unpredictable.

    class FileHandler:
        def __init__(self, filename):
            self.filename = filename
            self.file = open(filename, 'w')
    
        def __del__(self):
            if hasattr(self, 'file') and not self.file.closed:
                self.file.close()
                print(f"File {self.filename} closed")
    Python

    Comparison Magic Methods

    These methods define how objects are compared using comparison operators.

    __eq__(self, other) – Equality (==)

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __eq__(self, other):
            if not isinstance(other, Point):
                return NotImplemented
            return self.x == other.x and self.y == other.y
    
    p1 = Point(1, 2)
    p2 = Point(1, 2)
    p3 = Point(2, 3)
    
    print(p1 == p2)  # True
    print(p1 == p3)  # False
    Python

    __ne__(self, other) – Not Equal (!=)

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __ne__(self, other):
            return not self.__eq__(other)
    Python

    __lt__(self, other) – Less Than (<)

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __lt__(self, other):
            if not isinstance(other, Person):
                return NotImplemented
            return self.age < other.age
    
    people = [Person("Alice", 30), Person("Bob", 25), Person("Charlie", 35)]
    sorted_people = sorted(people)
    for p in sorted_people:
        print(f"{p.name}: {p.age}")
    # Output: Bob: 25, Alice: 30, Charlie: 35
    Python

    __le__(self, other) – Less Than or Equal (<=)

    def __le__(self, other):
        if not isinstance(other, Person):
            return NotImplemented
        return self.age <= other.age
    Python

    __gt__(self, other) – Greater Than (>)

    def __gt__(self, other):
        if not isinstance(other, Person):
            return NotImplemented
        return self.age > other.age
    Python

    __ge__(self, other) – Greater Than or Equal (>=)

    def __ge__(self, other):
        if not isinstance(other, Person):
            return NotImplemented
        return self.age >= other.age
    Python

    Using @functools.total_ordering

    Instead of defining all comparison methods, define __eq__ and one other, then use the decorator:

    from functools import total_ordering
    
    @total_ordering
    class Student:
        def __init__(self, name, grade):
            self.name = name
            self.grade = grade
    
        def __eq__(self, other):
            if not isinstance(other, Student):
                return NotImplemented
            return self.grade == other.grade
    
        def __lt__(self, other):
            if not isinstance(other, Student):
                return NotImplemented
            return self.grade < other.grade
    
    s1 = Student("Alice", 85)
    s2 = Student("Bob", 90)
    print(s1 < s2)   # True
    print(s1 <= s2)  # True (auto-generated)
    print(s1 > s2)   # False (auto-generated)
    print(s1 >= s2)  # False (auto-generated)
    Python

    Arithmetic Magic Methods

    Binary Arithmetic Operators

    __add__(self, other) – Addition (+)

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            if not isinstance(other, Vector):
                return NotImplemented
            return Vector(self.x + other.x, self.y + other.y)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v1 = Vector(1, 2)
    v2 = Vector(3, 4)
    v3 = v1 + v2
    print(v3)  # Vector(4, 6)
    Python

    __sub__(self, other) – Subtraction (-)

    def __sub__(self, other):
        if not isinstance(other, Vector):
            return NotImplemented
        return Vector(self.x - other.x, self.y - other.y)
    Python

    __mul__(self, other) – Multiplication (*)

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __mul__(self, scalar):
            if isinstance(scalar, (int, float)):
                return Vector(self.x * scalar, self.y * scalar)
            return NotImplemented
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v = Vector(2, 3)
    print(v * 3)  # Vector(6, 9)
    Python

    __truediv__(self, other) – True Division (/)

    def __truediv__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x / scalar, self.y / scalar)
        return NotImplemented
    Python

    __floordiv__(self, other) – Floor Division (//)

    def __floordiv__(self, scalar):
        if isinstance(scalar, (int, float)):
            return Vector(self.x // scalar, self.y // scalar)
        return NotImplemented
    Python

    __mod__(self, other) – Modulo (%)

    class Number:
        def __init__(self, value):
            self.value = value
    
        def __mod__(self, other):
            if isinstance(other, Number):
                return Number(self.value % other.value)
            elif isinstance(other, (int, float)):
                return Number(self.value % other)
            return NotImplemented
    Python

    __pow__(self, other) – Exponentiation (**)

    def __pow__(self, exponent):
        if isinstance(exponent, (int, float)):
            return Number(self.value ** exponent)
        return NotImplemented
    Python

    __matmul__(self, other) – Matrix Multiplication (@)

    class Matrix:
        def __init__(self, data):
            self.data = data
    
        def __matmul__(self, other):
            # Simplified 2x2 matrix multiplication
            if not isinstance(other, Matrix):
                return NotImplemented
            # Implementation would do actual matrix multiplication
            return Matrix([[0, 0], [0, 0]])  # Placeholder
    Python

    Reflected Arithmetic Operators

    These are called when the left operand doesn’t support the operation.

    __radd__(self, other) – Reflected Addition

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            if isinstance(other, Vector):
                return Vector(self.x + other.x, self.y + other.y)
            return NotImplemented
    
        def __radd__(self, other):
            # Called when other + self is evaluated and other doesn't have __add__
            return self.__add__(other)
    Python

    Similar reflected methods exist:

    • __rsub__(self, other) – Reflected subtraction
    • __rmul__(self, other) – Reflected multiplication
    • __rtruediv__(self, other) – Reflected true division
    • __rfloordiv__(self, other) – Reflected floor division
    • __rmod__(self, other) – Reflected modulo
    • __rpow__(self, other) – Reflected exponentiation
    • __rmatmul__(self, other) – Reflected matrix multiplication

    Augmented Assignment

    __iadd__(self, other) – In-place Addition (+=)

    class MutableVector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __iadd__(self, other):
            if isinstance(other, MutableVector):
                self.x += other.x
                self.y += other.y
                return self  # Must return self
            return NotImplemented
    
        def __repr__(self):
            return f"MutableVector({self.x}, {self.y})"
    
    v1 = MutableVector(1, 2)
    v2 = MutableVector(3, 4)
    v1 += v2
    print(v1)  # MutableVector(4, 6)
    Python

    Similar augmented assignment methods:

    • __isub__(self, other) – In-place subtraction (-=)
    • __imul__(self, other) – In-place multiplication (*=)
    • __itruediv__(self, other) – In-place true division (/=)
    • __ifloordiv__(self, other) – In-place floor division (//=)
    • __imod__(self, other) – In-place modulo (%=)
    • __ipow__(self, other) – In-place exponentiation (**=)
    • __imatmul__(self, other) – In-place matrix multiplication (@=)

    Unary Operators and Functions

    __neg__(self) – Unary Negation (-)

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __neg__(self):
            return Vector(-self.x, -self.y)
    
        def __repr__(self):
            return f"Vector({self.x}, {self.y})"
    
    v = Vector(3, 4)
    print(-v)  # Vector(-3, -4)
    Python

    __pos__(self) – Unary Plus (+)

    def __pos__(self):
        return Vector(+self.x, +self.y)
    Python

    __abs__(self) – Absolute Value

    import math
    
    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __abs__(self):
            return math.sqrt(self.x**2 + self.y**2)
    
    v = Vector(3, 4)
    print(abs(v))  # 5.0
    Python

    __invert__(self) – Bitwise NOT (~)

    class BinaryNumber:
        def __init__(self, value):
            self.value = value
    
        def __invert__(self):
            return BinaryNumber(~self.value)
    
        def __repr__(self):
            return f"BinaryNumber({self.value})"
    
    b = BinaryNumber(5)
    print(~b)  # BinaryNumber(-6)
    Python

    __round__(self, n=0) – Rounding

    class Decimal:
        def __init__(self, value):
            self.value = value
    
        def __round__(self, n=0):
            return Decimal(round(self.value, n))
    
    d = Decimal(3.14159)
    print(round(d, 2))  # Decimal(3.14)
    Python

    __floor__(self) – Floor Function

    import math
    
    class Decimal:
        def __init__(self, value):
            self.value = value
    
        def __floor__(self):
            return Decimal(math.floor(self.value))
    Python

    __ceil__(self) – Ceiling Function

    def __ceil__(self):
        return Decimal(math.ceil(self.value))
    Python

    __trunc__(self) – Truncation

    def __trunc__(self):
        return Decimal(math.trunc(self.value))
    Python

    Type Conversion Magic Methods

    __int__(self) – Convert to Integer

    class Fraction:
        def __init__(self, numerator, denominator):
            self.numerator = numerator
            self.denominator = denominator
    
        def __int__(self):
            return self.numerator // self.denominator
    
    f = Fraction(7, 2)
    print(int(f))  # 3
    Python

    __float__(self) – Convert to Float

    def __float__(self):
        return self.numerator / self.denominator
    
    f = Fraction(7, 2)
    print(float(f))  # 3.5
    Python

    __complex__(self) – Convert to Complex

    class ComplexNumber:
        def __init__(self, real, imag):
            self.real = real
            self.imag = imag
    
        def __complex__(self):
            return complex(self.real, self.imag)
    Python

    __bool__(self) – Convert to Boolean

    class Vector:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __bool__(self):
            return self.x != 0 or self.y != 0
    
    v1 = Vector(0, 0)
    v2 = Vector(1, 2)
    print(bool(v1))  # False
    print(bool(v2))  # True
    
    if v2:
        print("Vector is non-zero")
    Python

    __bytes__(self) – Convert to Bytes

    class Data:
        def __init__(self, value):
            self.value = value
    
        def __bytes__(self):
            return str(self.value).encode('utf-8')
    
    d = Data("Hello")
    print(bytes(d))  # b'Hello'
    Python

    __format__(self, format_spec) – Custom Formatting

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __format__(self, format_spec):
            if format_spec == 'polar':
                import math
                r = math.sqrt(self.x**2 + self.y**2)
                theta = math.atan2(self.y, self.x)
                return f"(r={r:.2f}, θ={theta:.2f})"
            elif format_spec == 'cartesian' or format_spec == '':
                return f"({self.x}, {self.y})"
            else:
                raise ValueError(f"Unknown format: {format_spec}")
    
    p = Point(3, 4)
    print(f"{p}")           # (3, 4)
    print(f"{p:cartesian}") # (3, 4)
    print(f"{p:polar}")     # (r=5.00, θ=0.93)
    Python

    __hash__(self) – Make Object Hashable

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __eq__(self, other):
            if not isinstance(other, Point):
                return NotImplemented
            return self.x == other.x and self.y == other.y
    
        def __hash__(self):
            return hash((self.x, self.y))
    
    # Now can be used in sets and as dict keys
    points = {Point(1, 2), Point(3, 4), Point(1, 2)}
    print(len(points))  # 2 (duplicates removed)
    
    point_data = {Point(1, 2): "Origin-ish", Point(3, 4): "Away"}
    Python

    __index__(self) – Convert to Index

    class Index:
        def __init__(self, value):
            self.value = value
    
        def __index__(self):
            return self.value
    
    idx = Index(2)
    my_list = [10, 20, 30, 40]
    print(my_list[idx])  # 30
    Python

    Container Magic Methods

    __len__(self) – Length

    class Playlist:
        def __init__(self):
            self.songs = []
    
        def add_song(self, song):
            self.songs.append(song)
    
        def __len__(self):
            return len(self.songs)
    
    playlist = Playlist()
    playlist.add_song("Song 1")
    playlist.add_song("Song 2")
    print(len(playlist))  # 2
    Python

    __getitem__(self, key) – Indexing and Slicing

    class Playlist:
        def __init__(self):
            self.songs = []
    
        def add_song(self, song):
            self.songs.append(song)
    
        def __getitem__(self, index):
            return self.songs[index]
    
        def __len__(self):
            return len(self.songs)
    
    playlist = Playlist()
    playlist.add_song("Song 1")
    playlist.add_song("Song 2")
    playlist.add_song("Song 3")
    
    print(playlist[0])      # Song 1
    print(playlist[-1])     # Song 3
    print(playlist[0:2])    # ['Song 1', 'Song 2']
    
    # Also enables iteration
    for song in playlist:
        print(song)
    Python

    __setitem__(self, key, value) – Item Assignment

    class Playlist:
        def __init__(self):
            self.songs = []
    
        def __getitem__(self, index):
            return self.songs[index]
    
        def __setitem__(self, index, value):
            self.songs[index] = value
    
        def __len__(self):
            return len(self.songs)
    
    playlist = Playlist()
    playlist.songs = ["Song 1", "Song 2", "Song 3"]
    playlist[1] = "New Song 2"
    print(playlist[1])  # New Song 2
    Python

    __delitem__(self, key) – Item Deletion

    def __delitem__(self, index):
        del self.songs[index]
    
    playlist = Playlist()
    playlist.songs = ["Song 1", "Song 2", "Song 3"]
    del playlist[1]
    print(len(playlist.songs))  # 2
    Python

    __contains__(self, item) – Membership Test (in)

    class Playlist:
        def __init__(self):
            self.songs = []
    
        def add_song(self, song):
            self.songs.append(song)
    
        def __contains__(self, song):
            return song in self.songs
    
    playlist = Playlist()
    playlist.add_song("Song 1")
    playlist.add_song("Song 2")
    
    print("Song 1" in playlist)  # True
    print("Song 3" in playlist)  # False
    Python

    __iter__(self) – Iterator

    class Countdown:
        def __init__(self, start):
            self.start = start
    
        def __iter__(self):
            self.current = self.start
            return self
    
        def __next__(self):
            if self.current <= 0:
                raise StopIteration
            self.current -= 1
            return self.current + 1
    
    for num in Countdown(5):
        print(num)  # 5, 4, 3, 2, 1
    Python

    __reversed__(self) – Reverse Iteration

    class Playlist:
        def __init__(self, songs):
            self.songs = songs
    
        def __reversed__(self):
            return reversed(self.songs)
    
        def __iter__(self):
            return iter(self.songs)
    
    playlist = Playlist(["Song 1", "Song 2", "Song 3"])
    for song in reversed(playlist):
        print(song)  # Song 3, Song 2, Song 1
    Python

    __missing__(self, key) – Handling Missing Keys (for dict subclasses)

    class DefaultDict(dict):
        def __init__(self, default_value):
            super().__init__()
            self.default_value = default_value
    
        def __missing__(self, key):
            # Called when key is not found
            self[key] = self.default_value
            return self.default_value
    
    dd = DefaultDict(0)
    dd['a'] = 5
    print(dd['a'])  # 5
    print(dd['b'])  # 0 (auto-created)
    print(dd)       # {'a': 5, 'b': 0}
    Python

    Attribute Access Magic Methods

    __getattr__(self, name) – Attribute Access

    Called when an attribute is not found through normal lookup.

    class DynamicAttributes:
        def __init__(self):
            self.data = {'name': 'Alice', 'age': 30}
    
        def __getattr__(self, name):
            # Only called if attribute doesn't exist normally
            if name in self.data:
                return self.data[name]
            raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
    
    obj = DynamicAttributes()
    print(obj.name)  # Alice
    print(obj.age)   # 30
    # print(obj.missing)  # Raises AttributeError
    Python

    __setattr__(self, name, value) – Attribute Assignment

    Called on every attribute assignment.

    class ValidatedAttributes:
        def __setattr__(self, name, value):
            if name == 'age' and (not isinstance(value, int) or value < 0):
                raise ValueError("Age must be a non-negative integer")
            # Use super() or __dict__ to avoid infinite recursion
            super().__setattr__(name, value)
    
    obj = ValidatedAttributes()
    obj.name = "Alice"
    obj.age = 30
    # obj.age = -5  # Raises ValueError
    # obj.age = "thirty"  # Raises ValueError
    Python

    __delattr__(self, name) – Attribute Deletion

    class ProtectedAttributes:
        def __init__(self):
            self.public = "Can delete"
            self._protected = "Cannot delete"
    
        def __delattr__(self, name):
            if name.startswith('_'):
                raise AttributeError(f"Cannot delete protected attribute '{name}'")
            super().__delattr__(name)
    
    obj = ProtectedAttributes()
    del obj.public  # OK
    # del obj._protected  # Raises AttributeError
    Python

    __getattribute__(self, name) – Unconditional Attribute Access

    Called for every attribute access. Use with caution to avoid infinite recursion.

    class LoggedAccess:
        def __init__(self):
            super().__setattr__('_log', [])
            super().__setattr__('value', 42)
    
        def __getattribute__(self, name):
            # Avoid logging access to _log itself
            if name != '_log':
                log = super().__getattribute__('_log')
                log.append(f"Accessed: {name}")
            return super().__getattribute__(name)
    
    obj = LoggedAccess()
    print(obj.value)  # 42
    print(obj._log)   # ['Accessed: value']
    Python

    __dir__(self) – Directory Listing

    class CustomDir:
        def __init__(self):
            self.x = 1
            self.y = 2
            self._hidden = 3
    
        def __dir__(self):
            # Customize what dir() shows
            return ['x', 'y', 'custom_method']
    
    obj = CustomDir()
    print(dir(obj))  # Includes 'x', 'y', 'custom_method'
    Python

    Callable Objects

    __call__(self, *args, **kwargs) – Make Object Callable

    class Multiplier:
        def __init__(self, factor):
            self.factor = factor
    
        def __call__(self, value):
            return value * self.factor
    
    double = Multiplier(2)
    triple = Multiplier(3)
    
    print(double(5))  # 10
    print(triple(5))  # 15
    
    # Can be used as a function
    numbers = [1, 2, 3, 4]
    doubled = list(map(double, numbers))
    print(doubled)  # [2, 4, 6, 8]
    Python

    Function Decorators Using __call__

    class CountCalls:
        def __init__(self, func):
            self.func = func
            self.count = 0
    
        def __call__(self, *args, **kwargs):
            self.count += 1
            print(f"Call {self.count} of {self.func.__name__}")
            return self.func(*args, **kwargs)
    
    @CountCalls
    def greet(name):
        return f"Hello, {name}!"
    
    print(greet("Alice"))  # Call 1 of greet
                           # Hello, Alice!
    print(greet("Bob"))    # Call 2 of greet
                           # Hello, Bob!
    Python

    Stateful Callable Objects

    class RunningAverage:
        def __init__(self):
            self.total = 0
            self.count = 0
    
        def __call__(self, value):
            self.total += value
            self.count += 1
            return self.total / self.count
    
    avg = RunningAverage()
    print(avg(10))  # 10.0
    print(avg(20))  # 15.0
    print(avg(30))  # 20.0
    Python

    Context Managers

    __enter__(self) and __exit__(self, exc_type, exc_val, exc_tb)

    Used with the with statement for resource management.

    class FileManager:
        def __init__(self, filename, mode):
            self.filename = filename
            self.mode = mode
            self.file = None
    
        def __enter__(self):
            print(f"Opening {self.filename}")
            self.file = open(self.filename, self.mode)
            return self.file
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            print(f"Closing {self.filename}")
            if self.file:
                self.file.close()
            # Return False to propagate exceptions, True to suppress
            if exc_type is not None:
                print(f"Exception occurred: {exc_type.__name__}: {exc_val}")
            return False  # Don't suppress exceptions
    
    # Usage
    with FileManager('test.txt', 'w') as f:
        f.write("Hello, World!")
    # File is automatically closed
    Python

    Database Transaction Context Manager

    class DatabaseTransaction:
        def __init__(self, connection):
            self.connection = connection
    
        def __enter__(self):
            self.connection.begin_transaction()
            return self.connection
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is None:
                # No exception, commit
                self.connection.commit()
            else:
                # Exception occurred, rollback
                self.connection.rollback()
            return False  # Propagate exceptions
    
    # with DatabaseTransaction(db_conn) as conn:
    #     conn.execute("INSERT INTO ...")
    Python

    Timer Context Manager

    import time
    
    class Timer:
        def __enter__(self):
            self.start = time.time()
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            self.end = time.time()
            self.elapsed = self.end - self.start
            print(f"Elapsed time: {self.elapsed:.4f} seconds")
            return False
    
    with Timer():
        # Code to time
        sum(range(1000000))
    # Elapsed time: 0.0234 seconds
    Python

    Suppressing Exceptions

    class SuppressException:
        def __init__(self, *exceptions):
            self.exceptions = exceptions
    
        def __enter__(self):
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            # Return True to suppress the exception
            return exc_type is not None and issubclass(exc_type, self.exceptions)
    
    with SuppressException(FileNotFoundError, PermissionError):
        with open('nonexistent.txt', 'r') as f:
            data = f.read()
    print("Continued execution")  # This runs even if file doesn't exist
    Python

    Descriptor Protocol

    Descriptors control attribute access on other objects. They implement __get__, __set__, and/or __delete__.

    __get__(self, instance, owner) – Attribute Retrieval

    class Descriptor:
        def __init__(self, name):
            self.name = name
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return instance.__dict__.get(self.name, None)
    
        def __set__(self, instance, value):
            instance.__dict__[self.name] = value
    
    class MyClass:
        attr = Descriptor('attr')
    
    obj = MyClass()
    obj.attr = 42
    print(obj.attr)  # 42
    Python

    Validated Descriptor

    class TypedProperty:
        def __init__(self, name, expected_type):
            self.name = name
            self.expected_type = expected_type
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return instance.__dict__.get(self.name)
    
        def __set__(self, instance, value):
            if not isinstance(value, self.expected_type):
                raise TypeError(
                    f"{self.name} must be {self.expected_type.__name__}, "
                    f"got {type(value).__name__}"
                )
            instance.__dict__[self.name] = value
    
        def __delete__(self, instance):
            del instance.__dict__[self.name]
    
    class Person:
        name = TypedProperty('name', str)
        age = TypedProperty('age', int)
    
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    p = Person("Alice", 30)
    print(p.name, p.age)  # Alice 30
    # p.age = "thirty"  # Raises TypeError
    Python

    Read-Only Property Descriptor

    class ReadOnly:
        def __init__(self, name):
            self.name = name
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return instance.__dict__.get(self.name)
    
        def __set__(self, instance, value):
            if self.name in instance.__dict__:
                raise AttributeError(f"Cannot modify read-only attribute '{self.name}'")
            instance.__dict__[self.name] = value
    
    class Config:
        api_key = ReadOnly('api_key')
    
        def __init__(self, api_key):
            self.api_key = api_key
    
    config = Config("secret123")
    print(config.api_key)  # secret123
    # config.api_key = "new"  # Raises AttributeError
    Python

    Lazy Property Descriptor

    class LazyProperty:
        def __init__(self, func):
            self.func = func
            self.name = func.__name__
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
    
            # Compute value and cache it
            value = self.func(instance)
            setattr(instance, self.name, value)
            return value
    
    class DataProcessor:
        def __init__(self, data):
            self.data = data
    
        @LazyProperty
        def processed_data(self):
            print("Processing data...")
            return [x * 2 for x in self.data]
    
    dp = DataProcessor([1, 2, 3, 4])
    print(dp.processed_data)  # Processing data... [2, 4, 6, 8]
    print(dp.processed_data)  # [2, 4, 6, 8] (no reprocessing)
    Python

    Copying and Pickling

    __copy__(self) – Shallow Copy

    import copy
    
    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __copy__(self):
            print("Creating shallow copy")
            return Point(self.x, self.y)
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
    p1 = Point(1, 2)
    p2 = copy.copy(p1)
    print(p2)  # Point(1, 2)
    Python

    __deepcopy__(self, memo) – Deep Copy

    class Node:
        def __init__(self, value, children=None):
            self.value = value
            self.children = children or []
    
        def __deepcopy__(self, memo):
            print(f"Deep copying node {self.value}")
            # Create new instance
            new_node = Node(self.value)
            # Deep copy children
            new_node.children = copy.deepcopy(self.children, memo)
            return new_node
    
        def __repr__(self):
            return f"Node({self.value})"
    
    root = Node(1, [Node(2), Node(3)])
    root_copy = copy.deepcopy(root)
    Python

    __getstate__(self) and __setstate__(self, state) – Pickling

    import pickle
    
    class Connection:
        def __init__(self, host, port):
            self.host = host
            self.port = port
            self.socket = None  # Not picklable
    
        def __getstate__(self):
            # Return state to pickle (exclude socket)
            state = self.__dict__.copy()
            state['socket'] = None
            return state
    
        def __setstate__(self, state):
            # Restore state from pickle
            self.__dict__.update(state)
            # Reconnect if needed
            self.reconnect()
    
        def reconnect(self):
            print(f"Reconnecting to {self.host}:{self.port}")
    
    conn = Connection("localhost", 8080)
    data = pickle.dumps(conn)
    conn2 = pickle.loads(data)  # Reconnecting to localhost:8080
    Python

    __reduce__(self) – Advanced Pickling

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __reduce__(self):
            # Return (callable, args) to recreate object
            return (Point, (self.x, self.y))
    
    p1 = Point(3, 4)
    data = pickle.dumps(p1)
    p2 = pickle.loads(data)
    print(p2.x, p2.y)  # 3 4
    Python

    String Representation

    __repr__(self) – Official String Representation

    Should return a string that ideally could recreate the object.

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __repr__(self):
            return f"Point({self.x}, {self.y})"
    
    p = Point(3, 4)
    print(repr(p))  # Point(3, 4)
    print(p)        # Point(3, 4)
    Python

    __str__(self) – Informal String Representation

    User-friendly string representation.

    class Person:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __repr__(self):
            return f"Person(name={self.name!r}, age={self.age!r})"
    
        def __str__(self):
            return f"{self.name}, {self.age} years old"
    
    p = Person("Alice", 30)
    print(str(p))   # Alice, 30 years old
    print(repr(p))  # Person(name='Alice', age=30)
    print(p)        # Alice, 30 years old (uses __str__)
    Python

    __bytes__(self) – Bytes Representation

    class Data:
        def __init__(self, content):
            self.content = content
    
        def __bytes__(self):
            return self.content.encode('utf-8')
    
        def __str__(self):
            return self.content
    
    d = Data("Hello")
    print(bytes(d))  # b'Hello'
    Python

    Advanced Magic Methods

    Bitwise Operators

    __and__(self, other) – Bitwise AND (&)

    class BitSet:
        def __init__(self, value):
            self.value = value
    
        def __and__(self, other):
            if isinstance(other, BitSet):
                return BitSet(self.value & other.value)
            return NotImplemented
    
        def __repr__(self):
            return f"BitSet({bin(self.value)})"
    
    b1 = BitSet(0b1100)
    b2 = BitSet(0b1010)
    print(b1 & b2)  # BitSet(0b1000)
    Python

    __or__(self, other) – Bitwise OR (|)

    def __or__(self, other):
        if isinstance(other, BitSet):
            return BitSet(self.value | other.value)
        return NotImplemented
    Python

    __xor__(self, other) – Bitwise XOR (^)

    def __xor__(self, other):
        if isinstance(other, BitSet):
            return BitSet(self.value ^ other.value)
        return NotImplemented
    Python

    __lshift__(self, other) – Left Shift (<<)

    def __lshift__(self, n):
        return BitSet(self.value << n)
    Python

    __rshift__(self, other) – Right Shift (>>)

    def __rshift__(self, n):
        return BitSet(self.value >> n)
    Python

    Reflected and In-place Bitwise Operators

    Similar patterns exist for:

    • __rand__, __ror__, __rxor__, __rlshift__, __rrshift__ (reflected)
    • __iand__, __ior__, __ixor__, __ilshift__, __irshift__ (in-place)

    __sizeof__(self) – Memory Size

    import sys
    
    class CustomList:
        def __init__(self, items):
            self.items = items
    
        def __sizeof__(self):
            # Return size in bytes
            return sys.getsizeof(self.items) + sys.getsizeof(self.__dict__)
    
    cl = CustomList([1, 2, 3, 4, 5])
    print(sys.getsizeof(cl))  # Uses __sizeof__
    Python

    __init_subclass__(cls, **kwargs) – Subclass Initialization

    Called when a class is subclassed.

    class PluginBase:
        plugins = []
    
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            cls.plugins.append(cls)
            print(f"Registered plugin: {cls.__name__}")
    
    class Plugin1(PluginBase):
        pass  # Registered plugin: Plugin1
    
    class Plugin2(PluginBase):
        pass  # Registered plugin: Plugin2
    
    print(PluginBase.plugins)  # [<class 'Plugin1'>, <class 'Plugin2'>]
    Python

    __class_getitem__(cls, item) – Generic Class Syntax

    Enables Class[type] syntax (like list[int] in type hints).

    class GenericList:
        def __class_getitem__(cls, item):
            print(f"Creating generic list of {item}")
            return cls
    
    # Works with type hints
    MyList = GenericList[int]  # Creating generic list of <class 'int'>
    Python

    __prepare__(metacls, name, bases, **kwargs) – Metaclass Namespace

    Returns the namespace dict for a new class.

    class OrderedMeta(type):
        @classmethod
        def __prepare__(metacls, name, bases, **kwargs):
            # Return OrderedDict to preserve definition order (not needed in Python 3.7+)
            return {}
    
        def __new__(metacls, name, bases, namespace, **kwargs):
            result = super().__new__(metacls, name, bases, dict(namespace))
            result.members = list(namespace.keys())
            return result
    
    class MyClass(metaclass=OrderedMeta):
        a = 1
        b = 2
        c = 3
    
    print(MyClass.members)  # ['__module__', '__qualname__', 'a', 'b', 'c']
    Python

    __instancecheck__(self, instance) – Custom isinstance()

    class AcceptAnything(type):
        def __instancecheck__(cls, instance):
            return True
    
    class MyClass(metaclass=AcceptAnything):
        pass
    
    print(isinstance(42, MyClass))      # True
    print(isinstance("hello", MyClass)) # True
    Python

    __subclasscheck__(self, subclass) – Custom issubclass()

    class MyMeta(type):
        def __subclasscheck__(cls, subclass):
            # Custom logic for subclass checking
            return hasattr(subclass, 'special_method')
    
    class MyBase(metaclass=MyMeta):
        pass
    
    class MySubclass:
        def special_method(self):
            pass
    
    print(issubclass(MySubclass, MyBase))  # True
    Python

    __set_name__(self, owner, name) – Descriptor Name Assignment

    Called when a descriptor is assigned to a class attribute.

    class NamedDescriptor:
        def __set_name__(self, owner, name):
            self.name = name
            self.private_name = '_' + name
    
        def __get__(self, instance, owner):
            if instance is None:
                return self
            return getattr(instance, self.private_name, None)
    
        def __set__(self, instance, value):
            setattr(instance, self.private_name, value)
    
    class MyClass:
        attr = NamedDescriptor()
    
    obj = MyClass()
    obj.attr = 42
    print(obj.attr)  # 42
    print(obj._attr) # 42
    Python

    Practical Examples

    Example 1: Complex Number Class

    import math
    
    class Complex:
        def __init__(self, real, imag):
            self.real = real
            self.imag = imag
    
        def __add__(self, other):
            if isinstance(other, Complex):
                return Complex(self.real + other.real, self.imag + other.imag)
            elif isinstance(other, (int, float)):
                return Complex(self.real + other, self.imag)
            return NotImplemented
    
        def __sub__(self, other):
            if isinstance(other, Complex):
                return Complex(self.real - other.real, self.imag - other.imag)
            elif isinstance(other, (int, float)):
                return Complex(self.real - other, self.imag)
            return NotImplemented
    
        def __mul__(self, other):
            if isinstance(other, Complex):
                real = self.real * other.real - self.imag * other.imag
                imag = self.real * other.imag + self.imag * other.real
                return Complex(real, imag)
            elif isinstance(other, (int, float)):
                return Complex(self.real * other, self.imag * other)
            return NotImplemented
    
        def __abs__(self):
            return math.sqrt(self.real**2 + self.imag**2)
    
        def __eq__(self, other):
            if isinstance(other, Complex):
                return self.real == other.real and self.imag == other.imag
            return NotImplemented
    
        def __repr__(self):
            sign = '+' if self.imag >= 0 else '-'
            return f"{self.real}{sign}{abs(self.imag)}j"
    
        def __str__(self):
            return repr(self)
    
    # Usage
    c1 = Complex(3, 4)
    c2 = Complex(1, 2)
    print(c1 + c2)   # 4+6j
    print(c1 * c2)   # -5+10j
    print(abs(c1))   # 5.0
    Python

    Example 2: Custom Dictionary with Default Values

    class DefaultDict(dict):
        def __init__(self, default_factory, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.default_factory = default_factory
    
        def __missing__(self, key):
            if self.default_factory is None:
                raise KeyError(key)
            self[key] = value = self.default_factory()
            return value
    
        def __repr__(self):
            return f"DefaultDict({self.default_factory}, {dict.__repr__(self)})"
    
    # Usage
    dd = DefaultDict(list)
    dd['fruits'].append('apple')
    dd['fruits'].append('banana')
    dd['vegetables'].append('carrot')
    print(dd)  # DefaultDict(<class 'list'>, {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']})
    Python

    Example 3: Chainable Query Builder

    class Query:
        def __init__(self, items=None):
            self.items = items or []
    
        def filter(self, predicate):
            return Query([item for item in self.items if predicate(item)])
    
        def map(self, func):
            return Query([func(item) for item in self.items])
    
        def __iter__(self):
            return iter(self.items)
    
        def __len__(self):
            return len(self.items)
    
        def __getitem__(self, index):
            return self.items[index]
    
        def __repr__(self):
            return f"Query({self.items})"
    
    # Usage
    data = Query([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    result = data.filter(lambda x: x % 2 == 0).map(lambda x: x ** 2)
    print(list(result))  # [4, 16, 36, 64, 100]
    Python

    Example 4: Fluent Interface Builder

    class EmailBuilder:
        def __init__(self):
            self._to = None
            self._subject = None
            self._body = None
    
        def to(self, recipient):
            self._to = recipient
            return self
    
        def subject(self, subject):
            self._subject = subject
            return self
    
        def body(self, body):
            self._body = body
            return self
    
        def __str__(self):
            return f"To: {self._to}\nSubject: {self._subject}\n\n{self._body}"
    
        def __repr__(self):
            return f"EmailBuilder(to={self._to!r}, subject={self._subject!r})"
    
    # Usage
    email = (EmailBuilder()
             .to("user@example.com")
             .subject("Hello")
             .body("This is a test email"))
    print(email)
    Python

    Example 5: Resource Pool with Context Manager

    from queue import Queue
    import time
    
    class ConnectionPool:
        def __init__(self, size):
            self.size = size
            self.pool = Queue(maxsize=size)
            for i in range(size):
                self.pool.put(f"Connection-{i}")
    
        def acquire(self):
            print("Acquiring connection...")
            return self.pool.get()
    
        def release(self, connection):
            print(f"Releasing {connection}")
            self.pool.put(connection)
    
        def __enter__(self):
            self.connection = self.acquire()
            return self.connection
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            self.release(self.connection)
            return False
    
    # Usage
    pool = ConnectionPool(3)
    
    with pool as conn:
        print(f"Using {conn}")
        time.sleep(0.1)
    # Connection automatically released
    Python

    Best Practices

    1. Always Return NotImplemented for Unsupported Operations

    def __add__(self, other):
        if isinstance(other, MyClass):
            # Implementation
            pass
        return NotImplemented  # Not TypeError or NotImplementedError
    Python

    2. Implement Both __eq__ and __hash__ for Hashable Objects

    class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __eq__(self, other):
            if not isinstance(other, Point):
                return NotImplemented
            return self.x == other.x and self.y == other.y
    
        def __hash__(self):
            return hash((self.x, self.y))
    Python

    3. Make __repr__ Unambiguous and Useful

    # Good
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"
    
    # Better (can recreate object)
    def __repr__(self):
        return f"Point({self.x}, {self.y})"
    Python

    4. Use __str__ for User-Friendly Output

    def __repr__(self):
        return f"Person(name={self.name!r}, age={self.age})"
    
    def __str__(self):
        return f"{self.name} ({self.age} years old)"
    Python

    5. Be Careful with __getattribute__

    Avoid infinite recursion by using super().__getattribute__():

    def __getattribute__(self, name):
        # Don't do: return self.something (infinite recursion)
        # Do:
        return super().__getattribute__(name)
    Python

    6. Context Managers Should Always Clean Up

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Always clean up, even if exception occurred
        self.cleanup()
        return False  # Don't suppress exceptions unless intended
    Python

    7. Make Augmented Assignment Return self

    def __iadd__(self, other):
        # Modify in place
        self.value += other.value
        return self  # Must return self
    Python

    8. Use Type Checking

    def __add__(self, other):
        if not isinstance(other, MyClass):
            return NotImplemented
        # Implementation
    Python

    9. Document Magic Method Behavior

    class Vector:
        """A 2D vector class.
    
        Supports addition, subtraction, multiplication by scalar,
        and comparison operations.
        """
    
        def __add__(self, other):
            """Add two vectors component-wise."""
            # Implementation
    Python

    10. Consider Using @functools.total_ordering

    from functools import total_ordering
    
    @total_ordering
    class Version:
        def __eq__(self, other):
            # Implementation
    
        def __lt__(self, other):
            # Implementation
    
        # Other comparison methods auto-generated
    Python

    Common Pitfalls

    1. Modifying Mutable Default Arguments

    # Bad
    def __init__(self, items=[]):
        self.items = items  # All instances share same list!
    
    # Good
    def __init__(self, items=None):
        self.items = items if items is not None else []
    Python

    2. Forgetting to Return self in Augmented Assignment

    # Bad
    def __iadd__(self, other):
        self.value += other.value
        # Forgot to return self!
    
    # Good
    def __iadd__(self, other):
        self.value += other.value
        return self
    Python

    3. Raising Wrong Exception Types

    # Bad
    def __add__(self, other):
        if not isinstance(other, MyClass):
            raise TypeError("Cannot add")
    
    # Good
    def __add__(self, other):
        if not isinstance(other, MyClass):
            return NotImplemented
    Python

    4. Making Objects Unhashable Unintentionally

    class Point:
        def __eq__(self, other):
            return self.x == other.x and self.y == other.y
        # Forgot __hash__! Now Point is unhashable
        # Python sets __hash__ = None when __eq__ is defined
    Python

    5. Infinite Recursion in __setattr__

    # Bad
    def __setattr__(self, name, value):
        self.name = value  # Infinite recursion!
    
    # Good
    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        # Or: self.__dict__[name] = value
    Python

    Performance Optimization

    __slots__ – Memory Optimization

    Reduce memory footprint by 40-50% and speed up attribute access.

    from typing import ClassVar
    import sys
    
    class WithoutSlots:
        def __init__(self, x: int, y: int) -> None:
            self.x = x
            self.y = y
    
    class WithSlots:
        __slots__ = ('x', 'y')  # Define allowed attributes
    
        def __init__(self, x: int, y: int) -> None:
            self.x = x
            self.y = y
    
    # Memory comparison
    w1 = WithoutSlots(1, 2)
    w2 = WithSlots(1, 2)
    
    print(sys.getsizeof(w1.__dict__))  # ~240 bytes
    print(sys.getsizeof(w2))           # ~64 bytes
    
    # Performance: attribute access is 10-15% faster with slots
    Python

    Slots Best Practices:

    from typing import ClassVar, Any
    
    class OptimizedClass:
        """Production-ready slots usage."""
        __slots__ = ('_x', '_y', '__weakref__')  # Include __weakref__ for weak references
        _cache: ClassVar[dict] = {}  # Class variables need ClassVar
    
        def __init__(self, x: int, y: int) -> None:
            self._x = x
            self._y = y
    
        def __getstate__(self) -> dict[str, Any]:
            """Needed for pickling with slots."""
            return {slot: getattr(self, slot) for slot in self.__slots__ if hasattr(self, slot)}
    
        def __setstate__(self, state: dict[str, Any]) -> None:
            """Needed for unpickling with slots."""
            for slot, value in state.items():
                setattr(self, slot, value)
    Python

    Lazy Evaluation Pattern

    from typing import Optional, Callable, Any
    from functools import wraps
    
    class LazyAttribute:
        """Descriptor for lazy evaluation with caching."""
    
        def __init__(self, func: Callable) -> None:
            self.func = func
            self.name = func.__name__
            wraps(func)(self)
    
        def __get__(self, instance: Any, owner: type) -> Any:
            if instance is None:
                return self
    
            # Compute and cache
            value = self.func(instance)
            setattr(instance, self.name, value)  # Replace descriptor with value
            return value
    
    class DataAnalyzer:
        def __init__(self, data: list[int]) -> None:
            self.data = data
    
        @LazyAttribute
        def expensive_calculation(self) -> float:
            """Only computed when first accessed."""
            print("Computing...")
            return sum(x**2 for x in self.data) / len(self.data)
    
    analyzer = DataAnalyzer([1, 2, 3, 4, 5])
    print(analyzer.expensive_calculation)  # Computing... 11.0
    print(analyzer.expensive_calculation)  # 11.0 (no recomputation)
    Python

    Type Hints & Protocols

    Structural Subtyping with Protocols

    from typing import Protocol, TypeVar, runtime_checkable
    from collections.abc import Iterator
    
    T = TypeVar('T', bound='Comparable')
    
    @runtime_checkable
    class Comparable(Protocol):
        """Protocol for comparable objects."""
    
        def __lt__(self, other: 'Comparable') -> bool: ...
        def __le__(self, other: 'Comparable') -> bool: ...
        def __gt__(self, other: 'Comparable') -> bool: ...
        def __ge__(self, other: 'Comparable') -> bool: ...
    
    def find_max(items: list[Comparable]) -> Comparable:
        """Works with any comparable type without inheritance."""
        return max(items)
    
    # Works with built-in types
    print(find_max([1, 5, 3]))  # 5
    print(find_max(['a', 'z', 'b']))  # 'z'
    
    # Works with custom types
    class Score:
        def __init__(self, value: int) -> None:
            self.value = value
    
        def __lt__(self, other: 'Score') -> bool:
            return self.value < other.value
    
        def __le__(self, other: 'Score') -> bool:
            return self.value <= other.value
    
        def __gt__(self, other: 'Score') -> bool:
            return self.value > other.value
    
        def __ge__(self, other: 'Score') -> bool:
            return self.value >= other.value
    
    print(isinstance(Score(10), Comparable))  # True (runtime check)
    Python

    Generic Magic Methods

    from typing import Generic, TypeVar, Callable
    from collections.abc import Iterator
    
    T = TypeVar('T')
    U = TypeVar('U')
    
    class Container(Generic[T]):
        """Generic container with type-safe magic methods."""
    
        def __init__(self, items: list[T]) -> None:
            self._items = items
    
        def __getitem__(self, index: int) -> T:
            return self._items[index]
    
        def __iter__(self) -> Iterator[T]:
            return iter(self._items)
    
        def __len__(self) -> int:
            return len(self._items)
    
        def map(self, func: Callable[[T], U]) -> 'Container[U]':
            """Type-safe map operation."""
            return Container([func(item) for item in self._items])
    
    # Type checker knows the types
    int_container: Container[int] = Container([1, 2, 3])
    str_container: Container[str] = int_container.map(str)
    print(str_container._items)  # ['1', '2', '3']
    Python

    Async Magic Methods

    Async Context Managers

    import asyncio
    from typing import Optional, Any
    
    class AsyncFileManager:
        """Async context manager for file operations."""
    
        def __init__(self, filename: str, mode: str = 'r') -> None:
            self.filename = filename
            self.mode = mode
            self.file: Optional[Any] = None
    
        async def __aenter__(self) -> Any:
            """Async version of __enter__."""
            print(f"Opening {self.filename}")
            # Simulating async file open
            await asyncio.sleep(0.1)
            self.file = open(self.filename, self.mode)
            return self.file
    
        async def __aexit__(
            self,
            exc_type: Optional[type],
            exc_val: Optional[Exception],
            exc_tb: Optional[Any]
        ) -> bool:
            """Async version of __exit__."""
            if self.file:
                self.file.close()
                print(f"Closed {self.filename}")
            return False
    
    # Usage
    async def process_file():
        async with AsyncFileManager('data.txt', 'w') as f:
            f.write("Hello, async world!")
    
    # asyncio.run(process_file())
    Python

    Async Iterators

    import asyncio
    from typing import AsyncIterator
    
    class AsyncRange:
        """Async iterator example."""
    
        def __init__(self, start: int, end: int, delay: float = 0.1) -> None:
            self.start = start
            self.end = end
            self.delay = delay
            self.current = start
    
        def __aiter__(self) -> 'AsyncRange':
            """Return async iterator."""
            return self
    
        async def __anext__(self) -> int:
            """Async version of __next__."""
            if self.current >= self.end:
                raise StopAsyncIteration
    
            await asyncio.sleep(self.delay)
            value = self.current
            self.current += 1
            return value
    
    # Usage
    async def main():
        async for num in AsyncRange(0, 5):
            print(num)
    
    # asyncio.run(main())
    Python

    Pattern Matching (Python 3.10+)

    __match_args__ for Structural Pattern Matching

    from dataclasses import dataclass
    from typing import Union
    
    @dataclass
    class Point:
        __match_args__ = ('x', 'y')  # Enable positional matching
        x: float
        y: float
    
    @dataclass
    class Circle:
        __match_args__ = ('center', 'radius')
        center: Point
        radius: float
    
    @dataclass
    class Rectangle:
        __match_args__ = ('top_left', 'width', 'height')
        top_left: Point
        width: float
        height: float
    
    Shape = Union[Circle, Rectangle, Point]
    
    def describe_shape(shape: Shape) -> str:
        """Use pattern matching with custom classes."""
        match shape:
            case Point(0, 0):
                return "Origin point"
            case Point(x, 0):
                return f"Point on X-axis at {x}"
            case Point(0, y):
                return f"Point on Y-axis at {y}"
            case Point(x, y):
                return f"Point at ({x}, {y})"
            case Circle(Point(0, 0), r):
                return f"Circle at origin with radius {r}"
            case Circle(center, radius):
                return f"Circle at {center} with radius {radius}"
            case Rectangle(Point(x, y), w, h):
                return f"Rectangle at ({x}, {y}) with size {w}x{h}"
            case _:
                return "Unknown shape"
    
    # Test pattern matching
    print(describe_shape(Point(0, 0)))           # Origin point
    print(describe_shape(Circle(Point(0, 0), 5))) # Circle at origin with radius 5
    Python

    Production Best Practices

    1. Defensive Magic Method Implementation

    from typing import Any
    import logging
    
    logger = logging.getLogger(__name__)
    
    class RobustClass:
        """Production-ready magic method implementation."""
    
        def __init__(self, value: int) -> None:
            self._value = value
    
        def __add__(self, other: Any) -> 'RobustClass':
            """Defensive addition with logging."""
            if not isinstance(other, (RobustClass, int, float)):
                logger.warning(f"Unsupported type for addition: {type(other)}")
                return NotImplemented
    
            try:
                if isinstance(other, RobustClass):
                    result = self._value + other._value
                else:
                    result = self._value + other
                return RobustClass(result)
            except (OverflowError, ValueError) as e:
                logger.error(f"Addition failed: {e}")
                raise
    
        def __repr__(self) -> str:
            """Safe repr that never raises."""
            try:
                return f"RobustClass({self._value!r})"
            except Exception as e:
                logger.error(f"__repr__ failed: {e}")
                return f"<RobustClass object at {hex(id(self))}>"
    
        def __eq__(self, other: Any) -> bool:
            """Type-safe equality."""
            if not isinstance(other, RobustClass):
                return NotImplemented
            return self._value == other._value
    
        def __hash__(self) -> int:
            """Consistent hash with __eq__."""
            return hash(self._value)
    Python

    2. Thread-Safe Magic Methods

    import threading
    from typing import Any
    from contextlib import contextmanager
    
    class ThreadSafeCounter:
        """Thread-safe counter with magic methods."""
    
        def __init__(self, initial: int = 0) -> None:
            self._value = initial
            self._lock = threading.RLock()  # Reentrant lock
    
        @contextmanager
        def _synchronized(self):
            """Context manager for thread-safe operations."""
            self._lock.acquire()
            try:
                yield
            finally:
                self._lock.release()
    
        def __iadd__(self, other: int) -> 'ThreadSafeCounter':
            with self._synchronized():
                self._value += other
            return self
    
        def __int__(self) -> int:
            with self._synchronized():
                return self._value
    
        def __repr__(self) -> str:
            with self._synchronized():
                return f"ThreadSafeCounter({self._value})"
    
    # Usage in multithreaded environment
    counter = ThreadSafeCounter()
    
    def increment():
        for _ in range(1000):
            counter += 1
    
    threads = [threading.Thread(target=increment) for _ in range(10)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    
    print(int(counter))  # 10000 (always correct due to locking)
    Python

    3. Comprehensive Documentation Standards

    from typing import Union
    
    class Vector:
        """
        Immutable 2D vector with comprehensive magic method implementation.
    
        This class implements the full suite of arithmetic, comparison,
        and container protocols for production use.
    
        Attributes:
            x: X-coordinate of the vector
            y: Y-coordinate of the vector
    
        Examples:
            >>> v1 = Vector(1, 2)
            >>> v2 = Vector(3, 4)
            >>> v3 = v1 + v2
            >>> print(v3)
            Vector(4, 6)
            >>> abs(v1)
            2.23606797749979
    
        Note:
            All operations return new Vector instances (immutable).
        """
    
        __slots__ = ('_x', '_y')
    
        def __init__(self, x: float, y: float) -> None:
            """
            Initialize a new Vector.
    
            Args:
                x: X-coordinate
                y: Y-coordinate
    
            Raises:
                TypeError: If x or y are not numeric
            """
            if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
                raise TypeError(f"Coordinates must be numeric, got {type(x)}, {type(y)}")
            object.__setattr__(self, '_x', float(x))
            object.__setattr__(self, '_y', float(y))
    
        def __add__(self, other: Union['Vector', tuple[float, float]]) -> 'Vector':
            """
            Add two vectors or a vector and a tuple.
    
            Args:
                other: Vector or (x, y) tuple to add
    
            Returns:
                New Vector representing the sum
    
            Raises:
                TypeError: If other is not a Vector or tuple
    
            Examples:
                >>> Vector(1, 2) + Vector(3, 4)
                Vector(4, 6)
            """
            if isinstance(other, Vector):
                return Vector(self._x + other._x, self._y + other._y)
            elif isinstance(other, tuple) and len(other) == 2:
                return Vector(self._x + other[0], self._y + other[1])
            return NotImplemented
    
        def __repr__(self) -> str:
            """Return unambiguous string representation."""
            return f"Vector({self._x}, {self._y})"
    Python

    Summary

    Summary

    Magic methods are powerful tools that allow you to:

    • Customize object behavior with Python’s built-in operations
    • Create intuitive APIs that feel natural to Python developers
    • Integrate seamlessly with Python’s standard library
    • Implement design patterns elegantly
    • Optimize performance with slots and lazy evaluation
    • Enable type safety with protocols and generics
    • Support async operations with async magic methods
    • Implement pattern matching with __match_args__

    Key categories:

    • Lifecycle: __init__, __new__, __del__, __post_init__
    • Representation: __repr__, __str__, __format__, __bytes__
    • Comparison: __eq__, __lt__, __gt__, __le__, __ge__, __ne__
    • Arithmetic: __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__
    • Containers: __len__, __getitem__, __setitem__, __contains__, __iter__, __reversed__
    • Attributes: __getattr__, __setattr__, __delattr__, __getattribute__, __dir__
    • Callables: __call__
    • Context Managers: __enter__, __exit__, __aenter__, __aexit__
    • Descriptors: __get__, __set__, __delete__, __set_name__
    • Async: __aiter__, __anext__, __aenter__, __aexit__
    • Memory: __slots__, __sizeof__
    • Pattern Matching: __match_args__

    Senior Engineer Checklist:

    • ✅ Return NotImplemented for unsupported operations
    • ✅ Implement __hash__ when implementing __eq__
    • ✅ Use super() to avoid infinite recursion
    • ✅ Add comprehensive type hints
    • ✅ Document all magic methods with examples
    • ✅ Test edge cases thoroughly (including thread safety)
    • ✅ Profile performance for hot paths
    • ✅ Use __slots__ for memory-intensive classes
    • ✅ Implement protocols for structural subtyping
    • ✅ Consider immutability (frozen dataclasses)
    • ✅ Use async variants for I/O-bound operations
    • ✅ Add logging for debugging
    • ✅ Implement defensive coding practices
    • ✅ Follow PEP 8 and type checking (mypy/pyright)

    Additional Resources:

    Mastering magic methods will help you write more Pythonic, elegant, performant, and maintainable code at scale!


    Quick Reference Card

    Object Lifecycle

    __new__(cls, ...)        # Object creation (before __init__)
    __init__(self, ...)      # Object initialization
    __del__(self)            # Object destruction (finalizer)
    __post_init__(self)      # Dataclass post-initialization
    Python

    String Representation

    __repr__(self)           # Official representation (for developers)
    __str__(self)            # Informal representation (for users)
    __format__(self, spec)   # Custom formatting (f-strings)
    __bytes__(self)          # Bytes representation
    Python

    Comparison Operators

    __eq__(self, other)      # ==
    __ne__(self, other)      # !=
    __lt__(self, other)      # <
    __le__(self, other)      # <=
    __gt__(self, other)      # >
    __ge__(self, other)      # >=
    __hash__(self)           # hash() - needed for sets/dicts
    Python

    Arithmetic Operators

    __add__(self, other)     # +
    __sub__(self, other)     # -
    __mul__(self, other)     # *
    __matmul__(self, other)  # @ (matrix multiplication)
    __truediv__(self, other) # /
    __floordiv__(self, other)# //
    __mod__(self, other)     # %
    __pow__(self, other)     # **
    __divmod__(self, other)  # divmod()
    Python

    Reflected Operators (right-hand side)

    __radd__, __rsub__, __rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rpow__
    Python

    Augmented Assignment

    __iadd__(self, other)    # +=
    __isub__(self, other)    # -=
    __imul__(self, other)    # *=
    __itruediv__(self, other)# /=
    __ifloordiv__(self, other)# //=
    __imod__(self, other)    # %=
    __ipow__(self, other)    # **=
    Python

    Unary Operators

    __neg__(self)            # -x
    __pos__(self)            # +x
    __abs__(self)            # abs(x)
    __invert__(self)         # ~x
    __round__(self, n)       # round(x, n)
    __floor__(self)          # math.floor(x)
    __ceil__(self)           # math.ceil(x)
    __trunc__(self)          # math.trunc(x)
    Python

    Bitwise Operators

    __and__(self, other)     # &
    __or__(self, other)      # |
    __xor__(self, other)     # ^
    __lshift__(self, other)  # <<
    __rshift__(self, other)  # >>
    Python

    Type Conversion

    __int__(self)            # int(x)
    __float__(self)          # float(x)
    __complex__(self)        # complex(x)
    __bool__(self)           # bool(x), if x:
    __index__(self)          # operator.index(x)
    Python

    Container Emulation

    __len__(self)            # len(x)
    __getitem__(self, key)   # x[key]
    __setitem__(self, key, value) # x[key] = value
    __delitem__(self, key)   # del x[key]
    __contains__(self, item) # item in x
    __iter__(self)           # for i in x:
    __reversed__(self)       # reversed(x)
    __missing__(self, key)   # dict subclass, missing key
    Python

    Attribute Access

    __getattr__(self, name)  # x.name (when not found normally)
    __setattr__(self, name, value) # x.name = value
    __delattr__(self, name)  # del x.name
    __getattribute__(self, name)  # x.name (always called)
    __dir__(self)            # dir(x)
    Python

    Descriptors

    __get__(self, instance, owner)   # Descriptor getter
    __set__(self, instance, value)   # Descriptor setter
    __delete__(self, instance)       # Descriptor deleter
    __set_name__(self, owner, name)  # Descriptor name assignment
    Python

    Callable Objects

    __call__(self, ...)      # x(args)
    Python

    Context Managers

    __enter__(self)          # with x:
    __exit__(self, exc_type, exc_val, exc_tb)  # End of with block
    Python

    Async Context Managers & Iterators

    __aenter__(self)         # async with x:
    __aexit__(self, ...)     # End of async with block
    __aiter__(self)          # async for i in x:
    __anext__(self)          # Next item in async iteration
    Python

    Copying & Serialization

    __copy__(self)           # copy.copy(x)
    __deepcopy__(self, memo) # copy.deepcopy(x)
    __getstate__(self)       # pickle.dump(x)
    __setstate__(self, state)# pickle.load()
    __reduce__(self)         # Advanced pickle control
    __reduce_ex__(self, protocol) # Protocol-specific pickling
    Python

    Memory & Introspection

    __sizeof__(self)         # sys.getsizeof(x)
    __slots__                # Class attribute for memory optimization
    Python

    Pattern Matching (3.10+)

    __match_args__           # Class attribute for positional matching
    Python

    Metaclass Methods

    __init_subclass__(cls, **kwargs)  # Called when subclassed
    __class_getitem__(cls, item)      # Class[Type] syntax
    __prepare__(mcs, name, bases)     # Metaclass namespace creation
    __instancecheck__(cls, instance)  # isinstance() behavior
    __subclasscheck__(cls, subclass)  # issubclass() behavior
    Python

    Common Patterns Quick Reference

    Immutable Object:

    @dataclass(frozen=True, slots=True)
    class Point:
        x: float
        y: float
    Python

    Comparable Object:

    from functools import total_ordering
    
    @total_ordering
    class Item:
        def __eq__(self, other): ...
        def __lt__(self, other): ...
    Python

    Hashable Object:

    def __eq__(self, other): return self.x == other.x
    def __hash__(self): return hash(self.x)
    Python

    Context Manager:

    def __enter__(self): return resource
    def __exit__(self, *args): cleanup(); return False
    Python

    Iterator:

    def __iter__(self): return self
    def __next__(self): 
        if done: raise StopIteration
        return next_item
    Python

    Callable:

    def __call__(self, *args, **kwargs): return result
    Python

    End of Guide – Happy Pythoning! 🐍


    Discover more from Altgr Blog

    Subscribe to get the latest posts sent to your email.

    Leave a Reply

    Your email address will not be published. Required fields are marked *