Frontend Labs
⋯

21. Projects and packaging: pyproject.toml, uv, lockfiles

21.1 The modern layout

uv init --package netkit     # src/ layout, installable package, entry point
# pyproject.toml: one file for metadata, dependencies, and tool config
[project]
name = "netkit"
version = "0.1.0"
description = "Small network tools"
readme = "README.md"
requires-python = ">=3.13"
dependencies = ["httpx>=0.28"]            # what users need: ranges, not pins

[project.scripts]
netkit = "netkit.cli:main"                # installs a `netkit` command

[dependency-groups]
dev = ["pytest>=8.4", "ruff>=0.13", "mypy>=1.18"]

[build-system]
requires = ["uv_build>=0.8"]              # or hatchling / setuptools
build-backend = "uv_build"

Why src/: with the package under src/, tests import the installed package, not the files that happen to sit in the current directory, so a missing file in your wheel fails your tests instead of your users.

21.2 Version ranges in pyproject.toml, exact versions in the lockfile

  • pyproject.toml says what versions work (httpx>=0.28). Libraries should only ever do this; pinning exact versions in a library makes it impossible to install next to other libraries.
  • uv.lock records the exact version and hash of every package, including transitive ones, for every platform. Commit it for applications. uv sync --locked in CI and Docker installs exactly that set, or fails.
uv add "pydantic>=2.12"       # updates pyproject.toml and uv.lock
uv lock --upgrade-package httpx
uv sync --locked              # reproducible install; fails if the lock is stale
uv tree                       # who pulled in what
uv build                      # sdist + wheel in dist/
uv publish                    # to PyPI (use trusted publishing from CI, Ch 50)

21.3 Wheels, sdists, and native code

A wheel (.whl) is a zip of ready-to-import files, tagged by Python version, OS, and CPU (numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl). A source distribution (sdist) must be built on install, which for packages with C or Rust code needs a compiler. If pip install starts compiling for minutes, it found no wheel for your platform; this happens with brand-new Python versions and unusual platforms (Alpine's musl libc is the classic, which is why Python Docker images are usually Debian-based, Ch 70).

21.4 War story

A data team's nightly job started failing on a Tuesday with AttributeError deep inside a plotting library. Nothing in their code had changed. Their requirements.txt listed pandas with no version, and a new major release had come out on Monday with a removed API. Their laptops still had the old version cached, so "it works on my machine" was literally true. Lesson: an application without a lockfile is a different program every time it is installed. Commit uv.lock, install with --locked, and upgrade dependencies on purpose, in a pull request that runs the tests.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36