pytest: writing and running tests in Python

pytest runs your tests and names the ones that fail: a function prefixed with test_, one assert, and a single command run from the project root.
5 min read
Believemy logo

Definition

Writing code and hoping it works only holds up for so long. pytest checks it for you and names what fails.

A test, here, is a plain function whose name starts with test_, and which holds at least one assert. No class to create, no special method to call: the function is the test.

An example, on a basket that adds up prices.

PYTHON
# file test_basket.py
from basket import total

def test_adds_the_prices():
    assert total([2.5, 3.5]) == 6.0

def test_empty_basket_is_zero():
    assert total([]) == 0

The command runs from the root of the project, with no argument: the tool looks for test_*.py files, imports them, calls every test* function, and details each failure.

BASH
pytest

pytest is not part of the standard library: it is installed with pip, preferably inside a virtual environment belonging to the project.


The problem it solves

With no test tool, checking that a program still works after a change means running it by hand and reading whatever a print puts on screen here and there. That method holds up to two hundred lines, while a project stays small.

Beyond that, nobody replays the fifteen edge cases of last month from memory, and one fix breaks another with nothing to report it.

An automated test freezes the check. The question asked after every change is no longer "does it still work", answered from memory, but "what stopped working", answered by the tool naming the file, the line and the value it got.


Against unittest

Python already ships unittest, a testing module modelled on JUnit. Here is what is gained, and what is lost, by installing a second one.

On this pointunittestpytest
InstallIncluded in PythonTo be installed
Writing a testA method inside a classA free function
CheckingSome thirty assertX methodsThe assert keyword alone
Preparing datasetUp, before every testFixtures asked for by name
Failure messageBoth values if the right method was pickedBoth values, always

The last row is what tips most projects over: with pytest, a failing assert result == expected is enough, the tool rewrites the expression and shows both sides. With unittest, someone would have had to guess the right assertX method among some thirty of them.

The other way round, unittest keeps a real advantage: no dependency to install, which matters for a library shipped to other people. pytest runs tests written for unittest anyway, so the move can be made one file at a time.


The first-day trap

The test_ prefix on the file and on the function is not a matter of style: it is the very mechanism by which pytest finds what to run.

Warning

A file called basket_test.py or a function called check_total never run. The tool announces collected 0 items, exits in a fraction of a second and reports no failure at all. Nothing turned green: nothing was run, sometimes for weeks.

The second setback is a ModuleNotFoundError on your own code, while the file sits right next to it. pytest adds the root it detected to the import path, not the current folder: running the command from the root of the project settles most cases, and installing the project in editable mode settles the rest.


Fixtures, parametrising and expected exceptions

Three needs come up often. The first: data that several tests share, a basket already filled for instance, declared once as a fixture with a decorator and asked for by every test that names it among its arguments.

The second: a check to replay over several values, without duplicating the function, thanks to parametrising, each value counted as a separate test.

The third: checking that an exception is indeed raised, through a dedicated context manager that fails if nothing was raised inside it.

All three, gathered in a single file.

PYTHON
import pytest
from basket import total

@pytest.fixture
def sample_basket():
    return [2.5, 3.5, 4.0]

def test_total(sample_basket):
    assert total(sample_basket) == 10.0

@pytest.mark.parametrize("price", [-1, -0.5])
def test_rejects_a_negative_price(price):
    with pytest.raises(ValueError):
        total([price])


What it does not prove

A green suite does not say the code is correct. It only says the cases that were written behave as expected, which is very different.

The classic mistake is testing the language rather than one's own logic: checking that an addition gives the right total, or chasing a coverage percentage. Those tests always pass, because they cover behaviour Python already guarantees, and so they never catch anything.

A useful test covers a rule that could change by accident: a capped discount, a rounding, an expected refusal. And pytest replaces neither ruff, which reviews the shape of the code, nor mypy, which checks type consistency without running anything. The three catch faults of a different nature, complementary rather than redundant.


Frequently asked questions

Question

Is pytest worth installing when Python already ships unittest?

In most projects, yes. The shorter syntax makes people write more tests, and it is the number of tests actually written that protects a codebase, never their elegance. The real exception is a library shipped to others, where unittest keeps the advantage of imposing no extra dependency.

Question

How many tests should a project write at the start?

Fewer than you would imagine. One test on the calculation that carries the value of the product, then one test per fixed bug, the one reproducing the bug before the fix is even written. That rule builds a useful suite without ever spending a dedicated session on it.

Question

How can a single test be run instead of the whole suite?

By following the file path with :: and the name of the function, for instance pytest tests/test_basket.py::test_total. The -k option filters by name fragment and -x stops everything at the first failure, which saves reading fifty errors coming from one single cause.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.