Skip to content
Hello Python
7/7

Design Patterns in Python

Topic 7 of 7, with 3 concept checks. Pythonic Singleton, Factory, and Observer

Apply patterns only where they simplify change

Design tradeoffs

Recognize the pressure a design pattern addresses, translate it into Python's language features, and reject ceremony that makes a small interview solution harder to explain.

Core lesson 01

Python modules are already singletons by nature (imported once, cached in sys.modules), so a module-level instance is often the simplest 'singleton' — formal class-based approaches exist too but aren't always necessary.

In languages without a true module system, Singleton classes solve the 'exactly one instance, globally accessible' problem. Python already gives you that for free: a module is loaded once and cached, so config.py defining settings = Settings() at module level IS effectively a singleton, importable everywhere as from config import settings. Formal class-based singletons (via __new__) are still used, especially for lazy initialization or when singleton-ness needs to be a property of the class itself.

Python example
# config.py
class _Settings:
    def __init__(self):
        self.debug = False

settings = _Settings()   # created once, on first import

# anywhere else:
# from config import settings
# settings.debug = True   -- same object everywhere

What to remember

How is a Singleton typically implemented in Python, and is it always the best choice?

Common footguns

  • Reaching for a heavyweight Singleton class pattern by default — often a plain module-level instance, or dependency injection, expresses the same intent more simply in Python.

Core lesson 02

A Factory centralizes object creation so callers don't need to know which concrete class to instantiate. In Python, this is often just a function with a few branches or a dict-based dispatch table.

In statically-typed languages, Factory patterns often require an abstract factory interface and dedicated factory classes to satisfy the type system. Python's duck typing means a simple function returning different concrete objects based on input already satisfies the pattern's intent — callers depend only on the returned object's interface, not the factory's type.

Python example
class Dog:
    def speak(self): return "Woof"
class Cat:
    def speak(self): return "Meow"

def animal_factory(kind):
    return {"dog": Dog, "cat": Cat}[kind]()

pet = animal_factory("cat")
print(pet.speak())   # Meow -- caller never referenced Cat directly

What to remember

What's the Factory pattern, and how does Python's dynamic typing make it lighter-weight?

Common footguns

  • Over-engineering a factory hierarchy (abstract factory classes, registries) for a handful of types where a simple dict dispatch or if/elif would be clearer.

Core lesson 03

A subject object keeps a list of subscriber callables and invokes each one when a notable event occurs — subscribers can be plain functions, bound methods, or any callable.

The core idea — decouple 'something happened' from 'here's what should happen in response' — doesn't need ceremony in Python. A Subject class holds a list of callbacks; subscribe(callback) appends to it, and notify(*args) calls each one. Because Python treats functions as first-class objects, subscribers can be anything callable, with no need for a formal Observer base class or interface.

Python example
class Subject:
    def __init__(self):
        self._subscribers = []
    def subscribe(self, callback):
        self._subscribers.append(callback)
    def notify(self, event):
        for callback in self._subscribers:
            callback(event)

def log_event(event):
    print("logged:", event)

subject = Subject()
subject.subscribe(log_event)
subject.notify("user_signed_up")   # logged: user_signed_up

What to remember

How is the Observer pattern typically implemented in Python?

Common footguns

  • Forgetting to unsubscribe callbacks tied to short-lived objects — the subject's list keeps a reference to them, quietly extending their lifetime (a subtle memory leak).

Python lab

Browser Python lab

Runtime · idle

Python loads on your first run. Your code stays in this browser.

Best practices

  • Favor plain functions and dict dispatch over formal design-pattern class hierarchies where Python's dynamic typing already gives you flexibility.
  • Use module-level instances for simple singleton needs instead of a full Singleton class.
  • Provide an unsubscribe/remove mechanism whenever you implement Observer, to avoid leaking references.
  • Recognize that many 'GoF pattern' problems in Python are solved more simply with first-class functions, closures, or generators than in their original object-oriented formulations.

Apply the concept in Interview practice

Design Snake GamemediumLeetCode #353 · O(1) amortized per move

Track the snake as a deque of coordinates; move by adding a new head and popping the tail unless food was eaten, checking wall/self collisions each step.

Open problem
LFU CachehardLeetCode #460 · O(1) get/put

Track frequency counts alongside an LRU-style structure per frequency bucket, evicting from the lowest-frequency, least-recently-used bucket — a good Strategy-style design exercise.

Open problem
Random Pick with WeightmediumLeetCode #528 · O(log n) per pick

Precompute a prefix-sum array of weights, then binary search for where a random value in the total range falls — Factory-esque object selection weighted by probability.

Open problem

Concept checks

Q01

How is a Singleton typically implemented in Python, and is it always the best choice?

Q02

What's the Factory pattern, and how does Python's dynamic typing make it lighter-weight?

Q03

How is the Observer pattern typically implemented in Python?