Architecture and engineering guide
Use this guide when deciding where code belongs, adding a dependency, or changing delivery assets. For executable procedures and troubleshooting, use the development guide.
Default design
The repository has one installable package and one thin application boundary:
CLI (Typer, settings, logging) -> core behavior (values and specific exceptions)
src/template_python/core.py contains deterministic behavior with no
environment, logging, or CLI side effects. cli.py loads settings, configures
logging, and calls the core. __init__.py exports reusable behavior without
importing the CLI; __main__.py is only an entry point.
This keeps library consumers from needing to understand Typer, environment loading, or process-global logging just to call a function.
Where to change code
| Change | Primary location | Contract to test |
|---|---|---|
| Reusable behavior | src/template_python/core.py or a focused new module |
Values, normalization, and specific exceptions in tests/test_core.py or a matching new test module |
| Command, option, or user-facing error | src/template_python/cli.py |
stdout, stderr, and exit codes in tests/test_cli.py |
| Environment configuration | src/template_python/settings.py and .env.template |
Defaults, valid values, and rejection in tests/test_settings.py |
| Logging setup | src/template_python/logging.py |
Level validation and emitted messages in tests/test_logging.py |
| Public library API | src/template_python/__init__.py |
Import and call the public API without initializing the CLI |
| Build or dependency change | pyproject.toml and uv.lock |
Installed wheel behavior via make package-check |
Keep the cognitive cost small
Defaults are deliberate. Change them for a concrete requirement, not a possible future one.
| Decision | Default | Reconsider when... |
|---|---|---|
| Where to put new behavior | A named function in a focused package module | A concrete external I/O requirement needs an application adapter |
| Whether to share code | Keep it local and name it for its responsibility | Multiple real call sites have the same stable behavior |
| Whether to add a class or framework | Prefer functions and explicit arguments | State or lifecycle ownership is necessary and simpler composition is insufficient |
| Whether to add a dependency or tool | Use the standard library and existing toolchain | A distinct, measurable gap justifies its runtime and maintenance cost |
| Whether to add an optional asset | Leave it unused or remove it | The project's actual delivery or development needs require it |
- Avoid generic
utils/commonbuckets, base-class hierarchies, service locators, and speculative dependency-injection frameworks. - Use names that reveal inputs, outputs, and responsibility. A reader should not need to follow several layers to understand a small operation.
- Return values or raise specific exceptions. Do not turn failures into success-shaped defaults, suppress errors broadly, or log secrets.
- Keep one authoritative procedure for each task. Link to it rather than copying commands into several documents or workflows.
For a non-obvious decision, include the problem, simplest alternative, chosen trade-off, and verification in the PR description. A separate design document is only useful when the rationale needs to outlive the change.
Sources of truth
| Concern | Source |
|---|---|
| Metadata, dependencies, tool configuration | pyproject.toml |
| Resolved dependency versions | uv.lock |
| Local and CI commands | Makefile |
| Minimum Python / tested versions | pyproject.toml / .github/workflows/test.yaml |
| Configuration schema / example overrides | src/template_python/settings.py / .env.template |
| Container behavior | Dockerfile and compose.yml |
| Documentation navigation | mkdocs.yml |
Update declarations and derived assets together. Dependency declarations change
in pyproject.toml; regenerate uv.lock with uv lock. CI should call Make
targets instead of maintaining another implementation of the same checks.
Runtime rules
- The public package must remain importable without configuring logging or reading environment variables.
- Core modules do not depend on Typer or settings. The CLI translates their values and exceptions into user-facing output and exit codes.
ProjectSettingsownsPROJECT_*environment and optional.envloading. Do not add a second dotenv loader.- Configure root logging once in the CLI callback. Library modules only obtain named loggers.
- Expose commands through
[project.scripts]; keeppython -m template_pythonas a convenience path.
Quality gate
Use the checks by change type. The Python gate checks formatting, types, lint, workflow rules, and all tests with branch coverage of at least 95%. It builds wheel and sdist, then runs the installed wheel's CLI and module entry point in isolation. It does not smoke-test the sdist installation.
Test observable contracts, including failure paths, instead of coupling tests to internal structure. Keep stdout, stderr, exit codes, and import-time behavior stable unless the change explicitly requires a compatibility break.
Delivery
- Pull requests and supported branches run Python and container validation.
mainpublishes the bilingual documentation with GitHub Pages artifacts.v*tags publish multi-platform images to Docker Hub and GHCR.- GHCR releases include build provenance tied to the pushed digest.
- Azure Static Web Apps deployment remains manual.
The release workflow verifies that the tag is exactly v followed by the
pyproject.toml version before publishing. It requires DOCKERHUB_USERNAME and
DOCKERHUB_TOKEN secrets for Docker Hub; GHCR uses the workflow's GitHub token.
The manual site workflow requires AZURE_STATIC_WEB_APPS_API_TOKEN. These
credentials are not required for local development. PyPI publishing is not
configured.
Third-party actions are pinned by commit SHA. Workflows use explicit minimum permissions, disabled checkout credentials, bounded timeouts, and cancellation only for superseded non-release runs.
Adopting the template
Do this once when creating a project, before enabling publishing.
- Set the project identity: distribution name, description, version, URLs, and
console script in
pyproject.toml. Confirm the license is appropriate. - If renaming the import package, rename
src/template_python/and update imports, test references, the script target, module-entry commands, and tool paths such as coverage. Preservepy.typedfor the typed package. - Update settings defaults,
.env.template, notebook examples, Makefile smoke commands and image defaults, Dockerfile entry point and labels, Compose, Dev Container name, documentation URLs, workflow image names, and README badges. Distribution names use hyphens; Python import names use underscores. - Remove unneeded assets using the table below; do not leave dead CI references.
- Run
uv lockand checks for the retained assets. Verify the renamed installed CLI, not just a direct source import. - Configure repository publishing settings and secrets only for retained delivery workflows. Match the package version to the release tag.
Optional assets
Notebook, docs, containers, registries, Azure, and the Dev Container are independent examples. They must consume the installed package rather than creating alternative application behavior.
| Remove this asset | Also remove or update |
|---|---|
| Notebook | notebooks/, the notebook dependency group, and jupyterlab Make target |
| Documentation site | docs/, mkdocs.yml, the docs group and Make targets, both site deployment workflows, and the docs command in test.yaml |
| Containers | Dockerfile, Compose, .dockerignore, Docker Make targets/variables, and container validation/release workflows |
| Registry publishing only | Unneeded jobs or the container-release workflow; keep local container checks if still useful |
| Manual site deployment only | azure-static-web-app.yaml and deployment references |
| Dev Container | .devcontainer/ and setup references |
In the same change, regenerate the lock file if groups changed and clean up badges, links, and guide entries. Avoid keeping an unused workflow that requires credentials.
MkDocs is intentionally constrained below version 2 while multilingual output depends on the MkDocs 1 plugin ecosystem.
Change checklist
- [ ] The change has one clear purpose and uses the module boundaries above.
- [ ] Tests cover the expected values and failures; compatibility changes are explicit.
- [ ] Dependency declarations, lock file, and affected delivery assets agree.
- [ ] Commands reuse existing Make targets; any new tool fills a demonstrated gap.
- [ ] Affected English and Japanese documentation is updated without duplicating procedures.
- [ ] Checks for each changed area pass, including installed-package checks where relevant.