Gyaan

Design Patterns in Python

advanced design-patterns singleton factory observer

Design patterns are reusable solutions to common problems. The good news? Python’s features — first-class functions, duck typing, decorators — make many patterns way simpler than in Java or C++. Some patterns are so baked into the language that we use them without realizing.

Singleton

Ensures only one instance of a class exists. Think of it like a database connection pool — we want exactly one.

class Database:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

db1 = Database()
db2 = Database()
print(db1 is db2)  # True — same object

The Pythonic shortcut? Just use a module. Module-level variables are singletons by nature — Python only loads a module once.

Factory

Creates objects without exposing the creation logic. In Python, we often use @classmethod as a factory.

class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role

    @classmethod
    def admin(cls, name):
        return cls(name, role="admin")

    @classmethod
    def guest(cls, name):
        return cls(name, role="guest")

admin = User.admin("Manish")   # cleaner than User("Manish", "admin")
guest = User.guest("Visitor")

Observer

When one object changes, all its “watchers” get notified. Think of it like a newsletter — subscribers get updates automatically.

class EventEmitter:
    def __init__(self):
        self._listeners = {}

    def on(self, event, callback):
        self._listeners.setdefault(event, []).append(callback)

    def emit(self, event, *args):
        for cb in self._listeners.get(event, []):
            cb(*args)

emitter = EventEmitter()
emitter.on("login", lambda user: print(f"{user} logged in"))
emitter.emit("login", "Manish")  # Manish logged in

Strategy

Swap out an algorithm at runtime. In languages like Java, this needs interfaces and classes. In Python, we just pass a function.

def sort_by_name(users):
    return sorted(users, key=lambda u: u["name"])

def sort_by_age(users):
    return sorted(users, key=lambda u: u["age"])

def display_users(users, strategy):
    for user in strategy(users):
        print(user)

users = [{"name": "Zara", "age": 25}, {"name": "Aman", "age": 30}]
display_users(users, sort_by_name)  # sorted by name
display_users(users, sort_by_age)   # sorted by age

In simple language, first-class functions eliminate the need for a whole Strategy class hierarchy.

Decorator Pattern

We already know this one from Python decorators. Wrap a function to extend its behavior without modifying it.

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def greet(name):
    return f"Hello, {name}"

greet("Manish")  # prints "Calling greet", then returns "Hello, Manish"

Iterator

Built right into Python. Any object with __iter__ and __next__ is an iterator. We use them every time we write a for loop.

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        val = self.current
        self.current -= 1
        return val

for num in Countdown(3):
    print(num)  # 3, 2, 1

Context Manager

The with statement pattern. Handles setup and teardown automatically — great for files, locks, database connections.

class Timer:
    def __enter__(self):
        import time
        self.start = time.time()
        return self

    def __exit__(self, *args):
        import time
        print(f"Elapsed: {time.time() - self.start:.2f}s")

with Timer():
    sum(range(1_000_000))  # prints elapsed time when block exits

The key takeaway: Python’s dynamic nature — first-class functions, duck typing, protocols — means we get many patterns “for free.” We don’t need heavy class hierarchies when a simple function or module does the job.