Part 7 of 8 · 4 min read
Exercises
Try each exercise before opening its hint.
Easy
Easy 1: Spot the broken environment (15 minutes)
A teammate says "it works on my machine". List four things that could differ between their environment and yours (think: Python version, installed packages, package versions, operating system). For each, name the file or command in this module that removes the difference.
Done when: you have four differences, each paired with a fix.
Easy 2: Read a pyproject.toml (15 minutes)
Open the pyproject.toml from this module and label every section: name, dependencies, dev dependencies, scripts, tool settings. Then say which line you would change to add a second runtime dependency.
Done when: every section is labelled and you can name the one line to edit.
Easy 3: Predict the validation error (15 minutes)
Imagine Note is a Pydantic model with title: str = Field(min_length=1, max_length=80) and body: str. For Note(title="x" * 81, body=""), Note(title="ok") and Note(title=5, body="b"), write down which will fail and why before running them. Then run them.
Done when: your three predictions match, or you can explain each difference.
Hint
Pydantic converts compatible values by default. Decide whether the number 5 is accepted for a text field before you run it, then check.
Medium
Medium 1: Build the project from nothing (30 minutes)
Without copying the example, create a folder notes-service with the layout from the diagram, a virtual environment and a pyproject.toml with one dependency (pydantic). Write a Note model with title (1 to 80 characters) and body (any string). Show that Note(title="", body="x") fails.
Done when: pip install -e . works in a fresh environment and the failing case prints a clear error.
Hint
if import notes fails after install, check that [tool.setuptools.packages.find] has where = ["src"].
Medium 2: Make the type checker earn its keep (20 minutes)
Add mypy as a dev dependency and set strict = true. Write a function longest_title(notes: list[Note]) -> str that returns the longest title. Run mypy src. Then deliberately return None for an empty list and read the error. Fix it two ways: return str | None, or raise ValueError.
Done when: you can explain when each fix is the better choice.
Hint
max(notes, key=lambda n: len(n.title)) works only when the list is non-empty.
Medium 3: Reproduce an environment (30 minutes)
Install your project, run pip freeze > requirements.lock, delete .venv/, create a new one and install only from the lock file. Confirm the tests still pass. Then write down two ways the lock file could go stale.
Done when: the second environment matches the first, version for version.
Hint
a lock file made on one operating system may not install on another if packages differ per platform.
Hard
Hard 1: Add a command with its own test (45 minutes)
Add a project.scripts entry so that notes add "Title" "Body" prints the note as JSON. Keep argument parsing in cli.py and the logic in another module. Add tests that call main(["add", "T", "B"]) and check the return code, and one that passes a title longer than 80 characters and expects a non-zero code and a readable message (no traceback).
Done when: the command works after pip install -e ., and both tests pass.
Hard 2: Validate a model reply (45 minutes)
A model was asked to return {"city": "...", "temperature_c": ...} but replied with {"city": "Pune", "temperature_c": "warm"}. Write a Pydantic model Weather, parse this reply, catch the ValidationError and turn it into a one-sentence message you could send back to the model asking it to fix the reply.
Done when: valid JSON parses, invalid JSON produces a readable correction request, and you have tests for both.
Hint
exc.errors() gives a list of dictionaries with loc and msg. Join them into one sentence.
Hard 3: A settings object that refuses nonsense (60 minutes)
Design a RunConfig model for an agent run with max_steps (1 to 50), timeout_seconds (positive, at most 600) and model_name (non-empty). Add a rule that timeout_seconds must be at least max_steps (a model validator). Write a table-driven test with at least eight cases, valid and invalid, and check that the error text names the field.
Done when: all eight cases behave as the table says and every error names the offending field.
Hint
look for model_validator(mode="after") in the Pydantic docs; pytest.mark.parametrize takes a list of tuples.