← back to section

Code that computes and prints is not a test yet. It becomes one when it decides for itself whether things add up and says so loudly when they do not. One word does that: assert.

assert: a claim that must hold

assert code == 200

It reads as "I claim: code equals 200". If that is true, the line does nothing and execution continues. If it is false, the program stops with AssertionError and the test counts as failed.

Compare with the manual version:

if code != 200:
    print("did not match")      # printed, and on we go — the "test" passed

The difference matters: print breaks nothing, so such a test is green whatever happens. assert stops and marks the failure.

The message is half the value

assert without an explanation says little on failure:

AssertionError

So a message goes after the condition, separated by a comma:

assert code == 200, f"expected 200, got {code}"
AssertionError: expected 200, got 404

The rule is simple: the message must contain the actual value. "Did not match" does not help; "expected 200, got 404" ends the question without debugging.

What belongs in the message: what was checked, what was expected, what actually arrived, and — for large responses — an identifier to find it by (order number, trace id).

One check, one claim

assert response["status"] == "PAID"
assert response["total"] == 4990

is better than

assert response["status"] == "PAID" and response["total"] == 4990

In the first case the failure tells you which part did not match. In the second you only learn that "one of the two" did not.

Errors that are not about the check

Besides AssertionError there are errors in the code itself — a different thing. A test failing with KeyError: 'total' means you asked for a field that is not there. That may be a defect (the field must exist) or a bad test (the field is optional). Telling them apart is your job.

The most common ones:

KeyError: 'total'        — no such key in the dict
IndexError               — no element with that index
TypeError                — added a string to a number
NameError                — used a name that does not exist (typo)
AttributeError           — called a method the object does not have
ZeroDivisionError        — division by zero

Read the message bottom up: the last line says what happened, the ones above say where.

try / except: catch an error and stay alive

try:
    total = int(text)
except ValueError:
    total = 0

Catch a specific kind of error, not everything:

try:
    ...
except Exception:        # do not do this
    pass

Such code swallows any breakage, including a typo in a variable name — and the test goes green on a broken system again. pass inside except is almost always a sign that nobody wanted to think about the error.

In tests try/except is needed less often than it seems. A normal test is supposed to fail — that is its job. You catch an error where you check that it happened, or where you need to clean up after yourself.

Checking that an error did happen

live example

try:
    int("not a number")
    assert False, "expected ValueError, but it did not happen"
except ValueError:
    pass     # correct: the error was supposed to happen
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →

pytest has a shorter form (pytest.raises), but the mechanics are the same and worth understanding.

What to solve

In short

  • assert condition, "message" is what turns code into a test: on falsehood it stops and marks the failure.
  • The message must carry the actual value: "expected 200, got 404".
  • One check, one claim — otherwise you cannot tell what broke.
  • KeyError, IndexError, TypeError are code errors, not check failures; read the message bottom up.
  • try/except catches a specific error; except Exception: pass makes a test green on a broken system.