Daily Tech Brief

Top startup stories in your inbox

Subscribe Free

© 2026 rakrisi Daily

Special Methods - Magical Python Classes

Special Methods: Magical Python Classes

Welcome to Special Methods! These are Python’s “magic methods” that start and end with double underscores. They make your classes work seamlessly with Python’s built-in operations and syntax.

What Are Special Methods?

Special methods (also called dunder methods) allow your objects to integrate with Python’s syntax and built-in functions. They’re the “magic” behind operator overloading and object behavior.

Basic Object Creation

class Person:
    def __init__(self, name, age):
        """Called when creating a new instance."""
        self.name = name
        self.age = age
        print(f"Person {name} created!")

    def __del__(self):
        """Called when object is about to be destroyed."""
        print(f"Person {self.name} is being destroyed!")

    def __str__(self):
        """Called by str() and print()."""
        return f"Person(name='{self.name}', age={self.age})"

    def __repr__(self):
        """Called by repr() - should be unambiguous."""
        return f"Person('{self.name}', {self.age})"

# Test
person = Person("Alice", 30)  # __init__ called
print(person)                  # __str__ called
print(repr(person))           # __repr__ called
del person                    # __del__ called

Comparison Operators

Rich Comparison Methods

class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def __eq__(self, other):  # ==
        if isinstance(other, Student):
            return self.grade == other.grade
        return NotImplemented

    def __lt__(self, other):  # <
        if isinstance(other, Student):
            return self.grade < other.grade
        return NotImplemented

    def __le__(self, other):  # <=
        return self == other or self < other

    def __gt__(self, other):  # >
        return not (self <= other)

    def __ge__(self, other):  # >=
        return not (self < other)

    def __str__(self):
        return f"{self.name}: {self.grade}"

# Test comparisons
students = [
    Student("Alice", 85),
    Student("Bob", 92),
    Student("Charlie", 78)
]

print("Students:", [str(s) for s in students])
print("Sorted by grade:", [str(s) for s in sorted(students)])

alice = students[0]
bob = students[1]
print(f"Alice == Bob: {alice == bob}")
print(f"Alice < Bob: {alice < bob}")

Arithmetic Operators

Mathematical Operations

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):  # self + other
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        return NotImplemented

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

    def __mul__(self, scalar):  # self * scalar
        if isinstance(scalar, (int, float)):
            return Vector(self.x * scalar, self.y * scalar)
        return NotImplemented

    def __rmul__(self, scalar):  # scalar * self (reflected)
        return self * scalar  # Reuse __mul__

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

    def __neg__(self):  # -self
        return Vector(-self.x, -self.y)

    def __abs__(self):  # abs(self)
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

# Test arithmetic operations
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v1 + v2: {v1 + v2}")
print(f"v1 - v2: {v1 - v2}")
print(f"v1 * 3: {v1 * 3}")
print(f"2 * v1: {2 * v1}")  # Uses __rmul__
print(f"v1 / 2: {v1 / 2}")
print(f"-v1: {-v1}")
print(f"abs(v1): {abs(v1):.2f}")

Container Methods

Making Objects Work Like Lists/Dictionaries

class SimpleList:
    def __init__(self):
        self._items = []

    def __len__(self):  # len(obj)
        return len(self._items)

    def __getitem__(self, index):  # obj[index]
        return self._items[index]

    def __setitem__(self, index, value):  # obj[index] = value
        self._items[index] = value

    def __delitem__(self, index):  # del obj[index]
        del self._items[index]

    def __iter__(self):  # for item in obj:
        return iter(self._items)

    def __contains__(self, item):  # item in obj
        return item in self._items

    def append(self, item):
        self._items.append(item)

# Test container methods
my_list = SimpleList()
my_list.append("apple")
my_list.append("banana")
my_list.append("cherry")

print(f"Length: {len(my_list)}")
print(f"First item: {my_list[0]}")
print(f"Contains 'banana': {'banana' in my_list}")
print(f"All items: {list(my_list)}")

my_list[1] = "orange"
print(f"After change: {list(my_list)}")

del my_list[0]
print(f"After deletion: {list(my_list)}")

Dictionary-like Behavior

