Template: Python
Template feature: when you spawn a new repo, the user must edit name and [project.scripts] at minimum. Everything else is good to go.
[project]
name = "my-package" # <-- EDIT THIS per project
dynamic = ["version"]
description = "One thing, done well."
readme = "README.md"
license = {text = "Apache-2.0"}
requires-python = ">=3.10"
dependencies = []
[tool.hatch.version]
path = "src/my_package/__init__.py"
[project.scripts]
my-package = "my_package:main" # <-- EDIT THIS per project
[dependency-groups]
dev = [
"pytest>=9.0",
"ruff>=0.15",
"fastship",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 160
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "W"]
[tool.fastship]
branch = "main"Template feature: when spawning, the user renames my_package/ to their package name. This is the only manual step (besides pyproject.toml edits).
__version__ = "0.0.1"
def main() -> None:
"""CLI entry point. Replace with actual logic."""
print(f"{__package__} v{__version__}")Why: __version__ as a simple literal is what fastship's ship-bump expects — it rewrites this line directly. main() is what [project.scripts] points at.
from . import main
if __name__ == "__main__":
main()Template feature: zero-dep, zero-config. Every Python CLI should support this — it's the stdlib way. No action needed per project.
Template feature: empty marker file. One-touch, no content, never edited.
Empty file. Its existence tells mypy / pyright / ty that the package is typed, so when other projects import your package, the type checker trusts it.
3.12
Template feature: uv reads this automatically — run uv sync and it downloads + uses exactly this Python version. Never edit per project unless you need a different version.
from my_package import __version__, main
def test_version():
assert __version__ is not None
def test_main_runs():
"""Doesn't crash, returns None (implicitly)."""
main()Template feature: the import path (my_package) is what the user changes. One smoke test is enough for a template — real tests come with the project.
<!-- do not remove -->Template feature: fastship's ship-changelog and ship-gh look for this HTML comment as an insertion point. Without it, they refuse to run. One line, never touched.
Template feature: standard OSS license. GitHub auto-detects this by filename. No edits needed — but if you want MIT instead, swap the file.
__pycache__/
*.pyc
dist/
*.egg-info/
.atlas/
Template feature: atlas/ excluded so atlas . doesn't recursively include its own output.
Template feature: having CI from the start means every spawned repo has green checks on day one. No secret setup needed for uv + basic pytest.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --group dev
- run: uv run pytest
- run: uv run ruff check src/When you gh repo create new-project --template 1iis/my-python-template:
- GitHub copies every file — literally
git clone --bare+ push to new repo - No git history — the new repo starts with a single initial commit, no link to the template
- No branch protections, no webhooks, no Actions secrets — those are repo-level settings, not file-level. Template only carries file content.
--cloneis optional — without it, yougit clonethe new repo separately. With it, one command creates + clones.
So the workflow is:
# One-time template creation
gh repo create my-python-template --public --template
git clone git@github.com:1iis/my-python-template
# ... populate files, push ...
# Per-project, one command
gh repo create atlas --template 1iis/my-python-template --public --clone
# then edit pyproject.toml name, rename src/my_package/ → src/atlas/Alright, makes sense on the missing files — those are standard enough to not need active management.
Here's the KISS uv lifecycle for the template, from zero to shipped.
There's no .venv folder, no activation, no pip, no pyenv. uv handles everything implicitly.
# Spawn from template
gh repo create atlas --template 1iis/py --public --clone
cd atlas
# Rename package directory
mv src/my_package src/atlas
# Install everything + create .venv
uv sync --group devThat's it. uv sync reads .python-version, downloads the right Python if missing, creates .venv/, resolves all deps, writes uv.lock. No manual python -m venv, no pip install.
You'll see .venv/ appear in the project root — but you never source .venv/bin/activate. Instead:
uv run pytest # run tests
uv run ruff check src/ # lint
uv run ruff format src/ # format
uv run atlas # run your CLIuv run activates the venv automatically for that command. No shell state to manage.
uv add requests # runtime dep → pyproject.toml + uv.lock
uv add --group dev pytest # dev dep → [dependency-groups] dev
uv sync # install everythingship-bump --part 2 # bump patch: 0.0.1 → 0.0.2
ship-pypi # build + upload to PyPIThese are already installed via uv sync --group dev. No activation needed.
Run it after:
git pull(someone changed deps)- switching branches
- manually editing
pyproject.tomldeps
That's the whole daily loop. No venv juggling, no activation scripts, no pip freeze.