pytest
pytest is Python’s most used test runner: it finds files named test_*.py, runs every function named test_* in them, and reports each assert that fails with the values involved [1].
The quality question it answers: do the examples I wrote down still hold after this change?
Run for this card with pytest 9.2.0, Python 3.12, October 2026.
Install and quick start
The function under test charges nothing for the first 15 minutes, then 2 per started hour, with members paying half and a cap of 20 for the whole stay; the docstring’s “per day” is looser than the code, one of the questions the later cards run into. (Constructed example.)
src/parking/fee.py:
"""Parking fee: first 15 minutes free, then 2 per started hour, capped at 20 per day."""
import math
def fee(minutes: int, member: bool = False) -> int:
if minutes < 0:
raise ValueError("minutes must be >= 0")
if minutes <= 15:
return 0
hours = math.ceil(minutes / 60)
total = 2 * hours
if member:
total = total // 2
return min(total, 20)
tests/test_fee.py:
import pytest
from parking.fee import fee
def test_free_quarter_hour():
assert fee(15) == 0
def test_one_started_hour():
assert fee(16) == 2
@pytest.mark.parametrize("minutes, expected", [(60, 2), (61, 4), (120, 4)])
def test_started_hours(minutes, expected):
assert fee(minutes) == expected
def test_negative_minutes_rejected():
with pytest.raises(ValueError):
fee(-1)
poetry add --group dev pytest # already there if you followed the Poetry card
poetry run pytest -q # -q: quiet, one character per test
Reading the output
...... [100%]
6 passed in 0.01s
Each dot is one passed test: four functions, but parametrize turned one of them into three tests. An F is a failed assertion, an E an error outside the assertion (an import failure, an exception in setup). -v prints one line per test, -k started runs only tests whose name contains “started”, and --collect-only -q lists the tests without running them:
tests/test_fee.py::test_started_hours[60-2]
tests/test_fee.py::test_started_hours[61-4]
For a failure, pytest prints the assertion with the values it compared; the Hypothesis card shows one in full. Read the bottom line first: it names the test and the comparison that failed.
Common mistakes
ModuleNotFoundErrorat collection. With asrc/layout, the package must be installed into the environment pytest runs in. A systempyteston this project stopped withNo module named 'parking'andInterrupted: 1 error during collection;poetry run pytestpassed.- A test that cannot fail. A test with no
assert, or asserting something always true, passes whatever the code does. mutmut finds these. - Several behaviours in one test. The first failing
assertstops the function, so the others go unchecked. Prefer one behaviour per test, orparametrize. - Checking an exception too loosely.
pytest.raises(ValueError)passes for anyValueError; addmatch=r"^minutes must be >= 0$"when the message matters.matchsearches the message, so an unanchored pattern also accepts longer messages.
In CI
- run: poetry run pytest -q
pytest exits 0 when every test passes, 1 when a test fails, 2 when the run was interrupted, for example by the import error above, and 5 when it found no tests at all.
Go deeper
- Test Levels and Test Doubles: what to test at which level.
- Coverage.py and pytest-cov: what these tests executed.
References
- pytest-dev,“pytest Documentation.” 2026. [Online]. Available: https://docs.pytest.org/
Disclaimer: AI is used for text summarization, polishing and explaining. Authors have verified all facts and claims. In case of an error, feel free to file an issue.