Reference for scoping Copier templates. Seven structural families, based on where source lives, whether there's a distributable package, and what kind of non-code assets the project carries.
Shared across all templates regardless of shape: uv, Ruff (extending
~/.dev-tools/ruff.toml, not duplicating it), mypy --strict, pytest +
coverage gate, justfile, codespell, gitleaks. Only directory shape and
pyproject.toml target change between templates.
For anything published to PyPI or installed by other projects (import foo).
my-lib/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── my_lib/
│ ├── __init__.py
│ ├── py.typed
│ └── core.py
├── tests/
│ ├── __init__.py
│ └── test_core.py
└── docs/ # optional, mkdocs/sphinx
- src-layout is the PyPA-recommended default. It forces tests/tooling to run
against the installed package (
uv pip install -e .), not the working directory — catches "works because CWD is on sys.path" bugs early. py.typedmarker required for downstream type-checkers to trust inline hints once installed elsewhere.
Same skeleton as a library, plus an entry point — for anything with a
runtime entry point that isn't meant to be imported elsewhere (CLI tools,
but also GUI apps: tkinter, pygame, etc.). Matches fb-fcc-dcc /
fcc-dcc-tool shape, and also the shape of a repo like oop-irv-kalb's
individual chapters (see #5 for the multi-chapter case).
my-app/
├── pyproject.toml # [project.scripts] my-app = "my_app.__main__:app"
├── src/
│ └── my_app/
│ ├── __init__.py
│ ├── __main__.py # entry point stub — CLI app object, tkinter
│ │ # mainloop, etc., whatever you're building
│ ├── commands/ # CLI flavor: subcommand wiring, thin
│ │ ├── __init__.py
│ │ └── fetch.py
│ └── core/
│ └── processing.py
├── tests/
└── justfile
commands/(subcommand wiring, thin) vscore/(logic, testable without invoking the CLI runner) — same split you already use between bash orchestration andprocess_challenge_md.py. For a GUI flavor,commands/doesn't apply — justcore/for testable logic kept out of event handlers/callbacks.- Toolkit-agnostic by design (2026-08-23), same reasoning as #3's
framework-agnostic decision: no CLI/GUI toolkit dependency is added by
the template —
__main__.pystays a stub, and the README documents the recommendation instead: Typer for a CLI (type-hint-driven, matches themypy --strictphilosophy already in every template — chosen over Click) viauv add typer; tkinter for a GUI needs no install (stdlib); pygame/PySide etc. viauv addas needed. Baking in Typer as a real dependency would have made the broadened CLI/GUI name misleading, so the choice moved from "templated" to "documented," matching #3 and #5's precedent of accepting a little pre-wired boilerplate loss in exchange for one template instead of near-duplicates.
Distinct because there's a runtime entrypoint, not a distributable package, and non-Python assets matter.
my-app/
├── pyproject.toml
├── src/
│ └── my_app/
│ ├── __init__.py
│ ├── main.py # app factory / ASGI entrypoint
│ ├── api/
│ │ └── routes/
│ ├── models/ # ORM / pydantic models
│ ├── services/ # business logic
│ └── config.py
├── tests/
│ ├── unit/
│ └── integration/
├── migrations/ # alembic, if using SQLAlchemy
├── static/
└── templates/ # if server-rendered
api/(transport) vsservices/(logic) vsmodels/(data shape) keeps route handlers thin.- Django has its own
manage.py/ app-per-directory convention — doesn't fit this shape, would need its own template if ever used. - Framework-agnostic by design (2026-08-23): FastAPI and Flask differ
enough in app-factory pattern, routing, and test client that picking one
via a copier question would mean the same kind of heavy per-branch
conditionals rejected at the repo level (independent per-shape templates
instead of one root selector) — and splitting into two full templates
(
fastapi/,flask/) would duplicate all the shared tooling (justfile, prek.toml, ruff/mypy config) for what's really just a few lines ofmain.py/routes difference. So: no framework dependency is added by the template,main.pystays a stub, and the README documents what to do once you've picked one:- Framework:
uv add fastapi uvicorn[standard]oruv add flask. pytest-asyncio(only needed if you go FastAPI + async routes and want to test them directly, i.e.async def test_...):uv add --group dev pytest-asyncio, then addasyncio_mode = "auto"under[tool.pytest.ini_options]inpyproject.toml.alembic(only if using SQLAlchemy for migrations):uv add sqlalchemy alembic, thenalembic init migrations.- justfile: add a
serverecipe once a framework's picked, e.g.uv run --frozen uvicorn {{ project_slug }}.main:app --reloadoruv run --frozen flask --app {{ project_slug }}.main run --debug. - prek: no adjustments needed — ruff/mypy/complexipy/pytest hooks are already framework-agnostic.
- Framework:
- Dash (Plotly) also fits this shape (2026-08-24), documented as a
third option: Dash is Flask underneath (
app.serveris a real Flask instance), has a runtime entrypoint rather than being an importable package, and theservices//models//config.pysplit still applies — same reasoning that qualifies FastAPI/Flask. A few Dash-specific conventions worth calling out in the README rather than assuming FastAPI/Flask defaults:- Framework:
uv add dash(bundles Flask + Plotly.js + the React wrapper). assets/replacesstatic/— Dash auto-serves and hot-reloads everything in a folder literally namedassets/; it isn't optional naming, it's a Dash convention.pages/can replaceapi/routes/if using Dash's multi-page app support (dash.register_page) — Dash apps are usually callback-driven rather than REST-routed, soapi/may go unused entirely unless you add your own Flask routes viaapp.server.route(...).- Testing: Dash's own browser-based integration testing
(
dash.testing, Selenium-driven) is heavier than the default unit/integration split and is opt-in —uv add --group dev dash[testing]only if you need it; plainservices//models/logic stays testable the normal way without it. pytest-asynciodoesn't apply (Dash/Flask are sync);alembicguidance is unchanged if a Dash app uses SQLAlchemy underneath.
- Framework:
Notebook-driven, exploratory. Output is a figure or conclusion, not a destination. Reproducibility comes from re-running transforms, not from committing data.
my-analysis/
├── pyproject.toml
├── data/
│ ├── raw/ # immutable, gitignored, read-only
│ ├── interim/ # intermediate transforms
│ └── processed/ # final, analysis-ready
├── notebooks/
│ └── 01-explore.ipynb # numbered, throwaway exploration
├── src/
│ └── my_analysis/
│ ├── __init__.py
│ ├── data_loading.py
│ └── transforms.py
├── reports/
│ └── figures/
└── tests/
- Notebooks stay exploratory; anything reused gets promoted into
src/my_analysis/as a tested function. - Consider a
configs/dir if instrument/acquisition parameters need separate version control from code (relevant given lab infra work).
Generalized from an original "Data Pipeline" framing. Shape: one src-layout
package, one pyproject.toml/lockfile, and N independent, self-contained
subpackages under a units directory, each free to hold whatever it needs
internally, sharing a shared/ for common code. No notebooks. Distinct
from #4 — units aren't exploratory research, whatever's inside them is
considered "done" code.
my-project/
├── pyproject.toml
├── src/
│ └── my_project/
│ ├── cli.py # optional: `my-project run <name>`
│ ├── units/
│ │ ├── example_unit_a/
│ │ │ └── ... # whatever this unit needs
│ │ └── example_unit_b/
│ │ └── ...
│ └── shared/
│ ├── io.py
│ └── config.py
├── configs/ # optional, if units need config files
│ ├── example_unit_a.toml
│ └── example_unit_b.toml
├── tests/
│ ├── example_unit_a/
│ └── example_unit_b/
└── justfile
- Extending = new subpackage under
units/+ its own test dir, sharingshared/and (optionally) its own config file. - The template scaffolds the shape only — one example unit, empty inside —
and doesn't bake in any convention for what goes inside a unit, since
that varies by use case. Two known flavors:
- Production pipeline (the original motivating case: a Canvas LMS grade report generator with sub-pipelines per course/report type). Recommended internal convention, documented in the generated README but not enforced by the template: stage each unit as extract.py → transform.py → load.py (E/T/L), idempotent, landing data somewhere (file, DB, report).
- Book/tutorial study repo (e.g. working through a book chapter by
chapter, each chapter its own runnable unit with its own
[project.scripts]entry point, sharing one venv/lockfile — the shapeoop-irv-kalbalready uses). No E/T/L convention applies; a unit here is just "chapter N's code," e.g.main.pyplus whatever supporting modules that chapter needs.
- Overlaps loosely with the CLI template's commands/core split; the differentiator is that each unit is independent enough to be added, removed, or run on its own, rather than being a subcommand of one cohesive CLI.
Deliberately not src-layout. For fCC-style daily challenges or any script meant to run standalone. Kept as a template mainly because the fCC repo is a real, if outlier, use case — not because the pattern generalizes widely yet.
my-scripts/
├── pyproject.toml
├── shared/
│ └── helpers.py
├── challenge_042.py
├── challenge_043.py
└── tests/
- Settled (2026-08-23):
shared/is the default, not per-script duplication. Thefcc-coding-challengesrepo's current per-script duplication was an unexamined artifact of how the repo grew, not a deliberate choice — the template should default toshared/for helpers reused across scripts (e.g. common test scaffolding). - Settled (2026-08-23): one shared
pyproject.toml, mechanically identical to every other template (onedependencies = []/devgroup, one venv, nosrc/-layout) — not a "strict PEP 723" variant where each script declares its own inline deps. That was considered and rejected: it splits the world in two (the dev venv pytest runs in vs. each script's own ephemeral PEP-723 env), which forces testable logic out of scripts and intoshared/just to keep it reachable from tests — disproportionate complexity for a use case (fcc-coding-challenges-style daily exercises) that rarely needs script-specific third-party deps anyway. PEP 723 inline metadata (# /// scriptblock) stays available as a documented, optional pattern in the README — for a genuinely standalone one-off script meant to be copied elsewhere without the repo — but the template doesn't structurally commit to it.
Description only — not being templated for now.
my-workspace/
├── pyproject.toml # workspace root, uv workspace config
├── packages/
│ ├── core-lib/
│ │ ├── pyproject.toml
│ │ └── src/core_lib/
│ └── cli-tool/
│ ├── pyproject.toml
│ └── src/cli_tool/
└── tests/
uvsupports workspaces natively ([tool.uv.workspace]) — multiple installable packages sharing one lockfile, each independently versioned.- Only worth building once two+ packages need independent versioning.
Related but distinct pattern (not a uv workspace, not templated):
multi-target embedded projects like pico-qty-expl — separate on-device
targets (e.g. Pico W, QT Py ESP32-S3) in one repo, each with its own
typings/ dir for IntelliSense, no pyproject.toml, no installable
package, no shared Python code between targets:
pico-qty-expl/
├── HANDOFF.md
├── README.md
├── pico-qty-expl.code-workspace
├── pico-w/
│ ├── sensor_test.py
│ └── typings/
└── qtpy-s3/
├── blink.py
├── hdc3022.py
└── typings/
This is worth keeping as a documented pattern for future embedded projects,
but it isn't a Python packaging structure — no template needed unless the
pattern repeats enough to be worth automating the typings/ + workspace
scaffolding itself.
| Template | Layout | Notes |
|---|---|---|
| Library/Package | src-layout | PyPA default |
| Application (CLI/GUI) | src-layout | toolkit-agnostic (Typer/tkinter/etc. documented, not templated); commands/ vs core/ |
| Web Application (FastAPI/Flask/Dash) | src-layout | api/ vs services/ vs models/; Dash uses assets/ + pages/ instead of static/ + api/ |
| Data Analysis/Research | src-layout + data/ + notebooks/ | data/raw read-only |
| Multi-subpackage Project | src-layout + units/ + optional configs/ | N independent units, extensible; flavors: pipeline (E/T/L convention) or book/tutorial study repo |
| Single-file Script/Collection | flat, no src/ | shared/ for helpers |
| Monorepo/Multi-package Workspace | (description only, not templated) | uv workspace; embedded multi-target noted separately |