Skip to content

Setting Up: Python 3.13, uv & the Modern Toolchain

What this lesson gives you

The 2026 way to install Python and manage projects — fast, reproducible, and the same setup a professional team would expect you to know.

Estimated time: 20 min read · Part: Why Python, and How It Runs

Learning objectives

  • Install and manage Python versions using uv without touching the system Python.
  • Understand what a virtual environment is and why every project needs its own.
  • Create a new project with uv init, add dependencies, and run code reproducibly.
  • Read a pyproject.toml and uv.lock file and explain what each section does.
  • Recognise the classic toolchain (pip/venv/pyenv/poetry) and explain when you'll still encounter it.

Getting the environment right is not busywork — a tangled Python setup is the single most common reason a beginner's code "works on my machine" and nowhere else. In 2026 the tooling has consolidated dramatically around one fast tool, uv, so the modern path is simpler than the historical mess of pip, virtualenv, pyenv, and poetry all at once. Even so, you will encounter those older tools in real codebases, so knowing both the new way and its predecessors is necessary.

Versions, as of June 2026

Python 3.13 is the current stable release (it shipped an experimental free-threaded build and a new interactive REPL); Python 3.14 is in late beta and due October 2026. Target 3.13 for new work. Python 2 is long dead — if you ever see print "x" without parentheses, you're looking at a fossil that needs updating.

uv: the tool that replaced the toolbox

uv (from Astral, the makers of the Ruff linter) is a single Rust-built binary that installs Python itself, creates virtual environments, resolves and installs dependencies, and locks them — doing in seconds what older tools did in minutes. It is 10–100x faster than pip for dependency resolution, and its lockfile (uv.lock) is deterministic: the same lockfile produces byte-identical environments on every machine, critical for reproducible demos and CI pipelines.

The design principle behind uv is "one tool, one interface." Before uv, a typical project setup required: pyenv to install the right Python version, venv to create the environment, pip to install packages, and optionally poetry or pip-tools to manage a lockfile. Each tool had its own configuration, its own bugs, and its own upgrade path. uv replaces all of them with a single coherent interface.

terminal
# ── 1. Install uv (macOS / Linux) ──────────────────────────────────
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell):
# powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# ── 2. Install a specific Python version ───────────────────────────
uv python install 3.13

# ── 3. Start a new project ─────────────────────────────────────────
uv init my-analysis          # creates directory, pyproject.toml, .python-version
cd my-analysis

# ── 4. Add dependencies ────────────────────────────────────────────
uv add pandas httpx           # resolves + installs + writes to pyproject.toml + lockfile

# ── 5. Run your script ─────────────────────────────────────────────
uv run main.py                # uses the project's managed venv automatically

Intuition: why virtual environments exist

Project A needs pandas 1.5; project B needs pandas 2.2. Install both globally and they collide — the second install overwrites the first, and whichever project was compatible with version 1.5 breaks. A virtual environment is a private, per-project folder of installed packages — an isolated sandbox so each project carries its own dependency tree and never poisons another. Think of it like Docker but lighter: separate layers of packages, not separate OS images. uv creates and manages this for you automatically; you rarely activate it by hand anymore.

Anatomy of a modern project

Running uv init my-analysis generates a small but complete project structure. Understanding each file is essential — you'll read and write these on every real project.

  my-analysis/
  ├── pyproject.toml      ← project metadata + dependencies (human-editable)
  ├── uv.lock             ← exact resolved versions of every package (committed to git)
  ├── .python-version     ← the Python version this project uses (e.g. "3.13")
  ├── .venv/              ← the virtual environment (git-ignored, rebuilt from lock)
  │   ├── bin/python      ← the Python interpreter for this project
  │   └── lib/...         ← installed packages
  └── main.py             ← starter script
pyproject.toml
[project]
name = "my-analysis"
version = "0.1.0"
requires-python = ">=3.13"    # minimum Python version
dependencies = [
    "pandas>=2.2",            # added by `uv add pandas`
    "httpx>=0.27",
]

[tool.uv]
# uv-specific settings (dev dependencies, source overrides, etc.)

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

