Found an issue with an exercise, or have an idea for a new one? Open an issue first before writing any code — it saves everyone rework if the idea doesn't fit the curriculum sequence.
Let's look at the test file first:
def test_prints_hello_world(run_script):
stdout, _ = run_script("hello_world.py")
assert stdout.strip() == "Hello, World!"run_script is a helper set up once for the whole repo. It runs hello_world.py exactly the way Python would if you typed python hello_world.py, and hands back everything it printed. The test then checks that what got printed matches 'Hello, World!' exactly — capitalization and punctuation included.
Early exercises don't assume you know about functions yet, so they're written as plain scripts — top-level code you'd type straight into the terminal, no def or import. Their tests use a shared run_script fixture (see conftest.py) that runs the file exactly like python <file>.py and checks what it printed.
Once functions show up in the curriculum, exercises switch to the function style you may be more used to seeing: a stub function you fill in, imported directly into the test file. Each exercise's README says which style it is, but you can also tell from the stub file itself — a bare script vs. a def.
Raw pytest failures (IndexError, assert '' == 'Hello, World!') are confusing before you know what a list or an assertion diff even is. Script-style tests instead use two shared helpers from conftest.py:
expect_output(stdout, "expected text")— checks the whole printed output at once, and on failure says plainly what was expected vs. what actually printed.expect_line(stdout, line_number, "expected text")— checks one line of output (1-indexed). If the script hasn't printed that many lines yet, it says so directly ("Have you added a print() statement for this part yet?") instead of raisingIndexError.
Both suppress the Python traceback entirely — the learner only sees the message, not internal test code.
From the repo root, pytest (no arguments) will discover and run every non-skipped test in the repo — handy as a sanity check, but exercises are meant to be done one at a time.
Don't build the folder by hand. Use the generator:
python scripts/new_exercise.py <category_path> "<Exercise Title>" [--style script|function]--style script (the default) scaffolds a plain-script exercise with no functions — use this for anything that comes before functions are introduced in the curriculum. Its tests use the shared run_script fixture in the root conftest.py.
--style function scaffolds the def-and-import style — use this once functions have been taught.
Examples:
python scripts/new_exercise.py foundations "Say Hi"
python scripts/new_exercise.py foundations "Number Checker" --style functionThis creates a numbered directory (auto-incremented within that category) with a README stub, an empty exercise file, a placeholder test file, and a matching solution/ folder. Fill in:
- The
README.md— a clear task description, plus any notes on edge cases. - The exercise stub — keep it minimal, just a function signature and a docstring or
pass. - The test file — write the real tests here. The first test should be un-skipped; every subsequent test should start with
@pytest.mark.skip(reason="...")so learners unlock them one at a time. solution/— a working reference implementation, and the same test file with every@pytest.mark.skipremoved.
Before opening a PR, run pytest from the solution/ directory to confirm your reference solution actually passes its own fully-unlocked tests.