Advanced·15 min·advanced · packaging · pip · wheel
Packaging with pyproject.toml
Packaging: from script to installable
The moment your Python file grows past 200 lines or someone else wants to run it, you need a package.
pyproject.toml — the single source of truth
Since PEP 621, one file replaces setup.py + setup.cfg + MANIFEST.in. Three sections you'll use:
- [build-system] — how to build.
hatchling(modern default) orsetuptools(legacy but fine). - [project] — name, version, description, dependencies, Python range.
- [project.scripts] — CLI entry points.
mytool = "my_tool.main:cli"makesmytoola command.
The src/ layout — do this
my-tool/
├── pyproject.toml
└── src/
└── my_tool/
├── __init__.py
└── main.py
Why src/? It stops import my_tool from accidentally picking up your working directory instead of the installed package during tests. Bit of a paper cut without it — worth the extra folder.
Editable install (development mode)
pip install -e ".[dev]"
- Symlinks the package instead of copying — edits take effect immediately.
.[dev]installs thedevextras from[project.optional-dependencies].
Building distributions
python -m build
Produces:
- wheel (
.whl) — pre-built binary. Fast install, most common. - sdist (
.tar.gz) — source distribution. Used when a wheel isn't available for a platform.
Version management
- SemVer:
MAJOR.MINOR.PATCH. 0.x.y= pre-1.0, expect breaking changes.- Bump on every release. Tools like
hatch versionorbump-my-versionautomate it.
The modern alternative: uv
uv (from Astral, same folks as ruff) is displacing pip + venv + poetry for many teams. uv init, uv add, uv run — 10-50× faster than pip. Same pyproject.toml. Worth trying on new projects.
Publishing to PyPI (once)
pip install twine
twine upload dist/*
- Register at pypi.org, verify email.
- Use an API token (not password) — set as
__token__username, token as password. - Test on test.pypi.org first if unsure.
Try it (locally)
- Copy the starter's layout to a folder.
pip install -e .and runmytool your-name.- Add a dependency, reinstall, use it in
main.py.