Skip to content

Tutorial

By the end of this tutorial, you'll have a Python package with a working CLI, a live documentation site, and CI that tests, lints, type-checks, and publishes to PyPI. The whole thing takes about 15 minutes.

Prerequisites

Step 1: Generate your package

uvx cookiecutter-pypackage

You'll be prompted for some values. See Prompts for details on each one.

[1/11] full_name (Audrey M. Roy Greenfeld): Your Name
[2/11] email (audreyfeldroy@example.com): you@example.com
[3/11] github_username (audreyfeldroy): your-github-username
[4/11] github_repo_owner (your-github-username):
[5/11] project_name (Python Boilerplate): My Package
[6/11] package_name (My-Package): my-package
[7/11] import_name (my_package):
[8/11] project_short_description (...): A short description of your package.
[9/11] pypi_username (your-github-username):
[10/11] author_website (https://audrey.feldroy.com/): https://example.com
[11/11] first_version (0.1.0):

GitHub setup is optional and defaults to no. If you opt in, repository visibility defaults to private and the generator shows every action before making changes:

Set up a GitHub repository now? [y/N]: y
Repository visibility [private/public] (private): public
Enable GitHub Pages? [Y/n]:

GitHub setup plan:
  - create https://github.com/your-username/my-package as a public repository
  - initialize Git and create the first commit
  - enable GitHub Pages (publishes a website)
  - enable the documentation deployment workflow
  - create the pypi environment
  - push main over SSH

Continue? [y/N]: y
Git initialized with first commit
GitHub repository created: https://github.com/your-username/my-package
GitHub Pages enabled for your-username/my-package
Documentation deployment workflow enabled for your-username/my-package
GitHub environment 'pypi' created for your-username/my-package
Pushed to https://github.com/your-username/my-package

To publish to PyPI, add a pending publisher at:
https://pypi.org/manage/account/publishing/
...

Your Python package project has been created successfully!

The transport in the plan follows gh's git_protocol setting, so it may say SSH or HTTPS. CI runs automatically on push. Check the Actions tab and you should see it pass: linting, type checking, and tests across three Python versions. Your docs site will be live at https://your-username.github.io/my-package/ within a couple of minutes.

Pressing Enter at the first GitHub question generates the project locally without creating a repository or Git commit. If you opt in but gh is unavailable, unauthenticated, or another requested action fails, the command exits nonzero and keeps the generated directory so you can inspect or recover the partial setup.

For a private repository, Pages defaults to off. GitHub Pages can publish a public website even when its repository is private, and Pages for a private repository may require a paid plan. The interactive flow explains this and requires a separate opt-in before enabling Pages. If you leave Pages off, the documentation deployment workflow stays paused rather than failing on the first push; local docs preview and builds still work.

For non-interactive automation, make the opt-in explicit:

uvx cookiecutter-pypackage --no-input --github private \
    full_name="Your Name" \
    email="you@example.com" \
    github_username=yourhandle \
    author_website="" \
    project_name="My Package" \
    package_name=my-package

Use --github public when automation should also enable Pages and documentation deployment.

Step 2: Look around

cd my-package

Here's what you got:

Path What it does
src/my_package/ Your Python package code
src/my_package/cli.py Typer CLI (run with uv run my-package)
src/my_package/utils.py Placeholder for utility functions (rename or delete)
src/my_package/py.typed Marker that tells tools your package has type annotations
tests/ pytest test suite
docs/ Documentation source (builds with Zensical)
justfile Task runner commands (run just list to see them all)
.github/workflows/ CI, PyPI publishing, and docs deployment
pyproject.toml Package metadata, dependencies, and tool configuration

The project uses a src layout, meaning your package code lives under src/ rather than at the root. This prevents accidentally importing local code during testing.

Step 3: Install and verify

uv sync
just fix-and-check

just fix-and-check applies Ruff's automatic formatting and lint fixes, then runs the local quality gate: formatting and lint checks, ty, and tests on Python 3.14. Use just check when you only want read-only verification; use just testall for the local Python 3.12, 3.13, and 3.14 test matrix.

Try the CLI:

uv run my-package
uv run my-package --help

You can also run it as a module: uv run python -m my_package.

Run just list to see all available commands.

Step 4: Preview docs locally

just docs-serve

This starts a local server at http://localhost:8000 with live reload. Edit a doc, save, and watch it update. The API reference page auto-generates documentation from your docstrings.

Step 5: Write some code

Open src/my_package/utils.py and replace the placeholder:

def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

Add a test in tests/test_my_package.py:

from my_package.utils import add

def test_add():
    assert add(1, 2) == 3

Run just check to verify everything still passes without changing your files. Push your changes and watch CI confirm the full Python-version matrix on GitHub too.

Step 6: Set up PyPI publishing

If you opted into GitHub setup, the post-generation hook printed the URL and form values you need:

To publish to PyPI, add a pending publisher at:
https://pypi.org/manage/account/publishing/

Fill in these values:
  PyPI project name:  my-package
  Owner:              your-username
  Repository:         my-package
  Workflow:           publish.yml
  Environment:        pypi

Then release with:
  just release

Go to that URL, fill in those values, and you're done. This uses OIDC (Trusted Publishers) so there are no API tokens to manage. See the PyPI Release Checklist for more details.

Step 7: Release

As you work, record user-visible changes in CHANGELOG/unreleased.md. When you are ready to release, bump the version and commit it first:

uv version <version>        # or: uv version --bump minor
git add pyproject.toml uv.lock
git commit -m "Bump version to <version>"

just release

just release finalizes the unreleased notes, commits and pushes that notes commit, creates the v<version> tag and GitHub Release, and then GitHub Actions builds, signs with Sigstore, and publishes to PyPI automatically. Check the Actions tab to confirm.