Section 1: pytest essentials
Why we test and what pytest gives us
Tests are small programs that check your code does what you expect. The real value is not proving that code works today. It is knowing quickly when a change breaks something, so you can refactor, upgrade dependencies and fix bugs without fear.
Without tests, every change requires manually clicking through the app or rerunning scripts, and people stop making improvements because the risk feels too high. With a good test suite, you run one command and know within seconds.
Why pytest
Python ships with unittest, which works fine. Most projects today use pytest because it removes boilerplate:
- Tests are plain functions, not methods on a class.
- You use the normal
assertstatement, and pytest rewrites it to show detailed failure messages. - Fixtures provide setup and teardown in a reusable, composable way.
- A large ecosystem of plugins covers coverage, async code, Django, parallel runs and more.
pytest can also run existing unittest tests, so you can switch gradually.
What makes a test useful
A useful test is:
- Fast, so you run it often. Milliseconds per test, seconds for the suite.
- Isolated, so it does not depend on other tests or on the order they run in.
- Deterministic, so it gives the same result every time. No real network calls or current time without control.
- Focused on behaviour, so it survives refactoring.
We will come back to these four properties throughout the course.
The project
Our codebase is Billable, a small invoicing service. It calculates invoice totals with discounts and taxes, stores invoices in a database, exposes a small web API and charges cards through an external payment provider. It currently has no tests, and as you will discover, a few bugs.
Clone the course repository, install the dependencies with uv sync (or create a virtual environment and pip install -e ".[dev]"), and check that pytest --version works. In the next lesson we write our first test.