The key distinction is between pyproject.toml (which records your constraints — "pandas 2.x or higher") and uv.lock (which records the exact resolved version — "pandas 2.2.3"). You commit both to git. A new team member runs uv sync and gets an identical environment in seconds, regardless of what newer package versions have shipped since you wrote the code.

common uv commands
# Add a production dependency:
uv add requests

# Add a development-only dependency (not shipped in production):
uv add --dev pytest ruff mypy

# Remove a dependency:
uv remove requests

# Sync environment to match lockfile (e.g. after a git pull):
uv sync

# Run a one-off script with temporary dependencies (no project needed):
uv run --with pandas python -c "import pandas; print(pandas.__version__)"

# Show the resolved dependency tree:
uv tree

# Update a specific package to the latest compatible version:
uv lock --upgrade-package pandas

The classic tools you must still recognise

Any codebase over two years old almost certainly uses some combination of the older toolchain. You'll encounter requirements.txt files, Pipfiles, and setup.py files in existing repositories. Recognising these and knowing how to work with them — and when to propose migrating to uv — is a real professional skill.

Tool What it does Status in 2026 uv equivalent
pip Installs packages from PyPI. Still everywhere; uv is a faster drop-in. uv add / uv pip install
venv Creates an isolated environment. Built into Python; uv does this automatically. uv venv
pyenv Manages multiple Python versions. Still used; uv can install/pin versions too. uv python install
poetry Dependency + packaging manager with lockfiles. Common in existing codebases; uv is the rising default. uv covers all use cases
pipenv pip + virtualenv combined. Declining; mostly found in older repos. uv
conda Env + package manager, scientific stack. Still used for heavy GPU/scientific stacks. No direct equivalent; use alongside uv
pip-tools Compiles requirements.in to requirements.txt with hashes. Used in production deployments; uv.lock is the modern equivalent. uv lock

The old-school equivalent (good to know)

If a team is still on pip + venv: python -m venv .venv creates the env, source .venv/bin/activate turns it on (macOS/Linux), .venv\Scripts\activate on Windows, pip install pandas installs into it, and pip freeze > requirements.txt records the exact versions. You'll meet this in older repos constantly — recognise it, then suggest uv for new work.

Editor and IDE setup

