The Python Toolchain
Python is the lingua franca of data engineering — the glue between databases, APIs, and pipelines. But writing Python professionally is more than opening a notebook: it means reproducible projects, isolated environments, and pinned dependencies so your code runs the same on your laptop, a teammate's, and a server. This lab sets up the modern toolchain and gets you running a real script.
You'll install Python 3.12+ and uv (a fast, modern package and environment manager) — and you'll do both through uv, so there's almost nothing to fumble. You also want the data/sales.csv file you made in Chapter 02; we'll read it in the script section. Keep your terminal open beside this page and run each command as you read.
Why a real toolchain, not just notebooks
Notebooks are wonderful for exploring — you'll use them forever. But production data work is code that runs unattended, on a schedule, on a machine that isn't yours. That code has to be reproducible: anyone who checks it out of git should be able to run it and get the same result. Notebooks alone don't give you that. A real project does.
The core problem a toolchain solves is dependency hell. Imagine two projects on your laptop:
| Project | Needs |
|---|---|
sales-pipeline | pandas 1.5 (an older API a legacy report depends on) |
ml-features | pandas 2.2 (a new feature only in the latest version) |
If Python installs packages globally — one shared pile for the whole machine — these two projects fight. Installing pandas 2.2 for one breaks the other. You can only ever have one version, so something is always wrong.
The fix is to give each project its own private, isolated set of packages — a virtual environment. Each project carries its exact dependencies; they never collide:
This is the same theme from Chapter 00: everything is code, everything is reproducible. A project that declares its exact dependencies in a file — and locks their versions — is one you can rebuild from scratch anywhere. "It works on my machine" dies here.
Install uv
uv is a single, very fast tool that manages Python versions, virtual environments, and packages — replacing the old tangle of pip, venv, pyenv, and friends. We install it once, then let it install Python for us.
# macOS / Linux / WSL2 — one line
curl -LsSf https://astral.sh/uv/install.sh | sh
# restart your shell (or open a new terminal), then confirm
uv --version
# let uv install a modern Python for you
uv python install 3.12uv --version prints something like uv 0.5.x. uv python install 3.12 downloads and reports an installed CPython 3.12.x. If uv isn't found, close and reopen your terminal so it picks up the updated PATH — the installer adds uv to your shell config.
Create a project
A "project" is just a folder with a few files that declare what it is and what it needs. uv init scaffolds one for you:
cd ~
uv init mini-griddp-pipeline
cd mini-griddp-pipeline
ls -aYou get a small, conventional layout. Here's what each piece is:
The heart of it is pyproject.toml — a plain text config file (in TOML format) that describes your project. Open it and you'll see something like:
[project]
name = "mini-griddp-pipeline"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.12"
dependencies = [] # packages get listed here as you add themls -a shows pyproject.toml, .python-version, README.md, main.py, and a .git folder. This whole folder is your project — copy it, share it, commit it, and someone else can rebuild it exactly.
Virtual environments, explained
A virtual environment (or "venv") is a private folder — .venv/ — that holds a copy of Python and only this project's packages. When a venv is active, python means "the Python in .venv" and its installed packages, nothing global. That's the isolation that makes the dependency-hell problem disappear.
The mental model: each project is a sealed box. What you install in one box never leaks into another. Two projects can hold different pandas versions because each has its own box.
# uv creates the venv for you (it also does this automatically on first 'uv add')
uv venv
# the magic command: 'uv run' runs anything INSIDE this project's venv,
# creating/syncing it first if needed — no manual "activate" step to remember
uv run python --versionuv run python --version prints Python 3.12.x — the version pinned in .python-version, not whatever Python your system happens to have. The big habit to form: prefix project commands with uv run, and you're always inside the right box automatically.
Old tutorials tell you to source .venv/bin/activate before working and deactivate after. uv run makes that bookkeeping mostly unnecessary — it picks the project's environment for you, every time. Less to forget, fewer "why is this package missing?" mysteries.
Adding dependencies
To use a package, you uv add it. This installs it into the venv and records it in pyproject.toml so the project remembers it needs it. Let's add the core data tools:
uv add pandas duckdb polars
# look at what changed
cat pyproject.toml # dependencies now lists pandas, duckdb, polars
ls # a new uv.lock file appearedTwo files now describe your dependencies, and the difference between them matters:
| File | What it records |
|---|---|
pyproject.toml | The packages you asked for, loosely (e.g. "pandas, any recent version"). Human-edited. |
uv.lock | The exact versions actually installed — pandas and every package pandas itself depends on, down to the digit. Machine-generated. |
The lockfile is what makes installs reproducible across machines. When a teammate clones your repo and runs uv sync, uv reads uv.lock and installs the identical versions you had — not "whatever's newest today." Same inputs, same environment, every time. Commit pyproject.toml and uv.lock to git; never commit .venv/ (it's rebuildable and large).
A starter map of packages you'll meet constantly:
| Package | What it's for |
|---|---|
pandas | The classic dataframe library — tables in memory, the workhorse of data wrangling |
polars | A newer, much faster dataframe library; great on bigger data |
duckdb | A fast in-process OLAP database — your local "data warehouse," queries CSV/Parquet with SQL |
requests | Calling HTTP APIs to pull data from external services |
pytest | Writing and running tests so your pipeline code stays correct |
After uv add pandas duckdb polars, the dependencies = [...] line in pyproject.toml now lists all three, and a uv.lock file exists. uv installs them in seconds.
Write a real script
Time to do something useful. We'll read the data/sales.csv from Chapter 02, count the rows, and total the amount per region. First make sure the data is here:
# bring the CSV into this project (adjust the path if yours lives elsewhere)
mkdir -p data
cp ~/cli-practice/data/sales.csv data/sales.csv # from Chapter 02
# if you skipped Chapter 02, recreate it:
cat > data/sales.csv <<'EOF'
date,region,amount
2026-06-01,us,120
2026-06-01,eu,80
2026-06-02,us,200
2026-06-02,eu,95
EOFNow create summarize.py in the project folder with your editor (VS Code, nano, whatever you like):
import pandas as pd
# read the CSV into a dataframe (a table in memory)
df = pd.read_csv("data/sales.csv")
print(f"rows: {len(df)}")
# group by region, sum the amount column, and print each region's total
totals = df.groupby("region")["amount"].sum()
for region, total in totals.items():
print(f"{region}: {total}")Run it inside the project's environment:
uv run python summarize.pyOutput like:
rows: 4
eu: 175
us: 320Four data rows; EU totals 80 + 95 = 175 and US totals 120 + 200 = 320. Notice you didn't activate anything — uv run used the project venv (with pandas installed) automatically. This is the same group-and-sum you did with the shell's cut in Chapter 02, now with a real dataframe.
A taste of good practice
You don't need to master these yet — Courses 3 and 4 cover them fully — but a small taste now builds the right habits. Two cheap upgrades make code easier to read and trust: a docstring (what does this do?) and type hints (what goes in, what comes out?). Refactor the summary into a function:
import pandas as pd
def region_totals(path: str) -> pd.Series:
"""Read a sales CSV and return total amount per region."""
df = pd.read_csv(path)
return df.groupby("region")["amount"].sum()
if __name__ == "__main__":
totals = region_totals("data/sales.csv")
print(totals)The -> pd.Series and path: str are type hints: they don't change how the code runs, but they document intent and let tools catch mistakes. Two tools you'll meet soon:
| Tool | What it does | Try later |
|---|---|---|
ruff | A fast linter/formatter — flags bugs and style issues, auto-fixes formatting | uv add --dev ruff then uv run ruff check . |
pytest | Runs your tests so changes don't silently break things | uv add --dev pytest then uv run pytest |
The --dev flag marks these as development dependencies — needed to work on the project, but not to run it in production. Keep it light for now; just know these exist and what they're for.
✓ Check yourself
- Can you explain, in one sentence, what problem a virtual environment solves?
- Do you know the difference between
pyproject.tomlanduv.lock— and which one makes installs reproducible? - Could you start a fresh project, add a package, and run a script with
uv runwithout looking it up? - Which files do you commit to git, and which do you never commit?
Exercise — Add a dependency and find the top sale
Write a short script top_sale.py that reads data/sales.csv and prints the single row with the highest amount. Use any dataframe library — try adding polars (if you haven't already) and using it, so you've added a dependency as part of the exercise.
uv add polars # already added earlier? uv just confirms it's presentimport polars as pl
df = pl.read_csv("data/sales.csv")
# sort by amount descending, take the first row
top = df.sort("amount", descending=True).head(1)
print(top)uv run python top_sale.pyYou should see the 2026-06-02, us, 200 row — the biggest sale. (Prefer pandas? df.loc[df["amount"].idxmax()] does the same thing.) If you reached for uv add and uv run without thinking, the toolchain habits have landed.
Next
You can build reproducible Python projects. Now let's give your pipelines real databases to talk to — spun up identically on any machine with containers. → Docker & Local Databases