class CaseInsensitiveDict:
    def __init__(self, data=None):
        self._data = {}
        if data:
            for key, value in data.items():
                self[key] = value

    def __setitem__(self, key, value):  # obj[key] = value
        self._data[key.lower()] = (key, value)

    def __getitem__(self, key):  # obj[key]
        return self._data[key.lower()][1]

    def __delitem__(self, key):  # del obj[key]
        del self._data[key.lower()]

    def __iter__(self):  # for key in obj:
        return (original_key for original_key, _ in self._data.values())

    def __len__(self):  # len(obj)
        return len(self._data)

    def __contains__(self, key):  # key in obj
        return key.lower() in self._data

    def keys(self):
        return [original_key for original_key, _ in self._data.values()]

    def values(self):
        return [value for _, value in self._data.values()]

    def items(self):
        return self._data.values()

# Test dictionary-like behavior
d = CaseInsensitiveDict()
d["Name"] = "Alice"
d["AGE"] = 30
d["City"] = "New York"

print(f"Keys: {list(d.keys())}")
print(f"Values: {list(d.values())}")
print(f"Length: {len(d)}")
print(f"'name' in dict: {'name' in d}")
print(f"'Name' in dict: {'Name' in d}")
print(f"d['name']: {d['name']}")
print(f"d['AGE']: {d['AGE']}")

Context Managers

__enter__ and __exit__ Methods

class Timer:
    def __init__(self, name="Timer"):
        self.name = name

    def __enter__(self):
        import time
        self.start_time = time.time()
        print(f"Starting {self.name}...")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        elapsed = time.time() - self.start_time
        print(f"{self.name} completed in {elapsed:.2f} seconds")

        # Return False to propagate exceptions
        return False

# Test context manager
with Timer("Data Processing") as t:
    # Simulate some work
    import time
    time.sleep(1)
    print("Processing data...")

print("Done!")

File-like Object

class StringIO:
    """Simple string-based file-like object."""
    def __init__(self):
        self._content = ""
        self._position = 0

    def write(self, text):
        self._content += text

    def read(self, size=-1):
        if size == -1:
            result = self._content[self._position:]
            self._position = len(self._content)
        else:
            result = self._content[self._position:self._position + size]
            self._position += len(result)
        return result

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Could flush or close here
        pass

# Test file-like object
with StringIO() as f:
    f.write("Hello, ")
    f.write("World!")
    f.write(" How are you?")

    content = f.read()
    print(f"Content: {content}")

    f.write(" I'm fine!")
    more_content = f.read()
    print(f"More content: {more_content}")

Callable Objects

__call__ Method

class Polynomial:
    def __init__(self, coefficients):
        """Coefficients from lowest to highest degree."""
        self.coefficients = coefficients

    def __call__(self, x):
        """Evaluate polynomial at x."""
        return sum(coeff * (x ** i) for i, coeff in enumerate(self.coefficients))

    def __str__(self):
        terms = []
        for i, coeff in enumerate(self.coefficients):
            if coeff == 0:
                continue
            if i == 0:
                terms.append(str(coeff))
            elif i == 1:
                terms.append(f"{coeff}x")
            else:
                terms.append(f"{coeff}x^{i}")
        return " + ".join(reversed(terms)) if terms else "0"

# Test callable object
p = Polynomial([1, 2, 1])  # x^2 + 2x + 1
print(f"Polynomial: {p}")
print(f"p(0) = {p(0)}")
print(f"p(1) = {p(1)}")
print(f"p(2) = {p(2)}")

# Can be used like a function
values = [p(x) for x in range(-2, 3)]
print(f"Values from x=-2 to x=2: {values}")

Attribute Access

Customizing Attribute Access

class ValidatedAttributes:
    def __init__(self):
        self._data = {}

    def __setattr__(self, name, value):
        if name.startswith('_'):
            # Allow private attributes
            super().__setattr__(name, value)
        else:
            # Validate public attributes
            if isinstance(value, str) and len(value.strip()) == 0:
                raise ValueError(f"Attribute '{name}' cannot be empty string")
            self._data[name] = value

    def __getattr__(self, name):
        if name in self._data:
            return self._data[name]
        raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __delattr__(self, name):
        if name in self._data:
            del self._data[name]
        else:
            super().__delattr__(name)

# Test attribute access control
obj = ValidatedAttributes()
obj.name = "Alice"
obj.age = 30

print(f"Name: {obj.name}")
print(f"Age: {obj.age}")

try:
    obj.empty = ""
except ValueError as e:
    print(f"Error: {e}")

try:
    print(obj.nonexistent)
except AttributeError as e:
    print(f"Error: {e}")

Practical Examples

Example 1: Enhanced Fraction Class

import math

class Fraction:
    def __init__(self, numerator, denominator=1):
        if denominator == 0:
            raise ValueError("Denominator cannot be zero")

        # Simplify fraction
        gcd = math.gcd(numerator, denominator)
        self.numerator = numerator // gcd
        self.denominator = denominator // gcd

        # Make sure denominator is positive
        if self.denominator < 0:
            self.numerator = -self.numerator
            self.denominator = -self.denominator

    def __add__(self, other):
        if isinstance(other, Fraction):
            new_num = self.numerator * other.denominator + other.numerator * self.denominator
            new_den = self.denominator * other.denominator
            return Fraction(new_num, new_den)
        elif isinstance(other, (int, float)):
            return self + Fraction(other)
        return NotImplemented

    def __sub__(self, other):
        return self + (-other)

    def __mul__(self, other):
        if isinstance(other, Fraction):
            return Fraction(self.numerator * other.numerator, self.denominator * other.denominator)
        elif isinstance(other, (int, float)):
            return Fraction(self.numerator * other, self.denominator)
        return NotImplemented

    def __truediv__(self, other):
        if isinstance(other, Fraction):
            return self * Fraction(other.denominator, other.numerator)
        elif isinstance(other, (int, float)):
            return self * Fraction(1, other)
        return NotImplemented

    def __neg__(self):
        return Fraction(-self.numerator, self.denominator)

    def __eq__(self, other):
        if isinstance(other, Fraction):
            return self.numerator * other.denominator == other.numerator * self.denominator
        elif isinstance(other, (int, float)):
            return self.numerator / self.denominator == other
        return False

    def __lt__(self, other):
        if isinstance(other, Fraction):
            return self.numerator * other.denominator < other.numerator * self.denominator
        elif isinstance(other, (int, float)):
            return self.numerator / self.denominator < other
        return NotImplemented

    def __le__(self, other):
        return self < other or self == other

    def __gt__(self, other):
        return not (self <= other)

    def __ge__(self, other):
        return not (self < other)

    def __float__(self):
        return self.numerator / self.denominator

    def __str__(self):
        if self.denominator == 1:
            return str(self.numerator)
        return f"{self.numerator}/{self.denominator}"

    def __repr__(self):
        return f"Fraction({self.numerator}, {self.denominator})"

# Test fraction operations
f1 = Fraction(1, 2)
f2 = Fraction(1, 3)

print(f"f1: {f1}")
print(f"f2: {f2}")
print(f"f1 + f2: {f1 + f2}")
print(f"f1 - f2: {f1 - f2}")
print(f"f1 * f2: {f1 * f2}")
print(f"f1 / f2: {f1 / f2}")
print(f"f1 == 0.5: {f1 == 0.5}")
print(f"f1 < f2: {f1 < f2}")
print(f"float(f1): {float(f1)}")

Example 2: Matrix Class

class Matrix:
    def __init__(self, data):
        """data should be a list of lists (rows)."""
        if not data or not all(isinstance(row, list) for row in data):
            raise ValueError("Matrix data must be a list of lists")

        self.rows = len(data)
        self.cols = len(data[0])
        if not all(len(row) == self.cols for row in data):
            raise ValueError("All rows must have the same length")

        self.data = [row[:] for row in data]  # Deep copy

    def __getitem__(self, key):
        """matrix[i, j] or matrix[i][j]"""
        if isinstance(key, tuple) and len(key) == 2:
            i, j = key
            return self.data[i][j]
        else:
            return self.data[key]

    def __setitem__(self, key, value):
        """matrix[i, j] = value"""
        if isinstance(key, tuple) and len(key) == 2:
            i, j = key
            self.data[i][j] = value
        else:
            self.data[key] = value

    def __add__(self, other):
        if not isinstance(other, Matrix):
            return NotImplemented
        if self.rows != other.rows or self.cols != other.cols:
            raise ValueError("Matrices must have the same dimensions")

        result = []
        for i in range(self.rows):
            row = []
            for j in range(self.cols):
                row.append(self.data[i][j] + other.data[i][j])
            result.append(row)
        return Matrix(result)

    def __mul__(self, other):
        if isinstance(other, Matrix):
            # Matrix multiplication
            if self.cols != other.rows:
                raise ValueError("Matrix dimensions don't match for multiplication")

            result = []
            for i in range(self.rows):
                row = []
                for j in range(other.cols):
                    sum_val = 0
                    for k in range(self.cols):
                        sum_val += self.data[i][k] * other.data[k][j]
                    row.append(sum_val)
                result.append(row)
            return Matrix(result)

        elif isinstance(other, (int, float)):
            # Scalar multiplication
            result = []
            for i in range(self.rows):
                row = []
                for j in range(self.cols):
                    row.append(self.data[i][j] * other)
                result.append(row)
            return Matrix(result)

        return NotImplemented

    def __eq__(self, other):
        if not isinstance(other, Matrix):
            return False
        return self.data == other.data

    def __str__(self):
        return "\n".join([" ".join(map(str, row)) for row in self.data])

    def __repr__(self):
        return f"Matrix({self.data})"