The dominant editors for Python in 2026 are VS Code (with the Pylance extension) and PyCharm (JetBrains' dedicated Python IDE). Both support uv-managed environments automatically. The key extensions you want in VS Code: Python (Microsoft), Pylance (type checking, autocomplete), Ruff (linting + formatting), and optionally a Jupyter extension if you work with notebooks.

ruff (linter + formatter)
# Ruff is the 2026 standard linter AND formatter (replaces flake8 + black + isort)
uv add --dev ruff

# Check for issues:
ruff check .

# Auto-fix what can be fixed:
ruff check --fix .

# Format code (replaces black):
ruff format .

# The ruff config goes in pyproject.toml:
# [tool.ruff]
# line-length = 88
# select = ["E", "F", "I"]  # pycodestyle, pyflakes, isort

Consulting lens

When you hand a script to a customer's engineering team, the first thing they'll do is try to run it. If it breaks because of a missing package or a wrong Python version, you've lost credibility before they've seen the logic. A well-structured uv project with a committed lockfile means anyone on their team can run uv sync && uv run main.py and get the same result you showed in the demo. That reproducibility is itself a trust signal — it says you ship things that work outside your laptop.

uv command What it does When to use
uv init <name> Create a new project directory with boilerplate. Starting any new project.
uv add <pkg> Add a dependency; updates pyproject.toml + lockfile. Whenever you need a new library.
uv sync Install all deps from lockfile into .venv. After cloning a repo or pulling changes.
uv run <script> Run a script using the project's managed env. Running any project script.
uv python install Download and install a Python version. Setting up a new machine or version.
uv lock Regenerate the lockfile from pyproject.toml constraints. After manually editing pyproject.toml.
uv tree Show the full resolved dependency tree. Debugging dependency conflicts.
uv pip compile Compile a requirements.in to a pinned requirements.txt. Compatibility with legacy pip-tools workflows.
Knowledge check

InterviewA teammate installs every package globally with pip install and two projects suddenly break each other. What is the root cause and the fix you'd articulate?

  • Python is broken; reinstall the operating system.
  • Dependency conflict from a shared global environment — give each project its own virtual environment (via uv or venv) so their package versions are isolated.
  • Use sudo on every install to fix permissions.
  • Delete one of the two projects.
Answer

Dependency conflict from a shared global environment — give each project its own virtual environment (via uv or venv) so their package versions are isolated.

Global installs force every project to share one set of versions, so incompatible requirements collide. Per-project virtual environments isolate dependencies — the foundational fix. uv automates env creation and locking; python -m venv is the manual equivalent.

Knowledge check

Concept checkWhat is the difference between pyproject.toml and uv.lock, and which one should you commit to git?

  • They are the same file with different names; commit either one.
  • Commit pyproject.toml but add uv.lock to .gitignore since it's machine-specific.
  • pyproject.toml records version constraints (human-editable); uv.lock records the exact resolved versions (deterministic). Commit both so collaborators reproduce the identical environment.
  • Commit uv.lock only; pyproject.toml is auto-generated.
Answer

pyproject.toml records version constraints (human-editable); uv.lock records the exact resolved versions (deterministic). Commit both so collaborators reproduce the identical environment.

The pyproject.toml is the specification ("I need pandas 2.x"), which humans edit. The lockfile is the solution ("pandas 2.2.3, numpy 1.26.4, ..."), which uv computes and should be committed so every collaborator and CI runner gets byte-identical environments.

Knowledge check

AppliedYou're handed a repository with only a requirements.txt file. What are the first three commands you'd run to work with it using uv?

  • Delete requirements.txt and start fresh with uv init.
  • uv venv to create an environment, uv pip install -r requirements.txt to install deps from the file, then uv run python script.py to execute with the managed env.
  • You cannot use uv with a requirements.txt file.
  • Run pip install -r requirements.txt globally — that's the only option.
Answer

uv venv to create an environment, uv pip install -r requirements.txt to install deps from the file, then uv run python script.py to execute with the managed env.

uv has a compatibility layer: uv pip install -r requirements.txt works exactly like pip but faster. You'd create a venv first with uv venv , then install, then run. This lets you use uv's speed and isolation benefits without requiring the repo to be migrated to pyproject.toml immediately.

Exercise

Bootstrap a new project called deal-probe using uv. Add httpx and rich as dependencies. Write a main.py that uses rich's Table to display a nicely formatted table of three hardcoded API endpoints alongside their status codes (use httpx to call them). Confirm you can run it with uv run main.py. Inspect the resulting uv.lock file — find the entry for httpx and note its exact pinned version and hash.

Show solution
# Terminal:
# uv init deal-probe
# cd deal-probe
# uv add httpx rich
# (then write main.py:)

import httpx
from rich.console import Console
from rich.table import Table

ENDPOINTS = [
    "https://httpbin.org/get",
    "https://httpbin.org/status/200",
    "https://httpbin.org/status/500",
]

table = Table(title="Endpoint Probe")
table.add_column("URL", style="cyan")
table.add_column("Status", justify="right")
table.add_column("Latency (ms)", justify="right")

for url in ENDPOINTS:
    try:
        r = httpx.get(url, timeout=5)
        ms = f"{r.elapsed.total_seconds() * 1000:.0f}"
        color = "green" if r.is_success else "red"
        table.add_row(url, f"[{color}]{r.status_code}[/{color}]", ms)
    except Exception as e:
        table.add_row(url, "[red]ERROR[/red]", str(e))

Console().print(table)

# Run: uv run main.py
# Inspect lockfile: grep -A5 'name = "httpx"' uv.lock

Key takeaways

  • Target Python 3.13 (3.14 lands Oct 2026); Python 2 is dead.
  • uv is the 2026 default: installs Python, manages venvs, resolves/locks dependencies — fast and reproducible.
  • A virtual environment isolates each project's packages so versions never collide.
  • pyproject.toml = constraints (commit it); uv.lock = exact resolved versions (commit it too).
  • uv run automatically uses the project's managed environment — no manual activation needed.
  • Still recognise pip, venv, pyenv, poetry, conda — you'll meet them in real codebases.