Preamble
The Gang of Four’s Design Patterns is a vocabulary book. Python’s first-class functions, duck typing, and simple namespaces remove much of the Java ceremony that made patterns look like XML factories with extra steps. I still name patterns in reviews when the name short-circuits twenty lines of explanation—skip the boilerplate when the language feature is the pattern.
Java-heavy codebases often lean on interfaces, abstract factories, and explicit hierarchies because the type system and enterprise frameworks reward that shape. Python codebases more often express the same intent with modules, callables, protocols (typing.Protocol), and dataclasses—fewer named types, more behavior at the boundary. The patterns below are the same ideas; the pros and cons shift with how much structure you buy.
Strategy (pluggable algorithm)
Strategy is “pluggable algorithm” whether you implement it as classes or as callables passed into a function. Sort keys, pricing rules, and validation pipelines are strategies in disguise. The pattern is not the class diagram—it is the injection point.
Compared to Java: Java often uses interface Strategy plus one class per implementation. Python usually needs only functions or small callables; use a class when strategy carries state or you want test doubles with clear seams.
| Pros | Cons |
|---|---|
| Easy to extend without editing the caller | Many strategies can sprawl if every variant is a new type |
| Excellent testability (swap in fakes) | Callable strategies can hide lifecycle (who owns resources?) |
| Aligns with open/closed at the right seam | Over-abstraction when a single if or dict dispatch would do |
Adapter (honest glue)
Adapter wraps a legacy API behind the interface your domain already speaks. Thin wrappers beat scattered if vendor == X branches. Adapters are where timeouts and retries belong when you cannot fix the upstream yet.
Compared to Java: Java adapters often implement a formal interface to satisfy DI containers. Python adapters are frequently thin classes or functions that translate types and errors; Protocol can document the expected surface without inheritance.
| Pros | Cons |
|---|---|
| Isolates legacy quirks from domain logic | Extra layer to step through when debugging |
| One place to add resilience (retries, circuit breaking) | Risk of the adapter becoming a god object if it keeps growing |
| Makes third-party churn a localized change | Duplicated concepts if “our” API mirrors the vendor too closely |
Observer (explicit registries)
Observer in Python need not mean Java’s listener interfaces—explicit registries of callables or pub/sub channels work fine. Magic event buses that reflect over the codebase trade debuggability for cleverness; I prefer boring registration sites I can grep.
Compared to Java: Java’s Listener/Event pairs and Swing-style hierarchies are familiar but verbose. Python often uses callbacks, async signals, or frameworks (Django signals, etc.); the main discipline is who registers whom and ordering, not the number of interfaces.
| Pros | Cons |
|---|---|
| Decouples publishers from subscribers | Harder to trace than direct calls (“who fired this?”) |
| Easy to add new reactions without editing the core | Ordering and re-entrancy bugs if not documented |
| Natural fit for UI and domain events | Global event buses amplify accidental coupling |
Factory and Abstract Factory
Factory hides construction details (parsing config, choosing a concrete class). Abstract Factory groups related factories for families of objects—think “this deployment uses these storage and queue implementations together.”
Compared to Java: Java often uses AbstractFactory + interfaces for every product type. Python more often uses a module or factory function that returns a protocol-typed instance, or a dict/registry keyed by name. You reach for classes when construction is complex or you need subclass hooks.
| Pros | Cons |
|---|---|
| Construction rules live in one place | Indirection: “where is this thing created?” |
| Easier to swap implementations for tests | Registry-heavy factories can become implicit DI containers |
| Abstract factory keeps product families consistent | Overkill when dataclass + one function suffices |
Decorator (GoF vs Python @)
GoF Decorator stacks objects that add behavior around a shared interface. Python’s @decorator is syntactic sugar for higher-order functions—related in spirit, not the same mechanism.
Compared to Java: Java uses wrapper classes or dynamic proxies; Python wraps with functions or descriptor-aware classes. For cross-cutting concerns (logging, timing), Python decorators are idiomatic; for composable object behavior, explicit wrapper classes still match the GoF shape.
| Pros | Cons |
|---|---|
| Python decorators are terse and readable | Stack order and signature preservation need care (functools.wraps) |
| GoF wrappers compose without subclass explosion | Deep stacks hurt debugging and typing |
| Good separation of core vs cross-cutting | Easy to over-decorate; “framework soup” in small modules |
Template Method
Template Method defines a skeleton in a base class and lets subclasses override steps. In Python, the same idea appears as hooks (on_start, process_item) or as a cooperative base using super().
Compared to Java: Java leans on abstract methods; Python often prefers hook methods with defaults or composition (pass in the varying step as a callable) to avoid deep inheritance trees.
| Pros | Cons |
|---|---|
| Documents the algorithm’s structure in one place | Inheritance ties subclasses to base class evolution |
| Subclasses customize only what varies | “Fragile base class” if hooks are poorly documented |
| Familiar to readers of OOP textbooks | Easy to default to inheritance when callables or composition would stay flatter |
Command (action as object)
Command packages a request as an object—useful for undo, queues, macros, and transactional workflows.
Compared to Java: Java commands are often classes implementing Command. Python may use dataclass commands, closures, or small callables stored in a list; the object form wins when you need serialization or uniform metadata (idempotency keys, tracing).
| Pros | Cons |
|---|---|
| Natural fit for job queues and replay | Boilerplate if every action is a class with one method |
| Decouples invoker from receiver | Overkill for simple “call this function once” |
| Enables logging/auditing of intent | Must design equality and serialization deliberately |
Chain of Responsibility
Chain of Responsibility passes a request along a chain until something handles it—parsing pipelines, middleware, validation stages.
Compared to Java: Java often builds explicit linked lists or lists of Handler interfaces. Python commonly uses lists of callables, middleware stacks (WSGI/ASGI), or generator pipelines—same flow, less interface noise.
| Pros | Cons |
|---|---|
| Add/remove stages without changing neighbors | Harder to see the full path than one function |
| Encourages single-purpose handlers | Risk of hidden ordering dependencies |
| Mirrors HTTP and message middleware mental models | “Silent pass” bugs if no handler claims the request |
Facade
Facade is a narrow entry point over a messy subsystem—one import, stable API, fewer leaks of internal types.
Compared to Java: Java facades often sit in front of packages or “modules” in the Maven sense. Python facades are frequently __init__.py exports or a service module that coordinates several internal modules.
| Pros | Cons |
|---|---|
| Lowers cognitive load for callers | Facade can become a dumping ground for unrelated helpers |
| Subsystem can evolve behind a stable surface | Indirection: specialists still need to read internals |
| Good onboarding boundary for large libraries | Wrong granularity splits teams (“everything through the facade”) |
Builder (and the Python escape hatches)
Builder separates stepwise construction from representation—common for objects with many optional fields or invariants checked at build time.
Compared to Java: Java often uses fluent Builder classes. Python frequently prefers dataclasses with defaults, NamedTuple, keyword-only arguments, or a build_x(**kwargs) with validation—builders still help when construction is multi-phase or readable prose matters (test fixtures, DSLs).
| Pros | Cons |
|---|---|
| Readable construction of complex objects | Another type to maintain when dataclass might suffice |
Enforces invariants at build() |
Fluent builders need discipline (immutable steps) |
| Nice for tests and sample data | Over-fluent APIs can obscure required fields |
Singleton (and what Python does instead)
Singleton guarantees one instance. In Python, module-level state is the usual singleton: import the module, use its attributes. Explicit singleton classes are rarer and often criticized.
Compared to Java: Spring and friends popularized singleton-scoped beans. Python rarely needs a getInstance(); configure once at startup and pass dependencies, or use a module as the namespace.
| Pros | Cons |
|---|---|
| True single shared resource (pool, config cache) | Global state complicates tests and concurrency |
| Module singleton is simple and idiomatic | Hides dependencies; encourages import-time side effects |
| Sometimes required by external APIs | Class-based singletons fight the import system and mocking |
When not to pattern
If a list comprehension and a function do the job, do not import AbstractSingletonBeanFactory energy into the module. Name the pattern when ambiguity hurts onboarding; omit it when clarity is already local.
Rule of thumb: Java teams often name types to satisfy the compiler and frameworks; Python teams often name functions and modules first and add Protocol/ABC only when multiple implementations or third-party contracts demand it.
Conclusion
Patterns are communication tools. Refactoring Guided by Tests and Code Smells pairs refactoring moves with tests so renames and extractions stay safe.