Skip to content

Writing Concise Tests with Plain assert

pytest rewrites asserts to show helpful diffs.

test_strings.py
 
def test_username_format():
    username = "alice_01"
    assert username.startswith("alice")
    assert " " not in username

If a check fails, pytest shows:

  • the values involved
  • a readable explanation
test_exceptions.py
import pytest
 
 
def parse_age(s: str) -> int:
    age = int(s)
    if age < 0:
        raise ValueError("age must be >= 0")
    return age
 
 
def test_parse_age_negative():
    with pytest.raises(ValueError):
        parse_age("-1")

Prefer many small asserts over one huge assert.

test_shop.py
def test_tax():
    assert price_with_tax(100) == 120.0
 
def test_in():
    assert "cat" in "concatenate"
 
def test_almost():
    assert price_with_tax(19.99) == 23.99

Measured failure output:

pytest -q
>       assert price_with_tax(100) == 121.0
E       assert 120.0 == 121.0
E        +  where 120.0 = price_with_tax(100)

The + where line is the part worth noticing: pytest reports not only the comparison but the sub-expressions that produced it, so you often do not need to open the file.

raises.py
import pytest
 
def test_negative_raises():
    with pytest.raises(ValueError, match="must not be negative"):
        price_with_tax(-1)

match= takes a regular expression and is searched against the message. Without it, the test passes for any ValueError — including one raised for a completely different reason later.

capture.py
def test_message_details():
    with pytest.raises(ValueError) as exc:
        price_with_tax(-1)
    assert "negative" in str(exc.value)
parametrize.py
@pytest.mark.parametrize("price,rate,want", [
    (100, 0.2, 120.0),
    (50, 0.1, 55.0),
    (0, 0.2, 0.0),
    (19.99, 0.2, 23.99),
])
def test_tax(price, rate, want):
    assert price_with_tax(price, rate) == want

Four independent tests, each named and reported separately. A loop inside one test stops at the first failure and hides the rest; parametrize runs them all and tells you exactly which inputs broke.

diagram Diagram mermaid
approx.py
assert 0.1 + 0.2 == pytest.approx(0.3)
assert result == pytest.approx(expected, rel=1e-3)

pytest.approx is the equivalent of assertAlmostEqual, and it works inside lists and dicts too — assert [0.1 + 0.2] == pytest.approx([0.3]).

sketch A loop against parametrize p5.js
A loop inside one test stops at the first failing case. parametrize makes each case its own test, so every failure is reported in one run.
pch.quizTag pch.quizDefaultTitle
  1. What does the line starting with + where add to a pytest failure?

    pch.quizShowAnswer

    B — the value of a sub-expression, such as where 120.0 = price_with_tax(100) — Reporting the intermediate value is often enough to diagnose the failure without opening the file.

  2. Why prefer @parametrize over a for loop inside one test?

    pch.quizShowAnswer

    B — the loop stops at the first failing case, while parametrize runs every case as its own test and reports all failures in one run — Each parametrized case is named independently, so a failure identifies exactly which inputs broke.

  3. What is wrong with pytest.raises(ValueError) without match=?

    pch.quizShowAnswer

    B — it passes for any ValueError, including one raised later for an entirely different reason — match= takes a regular expression searched against the message, which keeps the test tied to the failure it was written for.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading