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.