AlgoMaster Logo

Design Patterns

High Priority19 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

The classic Gang of Four patterns were written for C++ in 1994, and most of them grew up around problems that don't exist in Python: no first-class functions, no duck typing, no built-in iteration protocol, no context managers. Read those patterns in Python and many of them collapse to "use a function" or "the language already does this for you." A handful still apply, especially in larger codebases, and a few patterns have shapes that Python rewrites in a smaller form. This lesson covers which patterns still matter in Python, which ones become one line of code, and how to avoid dragging Java-shaped scaffolding into a language that doesn't need it.

Why Python Reshapes the Pattern Catalog

Three Python features replace large parts of the GoF book:

  1. Functions are first-class objects. You can pass them, return them, store them, and rebind them at runtime. Any pattern whose job was "wrap a single overridable operation in a class" is usually a function.
  2. Duck typing. Code calls obj.process(order) without caring whether obj is a RealProcessor, a FakeProcessor, or a random object that happens to have a process method. Patterns that exist to fake polymorphism in statically typed languages become invisible.
  3. Built-in protocols. Iteration, context management, decoration, and comparison are all baked into the language with dunder methods. Patterns that recreate these from scratch are unnecessary.

A quick map of which GoF patterns survive Python, which simplify, and which disappear:

GoF patternStatus in PythonWhat replaces it
SingletonUse with careModule-level state, or a class with a class-level instance
FactorySimplifiedA function, classmethod, or callable that returns instances
BuilderSimplifiedKeyword arguments, dataclasses, fluent return-self chains
StrategyMostly dissolvesPass a function or callable
ObserverSimplifiedCallbacks list, or an event bus
IteratorBuilt in__iter__ / __next__, generators, for x in obj
AdapterStill usefulWrapper class or function that translates the interface
DecoratorBuilt in (as a feature)@decorator syntax
Context ManagerBuilt in (as a protocol)with statement + __enter__ / __exit__
Template MethodStill usefulBase class with abstract steps
CommandSimplifiedFunction or callable (closures, partials)
Chain of ResponsibilityStill usefulList of handlers, middleware lists

The rest of the GoF set (Abstract Factory, Bridge, Composite, Flyweight, Proxy, Visitor, Mediator, Memento, State, Interpreter) shows up rarely in idiomatic Python application code. Some of it appears in frameworks, but rarely in your own modules.

The diagram captures the rough categories. The patterns on the left (built-in and one-line idiom) don't need their own pages of explanation in Python. The middle category is what this lesson focuses on, and the rightmost group is mostly framework territory.

Singleton

A singleton is a class that has exactly one instance for the lifetime of the program. The classic use cases are a config object, a connection pool, or a logger.

In most Python codebases, the simplest singleton is a module. Python imports a module once and caches it in sys.modules, so any module-level value is shared across every import.

Anywhere else in the program:

That's a singleton. There's one _active_promotions dict, every import sees the same object, and there's no class-shaped ceremony.

When a module isn't enough (you want polymorphism, configuration, or to support multiple variants), a small class with a class-level instance is the next step:

__new__ is the hook that decides which instance gets returned. By caching the first one on the class and returning it forever after, every AppLogger() call hands back the same object.

This works, but it has costs. Singletons are global state in disguise. They make testing harder (the cached instance leaks between tests), they hide dependencies (a function that uses AppLogger() looks parameter-free but depends on whatever the singleton holds), and they don't play well with parallel code (every thread shares the same object). When you need a singleton, prefer:

  1. A module with module-level state.
  2. Dependency injection: pass the shared object in as a parameter.
  3. The class-level instance only when you need a class.

A singleton makes every function that uses it harder to test in isolation. Tests have to reset the instance between runs, and parallel tests need extra locking. Module-level state has the same issue but is at least obvious.

Factory

A factory is a callable that produces objects, hiding the decision of which subclass or configuration to return. In Java this usually means a Factory class with a create method. In Python it's a function, a classmethod, or any callable.

The simplest factory is a function:

When the variants are bound to a class, a classmethod groups them cleanly:

The pattern here is that from_cart and from_payload are factories that know how to build an Order from different inputs. The constructor stays simple, and each factory method gets a name that tells the caller what kind of input it expects. This is a common factory shape in modern Python.

A registry-based factory is the third common variant. A dict maps a name to a class or callable, and make_x looks it up:

The advantage is that adding a new option is a single line in the dict, not a new elif branch. Plugins and frameworks rely on this shape because the registry can be extended at import time.

Strategy

The Strategy pattern wraps an interchangeable algorithm in an object so that callers can swap implementations without changing their code. In Java that's an interface and a few classes. In Python it's a function passed as an argument.

Three different discount strategies, no classes, no interface. Each strategy is a function that takes a price and returns the discount. total_with_discount is the "context" in the GoF sense; it accepts the strategy as a parameter and doesn't care which one it got.

A class-based Strategy is worth it when the strategy needs state. If the discount logic has to remember the customer's history, an object makes sense:

TieredDiscount is callable thanks to __call__, so total_with_discount doesn't have to know it received an object instead of a function. The interface is the same: "something I can call with a price."

The lesson is that the Strategy pattern in Python is usually "accept a callable as a parameter." Use a class only when the strategy needs state, configuration, or its own methods.

Observer

The Observer pattern lets one object notify a list of subscribers when something changes. Stock systems, event buses, and UI frameworks all use it. In Python, the simplest version is a list of callbacks.

