Skip to main content

pytest assertions & structure

Level 3: Level 3: Patternsmedium16 minpytesttestingassertionstdd

Make a suite of pytest tests pass by implementing the module they cover.

Why tests are the code that guards your code

When you change balance_after six months from now, the only thing standing between a clean refactor and a corrupted account balance is a test that still remembers what "correct" meant. On a real team, a pull request without tests is a pull request nobody can safely merge, because reviewers cannot tell whether it works, only that it compiles. pytest is the tool most Python shops reach for because it turns "I think this works" into a repeatable, machine-checkable claim.

The mental model: plain functions, plain asserts

pytest has almost no ceremony. You write a normal function whose name starts with test_, put a plain assert inside it, and run pytest. Discovery is convention-based: pytest walks the directory, imports files named test_*.py (or *_test.py), and runs every test_-prefixed function it finds. No base class, no registration, no main.

Check yourself
A test contains one line: assert balance_after(100, [10, -30, 5]) == 85. The function is buggy and returns 90. What does pytest print when the test fails?

The magic is in the assert. pytest rewrites the assert statements in your test files as it imports them, so a failing assert reports the actual and expected values instead of a bare AssertionError. That is why assert balance_after(100, [10, -30, 5]) == 85 is enough: on failure you see the number it actually got.

Arrange, act, assert

A readable test has three beats: set up inputs, call the code, check the result.

# tests/test_account.py
from bank.account import balance_after

def test_mixed_transactions():
    start, txns = 100, [10, -30, 5]   # arrange
    result = balance_after(start, txns)  # act
    assert result == 85               # assert

Test-driven development runs this loop backwards: write the failing test first (red), then write the smallest code that makes it pass (green). Here the tests already exist. Your job is to implement balance_after so start + sum(transactions) produces the expected total and the suite turns green. The Practice puts that same function in a bank/account.py package with a real test file, plus hidden cases you cannot peek at.

Pitfall: a test that never runs still "passes"

Check yourself
You add a function named check_deposits to tests/test_account.py. It contains an assert that would definitely fail. You run pytest. What do you see?

If you misspell the prefix and name a function check_deposits instead of test_deposits, pytest silently skips it. The suite goes green while testing nothing, which is worse than a red suite because it hands you false confidence. The same trap hits a file named account_tests.py (wrong pattern) or a helper that raises but is never called. The fix: keep the test_ prefix, name files test_*.py, and run pytest -v to read the count of collected tests. If the number looks low, something is not being discovered.

Check yourself
Money becomes float in your model, and a test now asserts: balance_after(0.0, [0.1, 0.2]) == 0.3. Does that test pass?

Interview nuance: never compare floats with == in a test. balance_after(0.0, [0.1, 0.2]) == 0.3 is False, because IEEE 754 stores 0.1 + 0.2 as 0.30000000000000004. An interviewer asking "how would you test a function that returns a float" wants to hear about tolerance, not exact equality. In pytest you write assert result == pytest.approx(0.3), which passes if the values are within a small relative tolerance. Integer transactions dodge this, but the moment money becomes float, exact-equality tests get flaky and the real bug hides behind the noise.

Worked example (Python)
def balance_after(start, transactions):
    return start + sum(transactions)


# what a pytest test would assert:
assert balance_after(100, [10, -30, 5]) == 85
print("all good")

Apply

Your turn

The task this lesson builds to.

Warm-up (one file): implement balance_after(start, transactions). Add every signed amount in transactions to start and return the result.

balance_after(100, [10, -30, 5]) is 85.

2 hints and 4 automated checks are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Make the pytest suite pass: implement balance_after(start, transactions) in bank/account.py so it applies the signed transactions to start. Open the visible test file to read the cases; some tests are hidden.

3 hints and 2 automated checks are waiting in the workspace.