Modern Python Dependency Management: A Complete Guide to uv and pyproject.toml

Say goodbye to requirements.txt headaches and hello to lightning-fast, modern Python dependency management


If you're still using requirements.txt and pip install -r requirements.txt to manage your Python dependencies, you're living in the past. The Python ecosystem has evolved dramatically, and there's a much better way to handle dependencies that's faster, more reliable, and easier to use.

In this comprehensive guide, I'll show you how to modernize your Python dependency management using uv and pyproject.toml – the tools that are rapidly becoming the new standard in 2025.

Why Change? The Problems with Traditional Approaches

Before we dive into the solutions, let's understand why the old ways are holding you back:

Problems with requirements.txt

  • No lock files: You can't guarantee reproducible builds
  • Slow installation: pip can be painfully slow with large dependency trees
  • No separation: Development and production dependencies mixed together
  • Manual management: Adding, removing, and updating dependencies requires manual file editing
  • Dependency hell: No automatic conflict resolution

Problems with the old setup.py

  • Complex syntax: Writing Python code just to declare metadata
  • Outdated standard: Replaced by modern standards like PEP 621
  • Limited functionality: Difficult to handle complex dependency scenarios

Enter the Modern Stack: uv + pyproject.toml

The modern Python ecosystem has converged on two key tools:

  1. pyproject.toml - The standardized configuration file (PEP 518/621)
  2. uv - The lightning-fast package manager written in Rust

Together, they provide:

  • 10-100x faster dependency installation
  • 🔒 Automatic lock files for reproducible builds
  • 📦 Clean separation of development and production dependencies
  • 🛠️ Built-in virtual environment management
  • 🎯 Simple, intuitive commands

Getting Started: Installation and Setup

Install uv

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or via pip
pip install uv

# Or via Homebrew (macOS)
brew install uv

Create Your First Modern Python Project

# Create a new project
uv init my-awesome-project
cd my-awesome-project

# Your project structure looks like this:
# my-awesome-project/
# ├── pyproject.toml
# ├── README.md
# └── hello.py

Understanding pyproject.toml

The pyproject.toml file is your project's command center. Here's what a modern configuration looks like:

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

[project]
name = "my-awesome-project"
version = "0.1.0"
description = "A modern Python project"
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
readme = "README.md"
requires-python = ">=3.9"

# Your main dependencies
dependencies = [
    "requests>=2.31.0",
    "click>=8.0.0",
    "pydantic>=2.0.0",
]

# Optional dependencies for different use cases
[project.optional-dependencies]
# Development tools
dev = [
    "pytest>=7.0.0",
    "ruff>=0.1.0",
    "mypy>=1.0.0",
]

# Web framework stack
web = [
    "fastapi>=0.100.0",
    "uvicorn>=0.23.0",
]

# Database tools
db = [
    "sqlalchemy>=2.0.0",
    "psycopg2-binary>=2.9.0",
]

Managing Dependencies with uv

Adding Dependencies

# Add a production dependency
uv add requests

# Add a development dependency
uv add --dev pytest

# Add to a specific optional group
uv add --optional web fastapi

Installing Dependencies

# Install all dependencies (including dev)
uv sync

# Install only production dependencies
uv sync --no-dev

# Install with specific extras
uv sync --extra web --extra db

Removing Dependencies

# Remove a dependency
uv remove requests

# Remove a dev dependency
uv remove --dev pytest

The Magic of Lock Files

When you run uv sync, uv automatically creates a uv.lock file. This file:

  • Pins exact versions of all dependencies and sub-dependencies
  • Ensures reproducible builds across different environments
  • Speeds up installation by avoiding dependency resolution
  • Should be committed to version control
# Update lock file with latest versions
uv lock --upgrade

# Update specific package
uv lock --upgrade-package requests

Running Your Code

Instead of activating virtual environments manually, use uv run:

# Run your Python script
uv run python main.py

# Run tests
uv run pytest

# Run any command in your project environment
uv run python -c "import requests; print(requests.__version__)"

Real-World Example: Web API Project

Let's build a complete example to see everything in action:

1. Set Up the Project

uv init web-api-project
cd web-api-project

2. Add Dependencies

# Web framework
uv add fastapi uvicorn

# Database
uv add sqlalchemy psycopg2-binary

# Utilities
uv add pydantic python-dotenv

# Development tools
uv add --dev pytest pytest-asyncio httpx ruff mypy

3. Create Your Application