Two subscribers, no formal interface. Each subscriber is a function that takes the same arguments the publisher sends. This is the Observer pattern in 12 lines.

For larger systems, an event bus generalizes the idea: instead of one publisher with its own subscriber list, a central object routes named events to handlers.

Either shape (direct subscriber list or central bus) covers most observer use cases without inheriting from any "Subject" base class.

Callbacks fired in a loop run synchronously. If one handler is slow (writes to disk, makes a network call), every subsequent handler waits. For high-traffic event streams, queue events to a background worker instead of firing handlers inline.

Adapter

Adapters wrap an object with an incompatible interface so the rest of your code can use a familiar one. This pattern still applies in Python because external libraries rarely match each other's APIs.

A small example. Consider code built around a PaymentProcessor interface with a charge(amount, customer) method, where the new vendor's library uses create_payment_intent(amount_cents, email):

The adapter does three things: it owns a reference to the wrapped client, it translates the argument names and shapes, and it presents the interface the rest of the code expects. The code that calls processor.charge(...) doesn't know or care that Stripe is the backend. Swap the adapter for PayPalAdapter later and nothing else changes.

This is the GoF Adapter pattern almost verbatim. Python doesn't shortcut it because the problem of matching a foreign API to a local interface is real and the solution is small.

Decorator (The Built-in Kind)

GoF Decorator is "wrap an object to add behavior without changing its class." Python has a syntax-level decorator (@decorator) that does this for functions and methods. They are related but not identical: GoF Decorator decorates instances at runtime, while the Python @decorator runs at function definition time. The everyday Python work, though, is the @decorator form.

@log_timing wraps total_cart_value so every call now also prints the timing. The wrapped function looks like the original to its callers (@wraps preserves the name, docstring, and signature) but adds behavior around it. The takeaway is that Python has the Decorator pattern as a language feature, so you rarely need to build one out of classes.

Iterator (The Built-in Kind)

GoF Iterator wraps a collection in an object that yields its elements. Python's iteration protocol is so deeply baked in that you usually don't write iterators by hand. Generators express the pattern in two or three lines.

paginate is a generator. Every yield hands a value to whoever is iterating and pauses; the next next() call resumes from where it left off. There's no __iter__, no __next__, no state class. Python has built-in iteration, so you rarely write the pattern out by hand.

Context Manager (The Built-in Kind)

GoF doesn't have a Context Manager pattern. Python does, because resource cleanup is a constant requirement and exceptions make it easy to leak handles. The with statement plus __enter__ / __exit__ (or @contextmanager for the function form) replace the "set up, do work in a try, clean up in a finally" pattern entirely.

The setup before yield runs when the with block starts. The teardown after yield runs no matter how the block exits, success or exception. This is the Resource Acquisition Is Initialization (RAII) pattern, expressed natively in the language. Context managers get a full lesson elsewhere.

When Patterns Help, When They Hurt

A pattern is a tool. Using one when the problem doesn't call for it adds ceremony without adding value. Using one when the problem benefits from a name and a shape makes the code easier to read for the next person.

Three rough heuristics for when a pattern is worth applying:

  1. A team needs a shared vocabulary. "We use the Adapter pattern here" is shorter than "we have a class that translates between two interfaces." On a large codebase with many contributors, that compression matters.
  2. A framework or library expects a shape. When writing a Django middleware, a pytest fixture, or a FastAPI dependency, the pattern is dictated by the framework's API. Use what it asks for.
  3. The pattern matches the problem. Need exactly one config object across the program? Singleton (or a module). Need to swap algorithms at runtime? Strategy. Need to broadcast events to subscribers? Observer.

The opposite case looks like a class hierarchy or a factory in a tiny script. If three lines of script-level code would do the job, three lines of code is the right answer.

A common issue for developers coming from Java or C# is recreating Java's habits in Python:

This works, but every line is doing the job of a feature Python already has. The same thing as idiomatic Python:

Two lines, no abstract base class, no factory, no inheritance. The "pattern" was a workaround for missing features that Python ships natively. When you find yourself writing scaffolding because a pattern says you should, ask first whether the language already provides the same thing.

That said, on a 50-file project with a dozen contributors and several discount variants that have state, configuration, and tests, the class-based version might be appropriate. The classes give the variants names, the factory gives a single seam for adding new ones, and the abstract base class documents the interface for static analyzers. The right shape depends on the project's size and what the team has decided to standardize on.

A Quick Tour of Patterns Still Worth Knowing

Three more patterns appear often enough in idiomatic Python that they're worth a brief mention.

Template Method

A base class defines the steps of an algorithm and leaves some steps abstract. Subclasses fill in the gaps. This is one of the few class-shaped patterns Python doesn't rewrite, because the structure is what gives it value.

The base class owns the order of operations. Subclasses control how each step happens. This is useful when you have several variants of a workflow that share the same structure but differ in pieces.

Command

A Command is "wrap an action in an object so you can pass it around, queue it, or undo it." In Python, a function with bound arguments (via functools.partial, a closure, or a small object with __call__) is usually enough.

Each partial is a Command object: it remembers the function and the bound arguments. When you call it later, it runs as if you had called the original with those arguments.

Chain of Responsibility

A request walks through a list of handlers until one of them processes it. Web framework middleware, exception handlers, and request validators all use this shape.

A list of small functions, each doing one check, walked in order. Adding a new validator is appending to the list. Removing one is deleting a line. This kind of "pipeline of small handlers" is how a lot of production Python middleware is structured.

Quiz

Design Patterns Quiz

10 quizzes