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
uvwithout 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.tomlanduv.lockfile 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.
# ── 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
[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.
# 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 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
sudoon 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.tomlbut adduv.lockto.gitignoresince it's machine-specific. pyproject.tomlrecords version constraints (human-editable);uv.lockrecords the exact resolved versions (deterministic). Commit both so collaborators reproduce the identical environment.- Commit
uv.lockonly;pyproject.tomlis 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 venvto create an environment,uv pip install -r requirements.txtto install deps from the file, thenuv run python script.pyto execute with the managed env.- You cannot use uv with a requirements.txt file.
- Run
pip install -r requirements.txtglobally — 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 runautomatically uses the project's managed environment — no manual activation needed.- Still recognise pip, venv, pyenv, poetry, conda — you'll meet them in real codebases.