The Modern Python Tooling Stack: uv, pytest, and Ruff
Elevate your engineering workflow with modern tools: lightning-fast dependency management with uv, test suites with pytest, and linting/type-checking with Ruff and mypy.
Chapter Objectives
- Structure reproducible Python projects using pyproject.toml standards
- Manage virtual environments, dependencies, and lockfiles with uv
- Architect scalable test suites with advanced pytest fixtures and mock isolation
- Configure Ruff for lightning-fast linting, import sorting, and code formatting
- Enforce strict static type analysis with mypy and pre-commit automation
The Modern Python Tooling Stack: uv, pytest, and Ruff
The Python tooling landscape has experienced a historic renaissance. Legacy configurations that required juggling setup.py, requirements.txt, Pipfile, flake8, isort, and black have been superseded by unified, high-performance tools written in Rust and standardized around modern PEP specifications. Today's production stack is dominated by uv for dependency management, Ruff for linting and formatting, and pytest with mypy for verification.
1. Project Standards with pyproject.toml
Defined across PEP 517, PEP 518, and PEP 621, pyproject.toml is the canonical, single-file configuration manifest for modern Python applications.
[project]
name = "locionic-service"
version = "1.0.0"
description = "High-performance data ingestion service"
readme = "README.md"
requires-python = ">=3.11"
authors = [{ name = "Loc Ionic", email = "engineering@locionic.com" }]
dependencies = [
"pydantic>=2.7.0",
"fastapi>=0.110.0",
"httpx>=0.27.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"mypy>=1.9.0",
"ruff>=0.4.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Consolidating configuration into pyproject.toml eliminates configuration sprawl and provides a uniform interface across CI/CD runners and developer workstations.
2. Next-Generation Package Management with uv
Developed by Astral and written in Rust, uv is an ultra-fast Python package installer and resolver designed as a drop-in replacement for pip, pip-tools, and virtualenv. Benchmarks show uv is 10x to 100x faster than pip when installing wheels and resolving complex dependency graphs.
# Initialize a new project with pyproject.toml
uv init my-service
# Create an isolated virtual environment
uv venv
# Add dependencies (updates pyproject.toml and resolves uv.lock)
uv add fastapi "httpx>=0.27.0"
# Add development-only dependencies
uv add --dev pytest ruff mypy
# Synchronize the exact dependencies pinned in uv.lock
uv sync
# Run scripts or tests inside the managed environment without manual activation
uv run pytest
uv run python main.py
Deterministic Builds with uv.lock
Always commit uv.lock to source control. While pyproject.toml specifies acceptable version ranges, uv.lock records the cryptographic hashes and exact versions of every transitive dependency, guaranteeing bit-for-bit reproducible environments in CI/CD and production containers.
3. Professional Test Architecture with pytest
pytest has replaced the legacy unittest.TestCase model. Its key strength is an extensible fixture dependency injection system that separates test setup, execution, and teardown.
Fixture Scopes and Resource Lifecycles
Fixtures can be scoped to control how frequently they are initialized:
function(default): Created and torn down for every individual test.class: Shared across methods on a test class.module: Initialized once per test file.session: Initialized once for the entire test suite run (ideal for Docker containers or test databases).
import pytest
from typing import Generator
import sqlite3
@pytest.fixture(scope="session")
def db_connection() -> Generator[sqlite3.Connection, None, None]:
# Setup: runs once before any tests execute
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
yield conn # Yield test resource
# Teardown: runs once after all tests in the session finish
conn.close()
@pytest.fixture(scope="function")
def clean_users_table(db_connection: sqlite3.Connection) -> None:
# Function-scoped fixture: guarantees clean state per test
db_connection.execute("DELETE FROM users")
db_connection.commit()
def test_user_creation(db_connection: sqlite3.Connection, clean_users_table: None) -> None:
db_connection.execute("INSERT INTO users (name) VALUES ('Alice')")
db_connection.commit()
cursor = db_connection.execute("SELECT count(*) FROM users")
assert cursor.fetchone()[0] == 1
4. Linting and Formatting with Ruff
Historically, a Python codebase required separate tools for formatting (Black), sorting imports (isort), checking code complexity (flake8), and upgrading legacy syntax (pyupgrade). Ruff replaces all of them in a single, blazing-fast Rust binary.
Configure Ruff inside pyproject.toml:
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
# Selected rule sets:
# E/W: pycodestyle errors and warnings
# F: Pyflakes (syntax, unused imports, undefined names)
# I: isort (deterministic import sorting)
# N: pep8-naming
# UP: pyupgrade (modernizes syntax for target Python version)
# B: flake8-bugbear (catches common architectural bugs)
select = ["E", "F", "I", "N", "UP", "B"]
ignore = ["E501"] # Formatting handles line length
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
# Automatically format all files in the project
uv run ruff format .
# Check code for lint violations and automatically fix safe issues
uv run ruff check --fix .
5. Strict Static Typing with mypy
Static type checking prevents production null reference errors before deployment. Configure mypy in strict mode inside pyproject.toml:
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
# Handle legacy packages without type stubs
[[tool.mypy.overrides]]
module = "legacy_library.*"
ignore_missing_imports = true
6. Pre-Commit Automation and Continuous Integration
Automate enforcement so defective code never enters the repository history:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
additional_dependencies: [pydantic>=2.7.0]
Interactive Knowledge Checks
Write a Scoped Pytest Fixture
mediumCreate a pytest fixture named 'temp_file' that creates a temporary text file, writes 'test-data', yields the file path to the test, and deletes the file during teardown.
Chapter Summary
- Adopt pyproject.toml: Consolidate project metadata, dependencies, build backends, and tool configurations into a single standard file.
- Standardize on uv: Accelerate environment provisioning and dependency resolution by 10x-100x; commit
uv.lockfor reproducible builds. - Master pytest fixtures: Structure tests around reusable fixtures with deliberate scopes (
functionvssession) to decouple test setup from assertion logic. - Use Ruff for quality: Replace multiple slow Python tools with Ruff's instant linter and formatter.
- Enforce strict typing: Run
mypy --strictin pre-commit hooks and CI pipelines to eradicate runtime null reference errors.