Module 2: Your First Pipeline Pytest In Ci
4. The steps that prepare the ground: checkout and setup-python
Description
You already have the when (on:) and the where (runs-on:). This lesson starts the what: the steps. And it starts with the two most beginners forget, because they seem "obvious" and they aren't —the runner knows nothing about you until you give it to it—. By the end you'll understand why the runner starts completely empty, what exactly actions/checkout (bring your code to the machine) and actions/setup-python (install the Python you asked for) do, how parameters are passed to an action with with:, and why the order of these steps isn't negotiable.
These two steps are the foundations. Without them, lesson 6's pytest would have neither code to test nor the correct Python to test it with. They're so routine they appear nearly identical in any Python workflow in the world, and that familiarity is an advantage: learning them well once is learning them forever. The module's rule still holds —the workflow is content we explain—; but understanding what each step does lets you write it with judgment instead of copying it blindly.
Connection to the module: in lesson 2 you classified these steps as type uses (they bring reusable actions) without opening them; here we open them. They prepare the ground the following lessons use: lesson 5 installs the dependencies on top of the Python setup-python put in place, and lesson 6 runs pytest on top of the code checkout brought. Stay on the boundary: why the runner can differ from your machine and what to do when CI fails and your local passes is module 3; running across several Python versions at once (the matrix) is module 4. Here we put one version, on one machine, well.
An empty hotel room that you equip
Imagine you book a hotel room to work on a project for a weekend. When you arrive, the room is spotless… and empty of your things. It has a bed, a desk, electricity —the basic infrastructure—, but it doesn't have your computer, or your documents, or the specific tools for your project. No one left your files there; why would they? It's a room that gets completely cleaned between guests, precisely so that what the previous one did doesn't affect you.
To be able to work, you do two things as soon as you enter. First, you take out your documents —the project material— and put them on the desk. Without them you have nothing to work on. Second, if your project needs a particular tool the room doesn't provide —say, a lamp of a certain light to review plans—, you install it. Only after those two steps does the empty room become your workspace.
GitHub's runner is that hotel room. It starts clean and empty of your things —a freshly formatted machine, with the operating system and general utilities, but without your code and without a guarantee of having the Python version you need—. And it gets completely cleaned between runs, on purpose, so that no run contaminates the next (that cleaning is what gives module 1's reproducibility: each run starts equally fresh). The first two steps of your workflow are taking out your documents and setting up your tool: checkout brings your code, setup-python installs the Python you ask for. Without those two, the room stays empty and there's nothing to test.
actions/checkout: bringing your code to the runner
- uses: actions/checkout@v5
This is the first step of almost every workflow, and it does one fundamental thing: it downloads your repository into the runner. Before this step, the machine doesn't have a single file of yours —no reservo/, no test_*.py, no requirements.txt—. After this step, your whole project is there, in the exact state of the commit that triggered the workflow, ready for the following steps to use.
The name says it: "checkout" is Git's term for "put the working tree at this commit". The action does precisely that on the clean machine: it clones your repo and leaves it at the corresponding commit. Notice what this implies about what gets tested: CI doesn't test what you have on your local disk or the latest of main, it tests the code of the commit that triggered the run. If you pushed a commit with a bug, checkout brings that commit with the bug, and that's why CI catches it. It's the direct connection between "I pushed this" and "CI tested exactly this".
It's an official GitHub action —the actions/ prefix signals that it's published by the actions organization, maintained by GitHub—, so you can trust it without a second thought. You don't write it; you use it. That's the whole point of uses: bringing proven pieces instead of reimplementing how a repo is cloned on a machine.
actions/setup-python: putting the correct Python
- uses: actions/setup-python@v5
with:
python-version: "3.14"
The Ubuntu runner ships some Python by default, but you shouldn't trust which: it may be a different version from the one your project needs, and depending on "whatever comes" is exactly the kind of fragile assumption CI exists to eliminate. actions/setup-python solves that: it installs and leaves active the exact Python version you ask for, so the following steps —install dependencies, run pytest— use that one and not another.
Here something new appears: the with: block. An action like checkout works without configuration —"bring the code", there's nothing to adjust—, but setup-python needs you to tell it which version. The with: is how parameters are passed to an action: it's a map (remember lesson 2's YAML) of options the action understands. Here we pass it one, python-version: "3.14", which means "install Python 3.14". Read it as filling out a form: the action has fields, and with: fills them in.
Two details about python-version that avoid surprises:
- The quotes around
"3.14"matter. In YAML, without quotes,3.14is interpreted as the decimal number three-point-fourteen, and3.10would be interpreted as three-point-one (the trailing zero is lost in a number!), asking for Python 3.1 instead of 3.10. Putting the version in quotes treats it as the text"3.10"and avoids that classic stumble. Get in the habit of always writingpython-versionin quotes. - You can ask for the version with as much detail as you want.
"3.14"takes the latest release of the 3.14 series;"3.14.0"pins the exact patch. For this module's CI,"3.14"is fine: we ask for the series the guide uses and let the runner bring its latest patch. Pinning the exact patch is a finer determinism decision that grazes module 3.
Worked example: the two steps, in order, with what they leave ready
Here are the workflow's first two steps, isolated to see them alone:
jobs:
test:
runs-on: ubuntu-latest
steps:
# Step 1: bring the code of the commit that triggered the run.
- uses: actions/checkout@v5
# Step 2: install and activate Python 3.14 on the runner.
- uses: actions/setup-python@v5
with:
python-version: "3.14"
# ...the install-dependencies and run-pytest steps follow here
Remember: we don't run a real CI. But we can describe exactly what each step leaves ready, because it's deterministic. What to expect (the runner's state after each step):
Runner's initial state: clean Ubuntu machine, without your code,
with some default Python (not guaranteed).
After step 1 (checkout):
/home/runner/work/reservo/reservo/
├── reservo/ ← your code is here now
│ ├── models.py
│ ├── pricing.py
│ └── ...
├── test_pricing.py
├── test_refunds.py
└── requirements.txt
After step 2 (setup-python 3.14):
$ python --version
Python 3.14.0 ← the version you asked for, active
That path /home/runner/work/reservo/reservo/ is the real working folder of a GitHub runner; we put it so you recognize the format when you see it in a CI log (lesson 7). The essence of the block: after these two steps, the hotel room stopped being empty. There's code on the desk (checkout) and the correct tool set up (Python 3.14). Now the following steps do have something to work on.
You can verify the "real" half of this on your own machine: python3 --version tells you which Python you have active, just as setup-python would leave it on the runner. When in lesson 6 you run the suite locally with Python 3.14.0, you'll be in the same state as this runner after step 2 —code present, correct Python— and that's why your pytest output will be the one CI would see.
The order isn't negotiable
Notice the sequence: checkout first, setup-python after, and both before any pip install or pytest. That order has a dependency logic worth making explicit, because breaking it produces confusing failures.
- Checkout goes first because everything else needs your code. You can't install
requirements.txtif the file isn't on the machine, and you can't runpytestonreservo/ifreservo/doesn't exist yet. If you putpip install -r requirements.txtbefore the checkout, it would fail with "no such file or directory": the file hasn't been brought yet. - Setup-python goes before installing and testing because
pipandpytestrun on a specific Python. If you install dependencies before pinning the version, you install them on the runner's default Python —maybe the wrong one—, and thensetup-pythonchanges the active Python and your packages "disappear" (they stayed on the other Python). The symptom is a baffling "no module named pytest" right after having installed it.
The mnemonic rule: first the ground (code and Python), then what leans on it (dependencies and tests). Each following step assumes the previous ones already happened. The steps run top to bottom by design, and that order is your tool for expressing "this depends on that".
An honest note about action versions (@v5)
The @v5 in actions/checkout@v5 and actions/setup-python@v5 pins the major version of the action you use. It's the same @version you saw in lesson 2, and it plays here the role pinning any dependency plays: it guarantees you use the same piece tomorrow as today, without surprises from an update you didn't ask for.
Now, the honest part: official actions publish new major versions from time to time. When you read this, it's very possible checkout and setup-python are already on a higher major version than v5 (GitHub's actions have advanced: v5, v6, and beyond). That doesn't invalidate anything you learned; the behavior of these steps —bring the code, set up Python— is stable across major versions. What changes are internal details (the version of Node the action uses under the hood, performance tweaks).
The correct habit, then, isn't to memorize a number, but this: always pin a major version with @vN, and check the action's page on GitHub's Marketplace to know which is current. Never use an action without @version (you'd be at the mercy of changes) or pin to something so old it no longer receives maintenance. In this guide we use @v5 as a concrete and valid version to illustrate; in your project, look at the actions/checkout and actions/setup-python pages and use the major they recommend. The lesson isn't the 5; it's the habit of pinning the major and checking which is current.
Common mistakes
Forgetting checkout and running pytest over nothing (absent ground). What happens: someone writes a workflow that jumps straight to setup-python and pytest, without checkout, and the pytest step fails with "no tests ran" or "no such file", because the code never reached the runner. Why it happens: on your machine the code is "always there", so you forget that on the clean runner it isn't until you bring it. How to spot it: if pytest in CI reports it found nothing, or pip install -r requirements.txt doesn't find the file, suspect an absent checkout. How to fix it: actions/checkout is the first step of practically every workflow; always put it, and always first. It's taking out your documents before trying to work.
Installing dependencies before setup-python and losing the packages (inverted order). What happens: someone puts pip install before setup-python, installs on the runner's default Python, and then setup-python activates another version where those packages don't exist; the later pytest fails with "no module named pytest". Why it happens: you think of "install" as an independent step, without noticing you install on a specific Python. How to spot it: a "no module named X" right after an apparently successful pip install is the signature of this error. How to fix it: setup-python goes before any pip install, so the dependencies install on the correct and active Python. First pin the Python, then install on it.
Writing python-version: 3.10 without quotes and asking for Python 3.1 by mistake (numeric YAML). What happens: someone puts python-version: 3.10 without quotes; YAML reads it as the number 3.1 (a decimal's trailing zero means nothing numerically), and setup-python tries to install Python 3.1, which doesn't exist in modern versions, failing or bringing something unexpected. Why it happens: without quotes, YAML treats 3.10 as a number, and 3.10 == 3.1 as numbers. How to spot it: if setup-python complains about a strange or nonexistent Python version, check whether you're missing the quotes. How to fix it: always put python-version in quotes —"3.10", "3.14"—, so it's treated as text and the version arrives exactly as you wrote it.
Exercises
Exercise 1 — Order the steps and justify. Here are four workflow steps, out of order. Put them in the correct order and explain, for at least two consecutive pairs, why one must go before the other.
(a) - run: pytest
(b) - uses: actions/setup-python@v5
with:
python-version: "3.14"
(c) - uses: actions/checkout@v5
(d) - run: pip install -r requirements.txt
See solution
The correct order is (c) → (b) → (d) → (a): checkout, setup-python, pip install, pytest.
- uses: actions/checkout@v5 # (c)
- uses: actions/setup-python@v5 # (b)
with:
python-version: "3.14"
- run: pip install -r requirements.txt # (d)
- run: pytest # (a)
Why the order matters, in the pairs:
- (c) before (d):
pip install -r requirements.txtneeds therequirements.txtfile to exist on the machine, and that file arrives with the checkout. Without checkout first, pip doesn't find the file. - (b) before (d):
pip installinstalls on a specific Python; if you install before pinning the version withsetup-python, the packages end up on the default Python and "disappear" when setup-python activates another. Pinning the Python first guarantees the dependencies stay where pytest will look for them. - (d) before (a):
pytestis a dependency installed in (d); running it before installing it would give "no module named pytest".
The rule that sums it all up: first the ground (code with checkout, Python with setup-python), then what leans on it (dependencies, tests).
Exercise 2 — Detect the version error. A teammate wants to run their tests on Python 3.12 and writes this. The run fails saying it can't find Python 3.1. What's wrong and how is it fixed?
- uses: actions/setup-python@v5
with:
python-version: 3.12
See solution
The problem is the absent quotes on the version. Without quotes, YAML interprets 3.12 as the decimal number three-point-twelve… which numerically is the same as 3.12, but the real danger appears with trailing zeros and with how some tools normalize the number. The textbook case is 3.10 without quotes, which YAML reads as 3.1 (a decimal's trailing zero doesn't change the numeric value), asking for Python 3.1. For 3.12 the risk is analogous depending on how the number is processed; the solution is the same and eliminates all ambiguity: treat the version as text.
Fixed:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
With the quotes, "3.12" is the literal string 3.12 and setup-python receives it exactly. The practical rule —always write python-version in quotes— avoids this whole class of errors without having to reason case by case about which number YAML normalizes and which it doesn't.
Exercise 3 — Explain the empty room. A teammate asks: "If the runner already has Ubuntu and ships Python, why do I need the checkout step and the setup-python one? Shouldn't it just run my tests?". Answer them with the hotel-room analogy and with the technical detail of what's missing without each step.
See solution
An answer that covers both layers:
The runner is like a freshly cleaned hotel room: it has the infrastructure (Ubuntu, electricity, a default Python) but it's empty of your things. It gets completely cleaned between runs —on purpose, so none affects another—, so it doesn't have your code or a guarantee of the Python you need.
- Without
checkout, your code isn't on the machine.pytestwould have nothing to test (reservo/doesn't exist there) andpip install -r requirements.txtwouldn't find the file. Checkout is "take out your documents and put them on the desk". - Without
setup-python, you'd run on the Python Ubuntu ships by default, which may be a different version from the one your project needs. Depending on "whatever comes" is exactly the fragile assumption CI exists to eliminate. Setup-python is "set up the correct tool", pinning the exact version.
And the important nuance: the runner having a Python doesn't mean it has the correct one. "Just run my tests" would work by coincidence if the default Python matched yours and you had no dependencies —but a pipeline is built on guarantees, not coincidences—. The two steps turn the empty room into a workspace that's identical and reproducible on every run.
Summary and next step
In this lesson you laid the foundations of the what. You understood that the runner starts like an empty hotel room —clean on purpose, without your code and without a guarantee of the correct Python— and that two steps equip it. actions/checkout brings your repository to the runner, in the exact state of the commit that triggered the run (that's why CI tests exactly what you pushed). actions/setup-python, with its with: python-version: block, installs and activates the Python version you ask for —always in quotes, so YAML doesn't read it as a number—. You saw why the order is immovable (checkout first because everything needs the code; setup-python before installing because pip and pytest run on a specific Python) and the honest rule about @v5: always pin a major version and check the action's page for which is current, because these actions advance over time.
Before moving on you should be able to: explain why the runner needs checkout and setup-python; write a setup-python with the version in quotes; correctly order checkout, setup-python, install, and pytest justifying each dependency; and describe what fails if you invert the order.
The ground is ready: there's code on the machine and the correct Python active. What's missing is what leans on it. In lesson 5 we install the dependencies: you'll see why the runner has Python but not your packages, how requirements.txt is the list of what's needed, and how python -m pip install --upgrade pip and pip install -r requirements.txt install it on the Python you just pinned. This time there is a part that runs for real —the real pip output— because installing dependencies is something you can reproduce in your own terminal.
Resources
- actions/checkout on GitHub Marketplace — the official page of the checkout action, with its current major version and its options. This is exactly where you check "which is the current
@vN?", just as the lesson's honest note recommends. - actions/setup-python on GitHub Marketplace — the official page of setup-python, with all the
with:options (available versions, cache, version files). The reference for configuring the runner's Python beyondpython-version. - Build and test Python (GitHub Actions documentation) — GitHub's official guide to testing Python projects, with the example workflow that uses these same two steps. Useful for seeing the complete pattern as GitHub documents it.