Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ jobs:
run: uv run mypy
- name: Test
run: uv run pytest
- name: README table drift
run: uv run python -m design_patterns.readme_table --check
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ typecheck: ## mypy --strict
test: ## Run test suite with coverage
uv run pytest

readme: ## Regenerate the README catalog table
uv run python -m design_patterns.readme_table

check: lint typecheck test ## Everything CI runs
uv run python -m design_patterns.readme_table --check

clean:
rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage htmlcov dist build
148 changes: 97 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,99 @@
# Python Design Patterns

A collection of design patterns for python: https://python-patterns.guide/

---

## GOF Patterns

__Behavioral Patterns__:

| Pattern | Description |
|:-------:| ----------- |
| [iterator](behavioral/iterator.py) | emplements pythonic style design with 3 rules |
| [command](behavioral/command.py) | Copy Paste implementation of command design pattern |
| [command_1](behavioral/command_1.py) | ![Command Design Pattern](behavioral/command-design-pattern.PNG) |

__Creational Patterns__:

| Pattern | Description |
|:-------:| ----------- |
| [builder_2](creational/builder_2.py) | builder description |
| [builder](creational/builder.py) | builder description |
| [prototype](creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) |
| [singleton_0](creational/singleton_0.py) | singleton description |
| [singleton_1](creational/singleton_1.py) | singleton description |
| [singleton_2](creational/singleton_2.py) | singleton description |
| [singleton_3](creational/singleton_3.py) | singleton description |
| [singleton_4](creational/singleton_4.py) | singleton description |

__Structural Patterns__:

