Modern Python Development Environment with pyenv, Poetry, uv, and pyproject.toml

Table of Contents
pyenv
pyenv
Poetry
Poetry
uv
uv
pyproject.toml
pyproject.toml
Virtual Environment
Virtual Environment
Modern Python Development Environment Setup
Using just pip install leads to version conflicts, "works on my machine" problems, and no reproducible builds. Modern Python development requires environment isolation, version locking, and consistent tooling — ideally from a single, fast tool.
Standard Modern Toolkit
- pyenv (or uv): Manage and switch between Python versions (3.10, 3.12, 3.14...).
- uv: The fastest package manager — replaces pip, pip-tools, venv, and pyenv in one binary.
- pyproject.toml: Standard config for all Python projects (PEP 621).
- Ruff: Linter and formatter — replaces flake8, black, isort in one tool.
- ty: Pyright-based type checker from Astral, the creators of uv and Ruff.
Complete Setup Steps
Step 1: Install uv (The Modern Way)
uv is now the recommended single tool for managing Python versions, virtual environments, and packages. Install it once, globally:
# Linux / macOS
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
After installation, uv handles everything — you don't need pyenv, pip, or venv separately.
Step 2: Pin and Install the Right Python Version
Use uv to install and pin a Python version per project. This creates a .python-version file that is committed to git — every developer on the team uses the same version automatically:
# Install a specific Python version
uv python install 3.14.0
# Pin it for this project (creates .python-version)
uv python pin 3.14.0
# Verify
uv python list
If you still prefer pyenv for version management, uv respects .python-version files from pyenv seamlessly.
Step 3: Initialize the Project and Virtual Environment
uv init creates a pyproject.toml and uv.lock file. uv sync creates the virtual environment and installs all dependencies from the lockfile in one command:
# New project
uv init myapp
cd myapp
# Or sync an existing repo after cloning
uv sync
The virtualenv is created at .venv/ automatically. You never need to activate it manually when using uv run.
Step 4: Add Dependencies (Never pip install directly)
Always add dependencies through uv add — this updates both pyproject.toml and uv.lock atomically, ensuring the lockfile stays consistent:
# Add a runtime dependency
uv add fastapi uvicorn[standard]
# Add a dev-only dependency
uv add --dev pytest ruff ty
# Remove a package
uv remove httpx
Never run pip install directly in a uv-managed project — it bypasses the lock file.
Step 5: Run Code with uv run
uv run executes commands in the project's virtualenv without activating it. This is the standard way to run scripts, tests, and tools:
# Run your app
uv run python main.py
# Run tests
uv run pytest -v
# Run the linter
uv run ruff check .
# Run the type checker
uv run ty check
# Run a one-off script (auto-installs dependencies from inline script metadata)
uv run scripts/migrate.py
This pattern works identically in CI/CD without any activation step.
Step 6: Set Up Pre-commit Hooks with Ruff
Enforce formatting and linting on every commit with pre-commit. Ruff handles both formatting (replacing Black) and linting (replacing flake8 + isort) in a single fast pass:
uv add --dev pre-commit
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Install the hooks:
uv run pre-commit install
Now every git commit automatically runs Ruff format + lint with auto-fix.
Complete pyproject.toml Reference
This is what a well-structured pyproject.toml looks like for a real project — covers metadata, dependencies, dev dependencies, tool configuration for Ruff and ty, and test settings:
[project]
name = "myapp"
version = "1.0.0"
description = "Your project description"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn[standard]>=0.32",
"httpx>=0.28",
"pydantic>=2.10",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"ruff>=0.9",
"ty>=0.0.1a14",
"pre-commit>=4.0",
]
[project.scripts]
myapp = "myapp.main:app"
[tool.uv]
required-version = ">=0.6"
dev-dependencies = [
"pytest>=8.0",
"ruff>=0.9",
"ty>=0.0.1a14",
]
[tool.ruff]
target-version = "py312"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "UP", "B", "I"]
ignore = ["E501"]
[tool.ruff.format]
quote-style = "double"
[tool.ty]
# ty is the Astral type checker — fast, Pyright-based
python-version = "3.12"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Which Tool Should You Choose?
| Tool | Best For | Verdict |
|---|---|---|
| venv | Python built-in, simple, reliable | Great for simple scripts and learning |
| Pipenv | Was popular but now slower, abandoned by many | Avoid for new projects |
| Poetry | Mature, excellent dependency resolution, good DX | Solid choice, especially for libraries |
| uv | Rust-based, 10-100x faster, replaces pip+venv+pyenv | Recommended for 2026+ projects |
Warning for Beginners
Absolutely avoid sudo pip install or installing packages directly into system Python. Always, always use virtual environments — whether via uv venv, python -m venv, or Poetry.
Quick Comparison: Poetry vs uv
| Feature | Poetry | uv |
|---|---|---|
| Speed | Good | Blazing fast (Rust, 10-100x) |
| Lock file | poetry.lock | uv.lock (cross-platform, exact) |
| Virtual env | Auto-managed | Auto-managed (.venv) |
| Python version mgmt | Delegates to pyenv | Built-in (uv python install) |
| Script runner | poetry run | uv run (+ inline script deps) |
| Build/package | poetry build / publish | uv build / publish |
| Maturity | Mature, stable (5+ years) | Rapidly evolving, Astral-backed |
Lock File Discipline
The uv.lock file (or poetry.lock) is as important as your code. It pins every transitive dependency to exact versions, ensuring that uv sync on a CI server, a colleague's machine, or a production container installs the exact same packages.
Rules:
- Always commit the lockfile to git — it is not a generated artifact to
.gitignore - Never edit it manually — use
uv add,uv remove, oruv lock --upgrade-package <name> - Update dependencies intentionally:
uv lock --upgradeupgrades all packages to their latest compatible versions, then review the diff
# Upgrade a single package
uv lock --upgrade-package fastapi
# Upgrade all packages (then run tests!)
uv lock --upgrade
# Check what changed
git diff uv.lock
Continue Learning
Next: Learn Python Fundamentals if you're new to Python, or explore Python Tricks & Patterns for practical patterns that save time.
You Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Serverless Analytics Warehouse with BigQuery & Cloud Run: From GA4 Streams to Automated SEO Alerts
How to build an automated serverless analytics warehouse with BigQuery, Google Analytics 4, and Cloud Run: schema modeling, scheduled SQL transformations, zero-idle cost, and automated SEO query alerts.
Read more
BigQuery + Cloud Run: Building a Production Serverless Data Ingestion Pipeline
A production-grade guide to serverless data ingestion on Google Cloud: the BigQuery Storage Write API, partitioning and clustering strategy, a FastAPI async receiver on Cloud Run, complete Terraform, a real cost breakdown, and the failure modes that page you at 3am.
Read more
FastAPI vs Litestar: Production Benchmarks and High-Throughput Microservice Architecture
An objective, benchmark-driven comparison between FastAPI and Litestar. Explore ASGI performance, dependency injection architectures, serialization speed, and OpenAPI typing.
Read more