---
title: "Python: Packaging"
slug: "python-packaging"
category: "coding"
tags: ["coding", "python", "packaging", "pyproject", "uv", "wheels"]
status: "stable"
last_updated: 2026-05-29
summary: "pyproject.toml as the single source of truth, backend choices, src layout, entry points, wheel building, and PyPI publishing with uv."
related: ["[[coding/python]]", "[[coding/python-dependency-management]]", "[[file-organization/project-structure]]", "[[file-organization/config-files]]", "[[coding/general-principles]]"]
---

## Overview

A Python package is defined by its `pyproject.toml`. No `setup.py`, no `MANIFEST.in`, no `requirements.txt` at the package root. This page covers the standard layout, backend choices, and the publishing pipeline. For day-to-day dependency and environment management, see [[coding/python-dependency-management]].

## `pyproject.toml` is the single source of truth

All project metadata, build configuration, tool settings, and dependency lists live in `pyproject.toml`. The one-source-of-truth rule from [[file-organization/config-files]] applies here.

```toml
[project]
name = "mypackage"
version = "0.3.1"
description = "Short sentence about what this does."
requires-python = ">=3.13"
license = { text = "MIT" }
dependencies = [
  "httpx>=0.27",
  "pydantic>=2.7",
]

[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.5", "pyright>=1.1.370"]
test = ["pytest>=8", "pytest-asyncio>=0.23", "hypothesis>=6"]
```

Never duplicate dependency lists. CI installs `.[dev]` or `.[test]`; production images install bare `.`.

## Choose a build backend and stick to it

The build backend compiles your `pyproject.toml` into a wheel and sdist. Pick one at project start; do not mix backends in the same repo.

- **Hatch**: the current recommended default. First-class `pyproject.toml` support, built-in env management, and versioning from VCS tags.
- **setuptools**: the fallback for complex C extensions or legacy codebases.
- **uv**: manages builds and installs; delegates to the declared backend for the actual wheel build.
- **Poetry**: avoid on new projects unless the team already standardizes on it; its PEP 517 compliance has historically lagged.

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

## Use a `src` layout

Place all importable code under `src/<packagename>/`. This layout prevents accidental imports of the development tree during `pytest` and forces a real install for tests to pass.

```
mypackage/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       └── core.py
├── tests/
│   └── test_core.py
├── pyproject.toml
└── README.md
```

See [[file-organization/project-structure]] for the broader directory layout rules that apply across languages.

## Declare entry points for CLI tools

`[project.scripts]` maps command names to Python callables. The build backend installs these as executable scripts when the package is installed.

```toml
[project.scripts]
myapp = "mypackage.cli:main"
myapp-worker = "mypackage.worker:run"
```

The function referenced (`mypackage.cli:main`) must be importable and must accept no required arguments when called from the shell. Use `argparse` or `click` for argument parsing inside the function.

## Build wheels with `uv build`

`uv build` produces an `sdist` and a `wheel` in `dist/`. It respects the declared build backend.

```bash
uv build
# produces:
# dist/mypackage-0.3.1.tar.gz
# dist/mypackage-0.3.1-py3-none-any.whl
```

Prefer pure-Python wheels (`py3-none-any`) when possible. C extension packages need platform-specific wheels built in CI via `cibuildwheel` for each target OS and architecture.

## Publish to PyPI with `uv publish`

`uv publish` uploads `dist/*` to PyPI using credentials from the environment.

```bash
export UV_PUBLISH_TOKEN="pypi-..."
uv publish
```

Use `UV_PUBLISH_TOKEN` (a PyPI API token scoped to the project) rather than username and password. Generate the token at pypi.org under account settings and store it as a CI secret. Never commit tokens to the repo.

For the publishing workflow in CI:

```yaml
- name: Build
  run: uv build
- name: Publish
  run: uv publish
  env:
    UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
```

## Version from VCS tags, not source files

Set version dynamically from the Git tag so the `pyproject.toml` and the release tag can never diverge.

```toml
[tool.hatch.version]
source = "vcs"

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
```

Tag releases as `v0.3.1`. The build system strips the `v` prefix when writing the wheel version.

## Related

- [[coding/python]]
- [[coding/python-dependency-management]]
- [[file-organization/project-structure]]
- [[file-organization/config-files]]
- [[coding/general-principles]]