| Pattern | Description |
|:-------:| ----------- |
| [composite](structural/composite.py) | lets clients treat individual objects and compositions uniformly |
| [decorator](structural/decorator.py) | wrap functionality with other functionality in order to affect outputs |
| [decorator_1](structural/decorator_1.py) | showing import time versus run time |
| [decorator_2](structural/decorator_2.py) | @functools.wraps & parameterized decorators |
| [decorator_3](structural/decorator_3.py) | making use of @singledispatch decorator to handle different input types |
| [flyweight](structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state |

---

## Videos

[Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg)

[Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0)

[You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I)

[Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU)

---
Look up any design pattern and see what a fluent Python developer would
*actually* write — the classic GoF form, the pythonic form, and where the
standard library already does it — with an honest verdict when the right
answer is "don't". All 23 Gang of Four patterns plus Python-native and
modern ones, every example typed, tested, and runnable.

## Use it

Each pattern is a folder — read them in this order:

```
patterns/structural/decorator/
├── README.md # the problem, the trade-offs, the verdict
├── naive.py # the classic 1994 translation
├── pythonic.py # what you actually write in Python
└── real_world.py # where the stdlib already does this
```

Run any example: `uv run python -m patterns.structural.decorator.pythonic`

Give it to your agents (MCP server with search, runnable examples, and
pattern recommendations):

```bash
claude mcp add design-patterns -- uv run --directory <this-repo> python-design-patterns-mcp
```

## Catalog

Based on [python-patterns.guide](https://python-patterns.guide/).

<!-- catalog:begin (generated: make readme) -->

### Principles

| Pattern | Verdict | Problem it solves |
|---|---|---|
| [Composition Over Inheritance](patterns/principle/composition_over_inheritance/) | ✅ pythonic | Vary independent behaviors without one subclass per combination of them. |

### Python-native

| Pattern | Verdict | Problem it solves |
|---|---|---|
| [Global Object](patterns/python/global_object/) | ⚠️ use with care | Give a whole program shared access to a constant or a pre-built object by assigning it at module level. |
| [Prebound Method](patterns/python/prebound_method/) | ✅ pythonic | Offer module-level functions that share state, by binding the methods of one hidden instance to module globals. |
| [Sentinel Object](patterns/python/sentinel_object/) | ✅ pythonic | Mark 'no value here' unambiguously when None itself is a legitimate value. |

### Creational (GoF)

| Pattern | Verdict | Problem it solves |
|---|---|---|
| [Abstract Factory](patterns/creational/abstract_factory/) | 🔄 prefer alternative | Let code build families of related objects without naming their concrete classes. |
| [Builder](patterns/creational/builder/) | ⚠️ use with care | Assemble a complex object step by step, so the assembly process is reusable and readable. |
| [Factory Method](patterns/creational/factory_method/) | 🔄 prefer alternative | Let a class defer which helper object it constructs, so subclasses or callers can substitute another. |
| [Prototype](patterns/creational/prototype/) | 🔄 prefer alternative | Create new objects by copying a pre-configured exemplar instead of constructing from scratch. |
| [Singleton](patterns/creational/singleton/) | 🔄 prefer alternative | Guarantee a class has exactly one instance and give the whole program access to it. |

### Structural (GoF)

| Pattern | Verdict | Problem it solves |
|---|---|---|
| [Adapter](patterns/structural/adapter/) | ✅ pythonic | Make an existing class usable through the interface your code expects, without editing either side. |
| [Bridge](patterns/structural/bridge/) | 🔄 prefer alternative | Let an abstraction and its implementation vary independently, instead of multiplying subclasses across both axes. |
| [Composite](patterns/structural/composite/) | ✅ pythonic | Let callers treat a single object and a whole tree of objects through one interface. |
| [Decorator](patterns/structural/decorator/) | ✅ pythonic | Add behavior around an object or callable without editing it or subclassing it. |
| [Facade](patterns/structural/facade/) | ✅ pythonic | Give a complicated subsystem one simple entry point for the common case. |
| [Flyweight](patterns/structural/flyweight/) | ⚠️ use with care | Support huge numbers of fine-grained objects by sharing immutable instances instead of duplicating them. |
| [Proxy](patterns/structural/proxy/) | ⚠️ use with care | Stand in for another object to control access to it — deferring, guarding, or instrumenting the real thing. |

### Behavioral (GoF)

| Pattern | Verdict | Problem it solves |
|---|---|---|
| [Chain of Responsibility](patterns/behavioral/chain_of_responsibility/) | 🔄 prefer alternative | Pass a request along a line of handlers until one of them takes it. |
| [Command](patterns/behavioral/command/) | ⚠️ use with care | Package a request as an object so it can be queued, logged, undone, or executed later by code that doesn't know its details. |
| [Interpreter](patterns/behavioral/interpreter/) | 🔄 prefer alternative | Represent a small language's grammar as data and evaluate sentences in it. |
| [Iterator](patterns/behavioral/iterator/) | ✅ pythonic | Traverse a container's elements without exposing how the container stores them. |
| [Mediator](patterns/behavioral/mediator/) | ⚠️ use with care | Stop a web of objects from referencing each other by routing their interactions through one coordinator. |
| [Memento](patterns/behavioral/memento/) | ⚠️ use with care | Capture an object's state so it can be restored later, without exposing its internals. |
| [Observer](patterns/behavioral/observer/) | ✅ pythonic | Notify interested parties when something changes, without the subject knowing who they are. |
| [State](patterns/behavioral/state/) | ⚠️ use with care | Change an object's behavior when its internal state changes, without an if-forest over a mode flag. |
| [Strategy](patterns/behavioral/strategy/) | 🔄 prefer alternative | Make an algorithm interchangeable at runtime without the caller knowing which variant it got. |
| [Template Method](patterns/behavioral/template_method/) | 🔄 prefer alternative | Fix an algorithm's skeleton while letting callers vary individual steps. |
| [Visitor](patterns/behavioral/visitor/) | 🔄 prefer alternative | Run a new operation over every node of an object structure without adding a method to every node class. |

### Modern Python

| Pattern | Verdict | Problem it solves |
|---|---|---|
| [Async Producer/Consumer](patterns/modern/async_producer_consumer/) | ⚠️ use with care | Decouple work generation from work processing under asyncio, with bounded memory and clean shutdown. |
| [Context Manager](patterns/modern/context_manager/) | ✅ pythonic | Guarantee acquire/release pairing around a block of code, even when it raises. |
| [Dependency Injection](patterns/modern/dependency_injection/) | ✅ pythonic | Hand an object its collaborators instead of letting it construct them, so they can be swapped — above all in tests. |
| [Registry](patterns/modern/registry/) | ✅ pythonic | Let implementations announce themselves by name, so dispatch is a lookup instead of an if/elif ladder. |
| [Repository](patterns/modern/repository/) | ⚠️ use with care | Keep domain logic ignorant of how objects are stored, behind a collection-like interface. |
<!-- catalog:end -->

More in [docs/](docs/index.md) — verdict definitions, MCP reference, contributing.
25 changes: 23 additions & 2 deletions docs/contributing.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# contributing
# Contributing

_Written in the docs phase._
## Workflow

Branches flow `main ← staging ← feat/<slug>`. PRs target `staging`; `main`
takes only reviewed milestone merges. CI (3.11/3.12/3.13) must pass.

## Adding a pattern unit

1. Scaffold: `/new-pattern <group>/<slug> "Name"` (Claude Code) or copy an
existing unit's shape.
2. Fill the frontmatter — every key; `id` must equal `<group>/<slug>`; pick
the verdict per [verdicts.md](verdicts.md). The catalog loader validates
this in CI and fails loudly.
3. Write the three variants (see [how-to-read-this-repo.md](how-to-read-this-repo.md)
for what each is for) and behavioral tests for all of them.
4. `make check` — ruff, mypy --strict, pytest must all pass.
5. `make readme` — regenerate the catalog table (CI rejects a stale one).

## Quality bar

- Full type hints; import-safe modules (no side effects at import).
- Tests assert behavior, not "it runs".
- Prose: one page, problem-first, no UML, no history lessons.
27 changes: 25 additions & 2 deletions docs/how-to-read-this-repo.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# how-to-read-this-repo
# How to read this repo

_Written in the docs phase._
Every pattern lives in `patterns/<group>/<slug>/` with the same five parts:

| File | Job |
|---|---|
| `README.md` | YAML frontmatter (machine-readable metadata) + one page of prose: problem → naive → pythonic → in the wild → verdict |
| `naive.py` | The Gang-of-Four/Java translation, faithfully — even where it looks silly in Python. It exists so you can diff it against `pythonic.py` and *see* what the language absorbs. |
| `pythonic.py` | What a fluent Python developer writes for the same problem. When the pattern collapses into a language feature, this file shows the collapse and names it. |
| `real_world.py` | A small program using the stdlib's own embodiment of the pattern. |
| `tests/` | Behavioral tests for all three variants. |

## Where to start

- Reading for education: start with `principle/composition_over_inheritance`,
then any pattern whose *symptom* you recognize (the frontmatter lists them).
- Solving a problem now: search the catalog through the [MCP server](mcp.md)
(`recommend_pattern`) or skim the README table's "problem it solves" column.
- Every example runs: `uv run python -m patterns.<group>.<slug>.<variant>`.

## Groups

`principle` · `python` (patterns native to the language, from
[python-patterns.guide](https://python-patterns.guide/)) · `creational` /
`structural` / `behavioral` (the GoF 23) · `modern` (post-GoF additions:
DI, Repository, Context Manager, Registry, async producer/consumer).
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Documentation

- [How to read this repo](how-to-read-this-repo.md)
- [Verdicts](verdicts.md) — what `pythonic` / `use-with-care` / `prefer-alternative` mean
- [How to read this repo](how-to-read-this-repo.md) — the unit anatomy and where to start
- [Verdicts](verdicts.md) — what / ⚠️ / 🔄 mean, and who decides
- [MCP server](mcp.md) — connect agents to the catalog
- [Contributing](contributing.md)
- [Contributing](contributing.md) — adding or improving a pattern unit
16 changes: 14 additions & 2 deletions docs/verdicts.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# verdicts
# Verdicts

_Written in the docs phase._
Every unit's frontmatter carries one verdict — the catalog's honest answer to
"should I write this in Python?"

| Verdict | Meaning |
|---|---|
| ✅ `pythonic` | Use it as shown in `pythonic.py`; the pattern (in its Python form) is what we'd genuinely recommend. |
| ⚠️ `use-with-care` | Legitimate uses exist, but each caveat in the frontmatter names a concrete failure mode. Read them first. |
| 🔄 `prefer-alternative` | The honest answer is usually "don't". The naive form exists for study; `pythonic.py` shows what to write instead (e.g. Singleton → module global, Visitor → `functools.singledispatch`). |

Where [python-patterns.guide](https://python-patterns.guide/) has a chapter,
its verdict wins and the unit links it. Where it doesn't, we reason from the
same principles: composition over inheritance, callables over class
hierarchies, the language's own features over ceremony.
71 changes: 71 additions & 0 deletions src/design_patterns/readme_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Generate the README catalog table from frontmatter -- and keep it honest.

``python -m design_patterns.readme_table`` rewrites the block between the
markers in README.md; ``--check`` exits non-zero if the block has drifted,
which CI runs so the table can never rot.
"""

from __future__ import annotations

import sys
from pathlib import Path

from design_patterns.catalog import find_patterns_root, load_catalog

BEGIN = "<!-- catalog:begin (generated: make readme) -->"
END = "<!-- catalog:end -->"

_GROUP_ORDER = ["principle", "python", "creational", "structural", "behavioral", "modern"]
_GROUP_TITLES = {
"principle": "Principles",
"python": "Python-native",
"creational": "Creational (GoF)",
"structural": "Structural (GoF)",
"behavioral": "Behavioral (GoF)",
"modern": "Modern Python",
}
_VERDICT_BADGES = {
"pythonic": "✅ pythonic",
"use-with-care": "⚠️ use with care",
"prefer-alternative": "🔄 prefer alternative",
}


def render_table() -> str:
catalog = load_catalog()
lines: list[str] = []
for group in _GROUP_ORDER:
members = [p for p in catalog.patterns if p.group == group]
if not members:
continue
lines.append(f"\n### {_GROUP_TITLES[group]}\n")
lines.append("| Pattern | Verdict | Problem it solves |")
lines.append("|---|---|---|")
for p in sorted(members, key=lambda p: p.slug):
link = f"[{p.name}](patterns/{p.id}/)"
lines.append(f"| {link} | {_VERDICT_BADGES[p.verdict]} | {p.problem} |")
return "\n".join(lines) + "\n"


def apply(readme: Path) -> str:
text = readme.read_text()
head, rest = text.split(BEGIN, 1)
_, tail = rest.split(END, 1)
return f"{head}{BEGIN}\n{render_table()}{END}{tail}"


def main() -> None:
readme = find_patterns_root().parent / "README.md"
fresh = apply(readme)
if "--check" in sys.argv:
if fresh != readme.read_text():
print("README catalog table is stale: run `make readme`", file=sys.stderr)
raise SystemExit(1)
print("README catalog table is current")
return
readme.write_text(fresh)
print(f"wrote {readme}")


if __name__ == "__main__":
main()
Loading