Python Packaging Reference: pyproject.toml, uv, pip, Requirements & PyPI Publishing
Python packaging has modernized significantly. pyproject.toml is now the standard config file. uv is the fastest tool for creating environments and installing packages. Understanding virtual environments, dependency files, and package publishing will stop you fighting dependency hell.
1. pyproject.toml — The Modern Standard
Project metadata, dependencies, and build system configuration
# pyproject.toml — replaces setup.py, setup.cfg, requirements.txt for libraries
[build-system]
requires = ["hatchling"] # or flit-core, setuptools, pdm-backend
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "1.0.0" # or use dynamic = ["version"] from __version__
description = "A brief description"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [{name = "Alice", email = "alice@example.com"}]
keywords = ["packaging", "example"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
]
# Runtime dependencies:
dependencies = [
"httpx>=0.26.0",
"pydantic>=2.0,<3.0", # version constraints
"typing-extensions>=4.0; python_version < '3.11'", # conditional
]
# Optional dependencies (extras):
[project.optional-dependencies]
dev = ["pytest>=7.0", "mypy", "ruff"]
docs = ["mkdocs", "mkdocs-material"]
# Install with: pip install my-package[dev]
# CLI entry points:
[project.scripts]
my-cli = "my_package.cli:main" # creates 'my-cli' command after install
# URLs:
[project.urls]
Homepage = "https://github.com/example/my-package"
Issues = "https://github.com/example/my-package/issues"
2. Virtual Environments & uv
venv, uv (fast modern tool), and pip basics
# uv — much faster than pip + venv (written in Rust, replaces pip/venv/pip-tools): # Install: curl -LsSf https://astral.sh/uv/install.sh | sh # macOS/Linux # Or: brew install uv # Create + activate venv with uv: uv venv # creates .venv in current dir uv venv --python 3.12 # specific Python version source .venv/bin/activate # activate (Unix) .venv\Scriptsctivate # activate (Windows) # Install packages (much faster than pip): uv pip install requests uv pip install -r requirements.txt uv pip install ".[dev]" # install with dev extras # Generate locked requirements.txt from pyproject.toml: uv pip compile pyproject.toml -o requirements.txt uv pip compile pyproject.toml --extra dev -o requirements-dev.txt uv pip sync requirements.txt # install exactly what's in requirements.txt # Traditional venv (no uv): python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt # Environment management: deactivate # exit venv which python # verify you're in the venv pip list # list installed packages pip freeze > requirements.txt # export current env pip install --upgrade pip # update pip itself # Never install packages globally! Always activate a venv first.
3. Dependency Files — requirements.txt vs pyproject.toml
When to use which format, pinning, and pip-tools workflow
# Two different use cases: # - LIBRARY (distributed via PyPI): use pyproject.toml with loose version ranges # - APPLICATION (deployed, not distributed): use locked requirements.txt with == # For applications: requirements.txt with pinned versions (reproducible deploys): # requirements.txt (pinned — production): httpx==0.27.0 pydantic==2.6.4 uvicorn==0.29.0 # requirements-dev.txt (dev extras): -r requirements.txt pytest==8.1.1 mypy==1.9.0 ruff==0.3.5 # pip-tools workflow (recommended for apps — compile abstract→concrete): # requirements.in (abstract — what you actually need): httpx>=0.26 pydantic>=2,<3 uvicorn # Compile to pinned file: pip-compile requirements.in -o requirements.txt # resolves + pins all transitive deps pip-sync requirements.txt # sync env to exactly this state # With uv (faster): uv pip compile requirements.in -o requirements.txt uv pip sync requirements.txt # Update a single package: pip-compile --upgrade-package httpx requirements.in -o requirements.txt uv pip compile --upgrade-package httpx requirements.in -o requirements.txt
4. Publish to PyPI
Build distributions and upload to PyPI or TestPyPI
# Build source distribution + wheel:
pip install build
python -m build
# Creates: dist/my_package-1.0.0.tar.gz (sdist) + dist/my_package-1.0.0-py3-none-any.whl
# Or with uv:
uv build
# Upload to TestPyPI first (safe practice):
pip install twine
twine upload --repository testpypi dist/*
# Install from TestPyPI to verify:
pip install --index-url https://test.pypi.org/simple/ my-package
# Upload to PyPI:
twine upload dist/*
# Prompts for username: __token__ and password: pypi-xxx... (API token from pypi.org)
# Better: configure ~/.pypirc:
[distutils]
index-servers = pypi testpypi
[pypi]
username = __token__
password = pypi-your-token-here
# GitHub Actions publishing (OIDC — no token stored as secret):
# .github/workflows/publish.yml:
# - uses: pypa/gh-action-pypi-publish@release/v1
# with:
# password: ${{ secrets.PYPI_API_TOKEN }}
# Or with trusted publisher (OIDC, no secret needed):
# - uses: pypa/gh-action-pypi-publish@release/v1
# permissions: {id-token: write}
# Version bump + tag (triggers CI publish):
# Edit pyproject.toml version manually or use:
# bump-my-version bump minor # 1.0.0 → 1.1.0
# git tag v1.1.0 && git push origin v1.1.0
5. Tool Configuration in pyproject.toml
Configure pytest, mypy, ruff, and coverage in one file
# All tools go in pyproject.toml — no more setup.cfg, .mypy.ini, .ruff.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = ["-v", "--tb=short", "--strict-markers"]
markers = [
"slow: mark test as slow",
"integration: requires external services"
]
[tool.mypy]
python_version = "3.12"
strict = true # enable all strict checks
ignore_missing_imports = false # fail on untyped third-party packages
# Per-package overrides:
[[tool.mypy.overrides]]
module = "some_untyped_library.*"
ignore_missing_imports = true
[tool.ruff]
line-length = 88
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B"] # pycodestyle, pyflakes, isort, naming, pyupgrade, bugbear
ignore = ["E501"] # ignore line length (handled by formatter)
fixable = ["ALL"] # allow auto-fix
[tool.ruff.format]
quote-style = "double"
[tool.coverage.run]
source = ["src/my_package"]
omit = ["tests/*", "*/migrations/*"]
[tool.coverage.report]
fail_under = 80 # fail CI if coverage < 80%
show_missing = true
Track Python, pip, uv, and packaging tool releases.
ReleaseRun monitors Python, Node.js, and 13+ technologies.
Related: Python Reference | GitHub Actions Reference | pytest Reference | Python EOL Tracker
🔍 Free tool: PyPI Package Health Checker — check any Python package for EOL status, known CVEs, and whether it's actively maintained — before pip install.
Founded
2023 in London, UK
Contact
hello@releaserun.com