# Test matrix operations
m1 = Matrix([[1, 2], [3, 4]])
m2 = Matrix([[5, 6], [7, 8]])

print("Matrix 1:")
print(m1)
print("\nMatrix 2:")
print(m2)

print("\nMatrix 1 + Matrix 2:")
print(m1 + m2)

print("\nMatrix 1 * Matrix 2:")
print(m1 * m2)

print("\nMatrix 1 * 2:")
print(m1 * 2)

print(f"\nElement [0,1]: {m1[0,1]}")
m1[0, 1] = 99
print(f"After change: {m1[0,1]}")

Example 3: Custom Collection

class PriorityQueue:
    def __init__(self):
        self._items = []

    def push(self, item, priority=0):
        """Add item with priority."""
        self._items.append((priority, item))
        self._items.sort(reverse=True)  # Highest priority first

    def pop(self):
        """Remove and return highest priority item."""
        if not self._items:
            raise IndexError("Queue is empty")
        return self._items.pop(0)[1]

    def peek(self):
        """Return highest priority item without removing."""
        if not self._items:
            raise IndexError("Queue is empty")
        return self._items[0][1]

    def __len__(self):
        return len(self._items)

    def __bool__(self):
        return bool(self._items)

    def __iter__(self):
        return (item for _, item in self._items)

    def __str__(self):
        items_str = ", ".join(str(item) for item in self)
        return f"PriorityQueue([{items_str}])"

    def __eq__(self, other):
        if not isinstance(other, PriorityQueue):
            return False
        return self._items == other._items

# Test priority queue
pq = PriorityQueue()
pq.push("Low priority task", priority=1)
pq.push("High priority task", priority=5)
pq.push("Medium priority task", priority=3)

print(f"Queue: {pq}")
print(f"Length: {len(pq)}")
print(f"Is empty: {not pq}")

print(f"Next task: {pq.peek()}")
completed_task = pq.pop()
print(f"Completed: {completed_task}")
print(f"Remaining: {pq}")

Practice Exercises

Exercise 1: Complex Number Class

Create a ComplexNumber class with:

  • __init__, __str__, __repr__
  • Arithmetic operators (+, -, *, /)
  • Comparison operators (==, !=)
  • abs() support with __abs__
  • Conjugate method

Exercise 2: Time Duration Class

Build a Duration class that:

  • Stores time in seconds internally
  • Supports creation from hours/minutes/seconds
  • Implements arithmetic operations
  • Provides string formatting
  • Supports comparison operations

Exercise 3: Sparse Array

Create a SparseArray class that:

  • Efficiently stores mostly-zero arrays
  • Implements __getitem__, __setitem__
  • Supports len() and iteration
  • Provides memory usage information
  • Handles negative indices

Exercise 4: Configuration Object

Build a Config class with:

  • Dictionary-like access (config['key'])
  • Attribute access (config.key)
  • Validation of values
  • Loading from/saving to JSON
  • Environment variable override

Exercise 5: Database Connection Pool

Create a ConnectionPool class that:

  • Manages multiple database connections
  • Implements context manager protocol
  • Provides connection checkout/checkin
  • Tracks connection usage statistics
  • Handles connection failures gracefully

Summary

Special methods make your classes integrate seamlessly with Python:

Object Creation/Destruction:

def __init__(self, ...): pass  # Constructor
def __del__(self): pass        # Destructor
def __str__(self): pass        # String representation
def __repr__(self): pass       # Unambiguous representation

Operators:

def __add__(self, other): pass      # +
def __eq__(self, other): pass       # ==
def __lt__(self, other): pass       # <
def __len__(self): pass             # len()
def __getitem__(self, key): pass    # obj[key]

Context Managers:

def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb): pass

Callable Objects:

def __call__(self, ...): pass  # obj(args)

Key Benefits:

  • Natural Python syntax integration
  • Operator overloading
  • Custom object behavior
  • Container protocol implementation
  • Context manager support

Common Special Methods:

  • __init__, __str__, __repr__ - Basic object setup
  • __eq__, __lt__, etc. - Comparisons
  • __add__, __mul__, etc. - Arithmetic
  • __len__, __getitem__ - Container behavior
  • __enter__, __exit__ - Context managers

Next: Advanced OOP - taking your skills to the next level! 🚀