# main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="My Awesome API")

class Item(BaseModel):
    name: str
    price: float

@app.get("/")
async def root():
    return {"message": "Hello, Modern Python!"}

@app.post("/items/")
async def create_item(item: Item):
    return {"item": item, "status": "created"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

4. Run Your Application

# Start the web server
uv run python main.py

# Or run tests
uv run pytest

5. Your Final pyproject.toml

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

[project]
name = "web-api-project"
version = "0.1.0"
description = "A modern web API built with FastAPI"
authors = [
    {name = "Your Name", email = "your.email@example.com"}
]
readme = "README.md"
requires-python = ">=3.9"

dependencies = [
    "fastapi>=0.100.0",
    "uvicorn>=0.23.0",
    "sqlalchemy>=2.0.0",
    "psycopg2-binary>=2.9.0",
    "pydantic>=2.0.0",
    "python-dotenv>=1.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "pytest-asyncio>=0.21.0",
    "httpx>=0.24.0",
    "ruff>=0.1.0",
    "mypy>=1.0.0",
]

[project.scripts]
start = "web_api_project.main:main"

Advanced Tips and Best Practices

1. Version Constraints

[project]
dependencies = [
    # ✅ Good: Allow patch updates
    "requests>=2.31.0,<2.32.0",

    # ✅ Good: Compatible release (tilde operator)
    "fastapi~=0.100.0",  # >=0.100.0, <0.101.0

    # ✅ Good: Minimum version for libraries
    "pydantic>=2.0.0",

    # ❌ Avoid: Exact pins (except for specific reasons)
    "requests==2.31.0",
]

2. Development Workflow

# Start working on a new feature
git checkout -b feature/new-feature

# Add dependencies as needed
uv add boto3

# Develop and test
uv run python main.py
uv run pytest

# Commit changes (including lock file!)
git add pyproject.toml uv.lock
git commit -m "Add AWS integration"

3. Environment-Specific Dependencies

[project.optional-dependencies]
# Production deployment
prod = [
    "gunicorn>=21.0.0",
    "redis>=4.5.0",
]

# CI/CD pipeline
ci = [
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "codecov>=2.1.0",
]

# Local development
dev = [
    "pytest>=7.0.0",
    "ruff>=0.1.0",
    "mypy>=1.0.0",
    "jupyter>=1.0.0",
]

Migrating from Legacy Tools

From requirements.txt

# 1. Create pyproject.toml and move dependencies
# 2. Separate dev dependencies into [project.optional-dependencies]
# 3. Run uv sync to create lock file
# 4. Test everything works
# 5. Delete old requirements.txt files

From Poetry

# 1. Convert [tool.poetry] section to [project] in pyproject.toml
# 2. Move dependencies from [tool.poetry.dependencies]
# 3. Delete poetry.lock
# 4. Run uv sync
# 5. Test and adjust as needed

Performance Comparison

Here's what you can expect when switching to uv:

OperationpipPoetryuv
Install 50 packages45s30s3s
Resolve dependencies20s15s1s
Create lock file25s20s2s
Cold start60s40s4s
Results may vary based on your specific dependencies and system

Troubleshooting Common Issues

Issue: "can't open file" Error

# Make sure your file exists
ls -la

# Create a test file
echo 'print("Hello, uv!")' > test.py

# Run it
uv run python test.py

Issue: Virtual Environment Conflicts

# If you see VIRTUAL_ENV warnings
unset VIRTUAL_ENV

# Or use the active environment
uv run --active python script.py

Issue: Dependency Conflicts

# Update lock file
uv lock --upgrade

# Or reinstall everything
uv sync --reinstall

The Future is Now

The Python ecosystem is rapidly adopting these modern tools:

  • uv is seeing explosive growth and adoption
  • pyproject.toml is becoming the standard for all Python projects
  • Lock files are now considered essential for serious projects
  • Speed is no longer a limitation with Rust-based tools

Getting Started Today

Ready to modernize your Python dependency management? Here's your action plan:

  1. Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh
  2. Try it on a new project: uv init test-project
  3. Migrate one existing project to see the difference
  4. Share the knowledge with your team

The transition to modern Python dependency management isn't just about keeping up with trends – it's about dramatically improving your development experience. With uv and pyproject.toml, you get faster builds, more reliable deployments, and a cleaner development workflow.

Stop fighting with slow dependency resolution and manual requirements.txt management. The future of Python development is here, and it's incredibly fast.