Dependencies, pyproject & a mini capstone
Understand pyproject.toml/uv and extend a small, tested multi-file project.
Why one file and one lockfile
A project that only runs on your laptop is a liability. The moment a teammate clones it, CI builds it, or you deploy it, "works on my machine" has to become "installs the same way everywhere." pyproject.toml plus a lockfile is how modern Python gets there. One file declares what the project is and what it needs, and the lockfile pins the exact versions that got resolved, so every install is byte-for-byte identical.
pyproject.toml: the single source of truth
pyproject.toml is the standard, tool-agnostic place to describe a Python project. It replaces the old setup.py plus requirements.txt sprawl.
[project]
name = "todo"
version = "0.1.0"
dependencies = ["httpx>=0.27"]
[project.scripts]
todo = "todo.cli:main"
The [project] table holds metadata: the package name, its version, and its dependencies. Note that dependencies are ranges (httpx>=0.27), a statement of intent, not exact pins. The [project.scripts] table wires a console command (todo) to a function (main in todo.cli), so installing the package gives you a runnable CLI.
Your capstone project is laid out this way: a todo/ package directory holds modules like tasks.py (sample tasks) and report.py (where summary lives), with pyproject.toml at the root naming the package.
uv: resolve, lock, run
uv is a fast package manager that replaces pip, virtualenv, and pip-tools with one tool. The day-to-day loop:
uv add httpx # add a dep: updates pyproject.toml AND uv.lock
uv sync # install exactly what uv.lock pins
uv run pytest # run a command inside the project's .venv
The mental model is two tiers. pyproject.toml declares intent as version ranges. uv.lock records the one exact resolution that satisfied those ranges. Commit both files; never commit .venv. That split is what makes builds reproducible: your teammate runs uv sync and gets your exact versions, not "whatever pip resolved today."
You are setting up the repo for this project. Sort each item by whether it goes into version control.
The reporting function
summary(tasks) is the kind of function a CLI or API endpoint calls to report state. Given task dicts with a "done" flag, it returns totals in one pass:
def summary(tasks):
done = sum(1 for task in tasks if task["done"])
return {"total": len(tasks), "done": done, "pending": len(tasks) - done}
print(summary([{"title": "a", "done": True}, {"title": "b", "done": False}]))
# {'total': 2, 'done': 1, 'pending': 1}
Deriving pending as total - done (rather than a second filtered loop) guarantees the invariant total == done + pending always holds, even if the two filters ever disagreed.
Pitfall: truthiness is not equality
if task["done"] tests truthiness, not "is this the boolean True." A flag of 0 or "" is falsy, but a flag of "false" (a non-empty string) is truthy and counts as done. If your data might carry non-bool flags, be explicit: if task["done"] is True. Silent truthiness bugs like this survive tests that only use clean True/False fixtures.
Interview nuance: in Python, bool is a subclass of int, so True == 1 and False == 0. That means sum(task["done"] for task in tasks) counts the done tasks without the literal 1, because the booleans add up directly. It is a clean one-pass, O(n) idiom that interviewers like, but flag its fragility: it assumes every flag is a real boolean (or at least an int). On the pitfall's own bad input, a string flag like "false" makes sum raise TypeError instead of counting, while a non-bool number like 2 silently overcounts. Correct behavior here rests on knowing your data's type, not just its shape.
def summary(tasks):
done = sum(1 for task in tasks if task["done"])
return {"total": len(tasks), "done": done, "pending": len(tasks) - done}
print(summary([{"title": "a", "done": True}, {"title": "b", "done": False}]))Apply
Your turn
The task this lesson builds to.
Warm-up (one file): implement summary(tasks). Given a list of task dicts (each with a
"done" flag), return {"total": n, "done": d, "pending": p}.
For [{"title": "a", "done": True}, {"title": "b", "done": False}] return
{"total": 2, "done": 1, "pending": 1}.
3 hints and 3 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Capstone: implement summary(tasks) in todo/report.py so it returns
{"total", "done", "pending"} counts for the project's tasks. Read pyproject.toml and
todo/tasks.py for context. Some tests are hidden.
3 hints and 2 automated checks are waiting in the workspace.