Skip to content

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