This guide is for maintainers, contributors, and architects working on the python-patterns repository — as opposed to developers simply reading the patterns (covered in the User Guide). It documents how the collection is structured, how its self-testing machinery works, the toolchain and CI that keep it consistent, the contribution contract, and the known issues and improvement opportunities surfaced by the project's own issue tracker.
1. Executive Summary
python-patterns is a teaching catalog, not a deployable application or a published library. Its "product" is a set of ~33 self-contained Python files, each demonstrating one design pattern or idiom, grouped by the classic Gang of Four taxonomy plus a few project-specific categories.
| Aspect | Reality |
|---|---|
| What it is | A curated reference collection of design-pattern implementations in Python |
| What it is not | Not a service, not a CLI tool, not an importable dependency library |
| Runtime dependencies | None — every pattern is standard-library only |
| The "build" output | There is none; the source files are the deliverable |
| The "tests" | Embedded doctests in every pattern file, plus a parallel tests/ suite of pytest unit tests |
| Quality gates | black, isort, flake8, mypy, pyupgrade, codespell, run via tox and Travis CI |
| Supported Python | 3.10 – 3.13 (requires-python = ">=3.10") |
| License | None declared in this revision (see §11 Known Issues — this is an open tracker item) |
Important
Because there is no service and no published package, the operational concerns here are entirely about repository hygiene: keeping every pattern consistent in style, ensuring every documented example actually runs, and managing contributions. There is nothing to deploy, monitor, scale, or secure at runtime. This guide reframes the usual "deployment / operations" chapters accordingly.
2. Architecture
The repository has two architectures worth understanding: the content taxonomy (how patterns are organized) and the self-verification architecture (how every example stays correct).
2.1 Content taxonomy
patterns/
├── creational/ 7 patterns — object creation
│ ├── abstract_factory.py borg.py builder.py factory.py
│ ├── lazy_evaluation.py pool.py prototype.py
│ └── viz/ rendered .png class diagrams (one per pattern)
├── structural/ 10 patterns — object/class composition
│ ├── 3-tier.py adapter.py bridge.py composite.py decorator.py
│ ├── facade.py flyweight.py flyweight_with_metaclass.py
│ ├── front_controller.py mvc.py proxy.py
│ └── viz/
├── behavioral/ 17 patterns — communication & responsibility
│ ├── catalog.py chain_of_responsibility.py chaining_method.py
│ ├── command.py iterator.py iterator_alt.py mediator.py
│ ├── memento.py observer.py publish_subscribe.py registry.py
│ ├── servant.py specification.py state.py strategy.py
│ ├── template.py visitor.py
│ └── viz/
├── fundamental/ 1 pattern — foundational idioms
│ └── delegation_pattern.py
├── other/ 3 non-GoF patterns
│ ├── blackboard.py graph_search.py
│ └── hsm/hsm.py (hierarchical state machine + its own exceptions)
├── dependency_injection.py design-for-testability (3 variants)
└── __init__.py
Each viz/ subfolder holds pre-rendered *.py.png class diagrams (one per pattern) used by the README. These are generated artifacts, not source.
Note
The taxonomy is deliberately the same one the Gang of Four book uses, so the repository doubles as a navigable index into that canon. The
fundamental, design-for-testability (dependency_injection.py), andotherbuckets extend it with idioms the GoF set does not cover.
2.2 Self-verification architecture — the core design decision
The single most important architectural choice in this repository is that every pattern file is its own test. There is no separate "examples vs. tests" split for the demonstration code; the demonstration is a test.
┌─────────────────────────────────────────────┐
│ patterns/<category>/<name>.py │
│ │
│ """module docstring — the WHY + trade-offs"""│
│ │
│ class / function implementations │
│ │
│ def main(): │
│ """ │
│ >>> usage example │ ← doctest: doc + example + test
│ expected output │
│ """ │
│ │
│ if __name__ == "__main__": │
│ import doctest │
│ doctest.testmod() │ ← runs the doctest on execution
└─────────────────────────────────────────────┘
│ │
▼ ▼
python <file>.py pytest --doctest-modules patterns/
(runs that file's (discovers and runs EVERY pattern's
doctest, silent on doctest in one pass)
success)
In parallel, a conventional unit-test suite mirrors the pattern tree:
tests/
├── behavioral/ test_catalog.py, test_mediator.py, test_memento.py,
│ test_observer.py, test_publish_subscribe.py, test_servant.py,
│ test_state.py, test_strategy.py, test_visitor.py
├── creational/ test_abstract_factory.py, test_borg.py, test_builder.py,
│ test_factory.py, test_lazy.py, test_pool.py, test_prototype.py
├── structural/ test_adapter.py, test_bridge.py, test_decorator.py,
│ test_facade.py, test_flyweight.py, test_mvc.py, test_proxy.py
├── fundamental/ test_delegation.py
└── test_hsm.py
Important
Two test surfaces coexist: doctests inside
patterns/(which verify the documented usage examples) and pytest unit tests insidetests/(which verify behavior more rigorously). Thepyproject.tomladdoptswires both together with--doctest-modules. When you change a pattern, you may have to update both the in-file docstring and the matchingtests/file.
2.3 Negative space — what the architecture intentionally omits
- No runtime dependencies.
pyproject.toml'sdependenciesarray is empty. This is a hard design constraint: every pattern must work on a clean standard-library Python so it can be copied out in isolation. - No Singleton, God Object, or deep-inheritance examples. These are listed in the README as anti-patterns and are deliberately not implemented.
- No
interpreter.py. Although an "interpreter" row appears in the README's behavioral table, no such file exists in this revision (commit74151cf). Treat the on-disk file tree, not the README table, as the source of truth for what exists. - No build/packaging output. There is no
dist/, no wheel pipeline intended for PyPI distribution; the editable install exists only to support testing.
3. System Requirements
| Component | Requirement | Source of truth |
|---|---|---|
| Python | >=3.10 (CI runs 3.12) |
pyproject.toml requires-python; .travis.yml python: "3.12" |
| Standard library only | No runtime deps | pyproject.toml dependencies = [] |
| Git | For cloning / legacy tag access |
README |
| Dev toolchain | mypy, pyupgrade, pytest>=6.2.0, pytest-cov>=2.11.0, pytest-randomly>=3.1.0, black>=25.1.0, build>=1.2.2, isort>=5.7.0, flake8>=7.1.0, tox>=4.25.0, pipx>=1.7.1 |
pyproject.toml [project.optional-dependencies].dev |
Note
The
requirements-dev.txtfile lists the dev tools unpinned (flake8,black,isort,pytest,pytest-randomly,mypy,pyupgrade,tox). The authoritative pinned versions live inpyproject.toml'sdevextra. When the two drift, preferpyproject.toml.
4. Development Environment Setup
git clone https://github.com/faif/python-patterns.git
cd python-patterns
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
python -m pip install -e ".[dev]"
- [ ] Python 3.10+ active (
python --version) - [ ] Virtual environment created and activated
- [ ]
pip install -e ".[dev]"completed without errors - [ ]
pytestruns green - [ ]
./lint.shruns clean (bash-compatible shell)
Warning
The
Makefiletargets (make pyupgrade,make pylinter) assume an embeddedvenvat./venv, not a virtualenv elsewhere — thecheckvenvtarget hard-fails if$VIRTUAL_ENVis unset, andpyupgradelooks forvenv/bin/pip-compile. Create the venv exactly aspython -m venv venvfor the Makefile to work as written.
5. Testing
5.1 Test configuration
The pytest configuration lives in pyproject.toml under [tool.pytest.ini_options]:
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
addopts = "--doctest-modules --randomly-seed=1234 --cov=patterns --cov-report=term-missing"
doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"]
log_level = "INFO"
Key points:
--doctest-modulesmakes pytest collect and run the doctests embedded in the pattern files, in addition to thetests/unit tests.--randomly-seed=1234(frompytest-randomly) fixes the test execution order so randomized ordering is reproducible.--cov=patternsproduces a coverage report for thepatterns/package, with missing lines listed in the terminal.doctest_optionflagsenablesELLIPSIS(so...matches variable output like memory addresses) andNORMALIZE_WHITESPACE.
Note
A second, minimal pytest config exists at
pytest_local.ini(addopts = -q,testpaths = tests). It is a lightweight local override for running just the unit tests quietly, without coverage or doctest collection.
5.2 Running the tests
Everything (coverage + doctests)
pytest
Just the unit tests, quietly
pytest -c pytest_local.ini
Just the in-file doctests
pytest --doctest-modules patterns/
A single pattern's doctest, verbose
python -m doctest -v patterns/creational/factory.py
5.3 Coverage configuration
[tool.coverage.*] in pyproject.toml enables branch coverage with a per-test dynamic context and excludes boilerplate lines from the report:
[tool.coverage.run]
branch = true
source = ["./"]
relative_files = true
parallel = true
dynamic_context = "test_function"
[tool.coverage.report]
exclude_lines = [
"def __repr__", "if self\\.debug", "raise AssertionError",
"raise NotImplementedError", "if 0:",
"if __name__ == .__main__.:", "@(abc\\.)?abstractmethod",
]
Tip
The
exclude_linespatterns mean abstract methods,__repr__, and the__main__guards do not count against coverage — so a green coverage report reflects the pattern logic, not the scaffolding around it. An HTML report is configured to land incoverage_html_report/.
6. Quality Toolchain & CI
6.1 The linting pipeline
The canonical lint sequence is captured in lint.sh:
codespell --quiet-level=2 ./patterns # spelling
flake8 ./patterns --count --show-source --statistics
isort --profile black ./patterns # import ordering
tox # tests across env
mypy --ignore-missing-imports ./patterns || true
pytest ./patterns
pytest --doctest-modules ./patterns || true
shopt -s globstar && pyupgrade --py312-plus ./patterns/*.py
| Tool | Role | Config location |
|---|---|---|
black |
Code formatting (line length 88 via Makefile) | invoked directly / make pylinter |
isort |
Import sorting, --profile black |
lint.sh, Makefile |
flake8 |
Linting | [tool.flake8] in pyproject.toml: max-line-length = 120, ignore = ["E266","E731","W503"], exclude = ["venv*"] |
mypy |
Static type checking | [tool.mypy]: python_version = "3.12", ignore_missing_imports = true |
pyupgrade |
Modernize syntax to --py312-plus |
lint.sh, Makefile |
codespell |
Catch typos | .codespellignore |
Caution
There are two different flake8 line-length settings in play.
pyproject.toml's[tool.flake8]setsmax-line-length = 120, while theMakefile'spylintertarget passes--max-line-length 88(matching black's default) with a broader--select/--ignoreset. TheMakefilepath is the stricter one. When reconciling lint failures, know which entry point produced them.
6.2 Continuous integration
CI runs on Travis CI (.travis.yml):
os: linux
dist: noble
language: python
jobs:
include:
- python: "3.12"
env: TOXENV=py312
install:
- pip install codecov tox
script:
- tox
after_success:
- codecov
The pipeline installs tox + codecov, runs tox, and uploads coverage to Codecov on success.
6.3 tox
tox configuration lives inline in pyproject.toml under [tool.tox] as legacy_tox_ini:
[tox]
envlist = py312,cov-report
skip_missing_interpreters = true
usedevelop = true
Note
Most of the per-environment
toxcommand detail (the[testenv]and[testenv:cov-report]blocks) is commented out in the currentlegacy_tox_ini. The active configuration declares thepy312andcov-reportenvironments withusedevelop = true(editable install) andskip_missing_interpreters = true. If you are revamping CI, this commented block is the historical reference for what the full matrix used to run (flake8,coverage run -m pytest --doctest-modules, then a combined coverage report).
7. Module & Pattern Reference (selected internals)
Rather than re-document every pattern (the User Guide catalogs all of them), this section highlights the implementations whose internals are notable for a maintainer.
| Pattern | Internal technique worth knowing |
|---|---|
creational/borg.py |
Reassigns self.__dict__ to a class-level _shared_state dict — the entire Monostate trick is one line in __init__ |
creational/lazy_evaluation.py |
Two caching strategies: a descriptor (lazy_property.__get__) and a decorator (lazy_property2) that stash the result in the instance __dict__ / a _lazy__<name> attribute |
structural/flyweight.py |
Pools instances in a weakref.WeakValueDictionary keyed by value+suit, overriding __new__ (not __init__) |
structural/flyweight_with_metaclass.py |
Same pooling via a FlyweightMeta(type) metaclass intercepting __call__; verified by asserts, not a doctest |
behavioral/registry.py |
A RegistryHolder(type) metaclass auto-registers every subclass in a shared REGISTRY dict at class-creation time |
behavioral/memento.py |
A memento() closure snapshots obj.__dict__; the @Transactional descriptor rolls back on exception |
behavioral/strategy.py |
Uses a descriptor (DiscountStrategyValidator) with __set_name__/__set__/__get__ to validate the injected strategy |
behavioral/visitor.py |
Dispatches by walking node.__class__.__mro__ to find a visit_<ClassName> method, falling back to generic_visit |
structural/mvc.py |
The only file with a substantive executable __main__ — a Router plus sys.argv-driven command dispatch |
other/hsm/hsm.py |
A nested FSM with four states and its own exception hierarchy (UnsupportedMessageType, UnsupportedState, UnsupportedTransition) |
Tip
Several patterns lean on Python's data-model hooks (
__new__,__getattr__,__get__/__set__/__set_name__, metaclasses). These are exactly the places where a "port from Java" would look different — and the module docstrings frequently call out the Python-specific simplification (e.g. "in Python the interface is simply a callable").
7.1 Error/exception inventory
The patterns raise a small, well-defined set of exceptions — useful when writing or reviewing tests:
| Exception | Message | Location |
|---|---|---|
NotImplementedError |
Must provide implementation in subclass. |
other/blackboard.py (abstract expert) |
NotImplementedError |
You should implement this! |
structural/composite.py (Graphic.render) |
KeyError |
No controller registered for path '{path}' |
structural/mvc.py (Router.resolve) |
KeyError |
... not in the model's item list. |
structural/mvc.py (ProductModel.get) |
ValueError |
Invalid Value for Param: {param} |
behavioral/catalog.py (all four catalog classes) |
ValueError |
Unsupported shape type |
behavioral/servant.py (GeometryTools) |
ValueError |
Discount cannot be applied due to negative price resulting. {name} |
behavioral/strategy.py (DiscountStrategyValidator) |
UnsupportedMessageType / UnsupportedState / UnsupportedTransition |
(no message) | other/hsm/hsm.py |
8. Code Conventions
The contribution guidelines in the README codify the house style:
- Module docstring required. Every pattern starts with a docstring describing the pattern, ideally with
*What is this pattern about?,*What does this example do?,*Where is the pattern used practically?,*References:, and a*TL;DRline.structural/facade.pyis cited as the gold-standard detailed example;behavioral/template.pyas an acceptable shorter one. - "Examples in Python ecosystem" sections are encouraged where real-world equivalents exist (e.g. Django Signals for Observer,
ast.NodeVisitorfor Visitor, Django middleware for Chain of Responsibility). - Type hints throughout. Most files annotate parameters and return types;
mypyruns in CI. - Black formatting + isort ordering are mandatory before submitting.
- Doctests as examples. New patterns put their worked example in a
main()docstring as a doctest so it is verified automatically. - README update. When a pattern is added or changed, the corresponding README table/section must be updated to match.
Important
The README explicitly states the project's editorial stance: emphasize why you'd choose a pattern (and its trade-offs) over how to implement it. Contributions that add an exotic implementation without explaining when to reach for it run against the grain of the project.
9. Contribution Workflow
1. Implement / modify a pattern under patterns/<category>/<name>.py
2. Add a module docstring (why + example + references + TL;DR)
3. Add a main() doctest demonstrating usage
4. Add/adjust the mirror test under tests/<category>/test_<name>.py
5. Run quality gates:
black .
./lint.sh (bash-compatible shells)
tox or tox -e ci37
6. Update the matching README section/table
7. Open a PR
Pre-submission checklist (from the README's "Travis CI" contribution note):
- [ ]
black .applied - [ ]
tox(ortox -e ci37) passes - [ ]
./lint.shclean (if you have a bash-compatible shell) - [ ] README updated to reflect the change
Note
The README also points contributors at CodeTriage for issue/PR triage, and notes that Python 2-compatible versions of some patterns are preserved on the
legacygit tag — relevant if a contributor asks where Python 2 support went.
10. Security Analysis
There is no running service, no network surface, no authentication, no secrets, and no third-party runtime dependencies — so the conventional application threat model does not apply. The realistic security considerations are repository-and-supply-chain concerns:
| Concern | Assessment |
|---|---|
| Runtime dependency CVEs | None — standard-library only, so there is no third-party runtime attack surface |
| Dev-toolchain supply chain | The dev extra pulls black/flake8/mypy/tox/etc. from PyPI; pin versions (already done in pyproject.toml) and enable 2FA on the project's PyPI/GitHub accounts |
| CI token exposure | Travis + Codecov integration — keep the Codecov upload token and any Travis secrets out of the repo; they are not in the source tree |
| Untrusted example input | Patterns like mvc.py read sys.argv and graph_search.py/hsm.py process caller-supplied data, but they are illustrative; they perform no file/network/eval operations and are safe to run locally |
| Code-injection vectors | No use of eval, exec, pickle.loads on untrusted data, subprocess, or os.system in the pattern files |
Caution
The biggest governance gap is the missing LICENSE file (tracked as issue #143). Without an explicit license, the default is "all rights reserved," which complicates downstream reuse despite the educational intent. This is a legal/compliance issue rather than a runtime security one, but it is the most consequential open item for anyone copying code out of the repo.
11. Known Issues & Improvement Opportunities
These are drawn from the project's own open GitHub issues (community-reported, real signal):
| # | Title | Type | Maintainer note |
|---|---|---|---|
| #143 | Needs a COPYRIGHT/LICENSE file | Governance | Highest-impact: blocks clean downstream reuse |
| #468 | Visualizing pattern relationships with Mermaid diagrams | Enhancement | README already uses some mermaid graphs; this would extend them |
| #373 | Support for type hints | Enhancement | Largely already in place; tracks completing coverage |
| #433 | Remove deprecated Python versions | Maintenance | Aligns with requires-python >= 3.10 |
| #333 | Notebook version of the repo | Enhancement | Alternative presentation of the catalog |
| #277 | Code coverage (infrastructure) | Infrastructure | Coverage is configured via pytest-cov + Codecov; this tracks surfacing/raising it |
| #278 | Add DAO / DTO patterns | New content | Would extend the catalog |
| #245 | Add an automation-testing pattern | New content | Fits the design-for-testability bucket |
| #279 | List anti-patterns in README | Docs | Partially done — the README already has an Anti-Patterns section |
| #120 | 3-tier.py and mvc.py are almost identical |
Refactor/dedup | Both model a product catalog; worth differentiating or cross-referencing |
| #244 | Functional programming patterns (question) | Discussion | Scope question for the catalog |
| #437 | Replace setuptools with pyproject.toml |
Build | Closed — the project now uses pyproject.toml with the setuptools build backend |
Tip
Issue #120 (3-tier vs. MVC near-duplication) is a genuine code-quality observation: both files use the same milk/eggs/cheese product model and a similar layered structure. If you touch either, consider adding a comment cross-referencing the other and clarifying the strict vs. non-strict layering distinction that motivates having both.
11.1 Additional maintainer-visible gaps
- README vs. file tree drift. The README's behavioral table lists an
interpreterpattern, but nointerpreter.pyexists in this revision. Either add the implementation or correct the table. (Per the README's own contribution rule #6, the table should track the files.) - Dev dependency duplication.
requirements-dev.txt(unpinned) andpyproject.toml'sdevextra (pinned) list overlapping but not identical tool sets. Consolidating to a single source would prevent drift. - Two flake8 line lengths (120 in
pyproject.toml, 88 in theMakefile) — reconcile to one. - Mostly-commented
toxmatrix. The activelegacy_tox_inideclares environments but leaves the command detail commented out; the real test commands effectively come frompyproject.toml'saddopts. Consider makingtoxself-contained again.
12. Quick Reference
Commands
| Task | Command |
|---|---|
| Clone | git clone https://github.com/faif/python-patterns.git |
| Dev install | python -m pip install -e ".[dev]" |
| Run one pattern (verify) | python patterns/<cat>/<name>.py |
| Run one pattern (verbose) | python -m doctest -v patterns/<cat>/<name>.py |
| Run all doctests | pytest --doctest-modules patterns/ |
| Full test + coverage | pytest |
| Quick unit tests | pytest -c pytest_local.ini |
| Format | black . |
| Full lint sequence | ./lint.sh |
| Lint via Makefile | make pylinter (requires ./venv) |
| CI environment | tox |
| Python 2 versions | git checkout legacy |
Key files
| File | Purpose |
|---|---|
pyproject.toml |
Single source of truth: build, deps, pytest, coverage, mypy, flake8, tox |
pytest_local.ini |
Lightweight local pytest config (-q, tests only) |
lint.sh |
Canonical lint + test sequence |
Makefile |
pylinter / pyupgrade targets (assume ./venv) |
.travis.yml |
CI: Python 3.12, tox, Codecov |
requirements-dev.txt |
Unpinned dev tool list (prefer pyproject.toml dev extra) |
.codespellignore |
Spelling-checker allowlist |
README.md |
Catalog tables, mermaid diagrams, anti-patterns, contribution rules |
Configuration facts
| Setting | Value | Source |
|---|---|---|
| Minimum Python | 3.10 | pyproject.toml |
| CI Python | 3.12 | .travis.yml |
| Runtime dependencies | none | pyproject.toml |
| Random test seed | 1234 | pyproject.toml addopts |
| Coverage target package | patterns |
pyproject.toml |
| flake8 line length | 120 (pyproject) / 88 (Makefile) | both |
| mypy Python version | 3.12, ignore_missing_imports |
pyproject.toml |
| Build backend | setuptools.build_meta |
pyproject.toml |
| Packaged package | patterns |
pyproject.toml [tool.setuptools] |