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,TypeErrorare code errors, not check failures; read the message bottom up.try/exceptcatches a specific error;except Exception: passmakes a test green on a broken system.
What to read next
- Working with data — where these checks are applied.
- pytest: your first real test — the shape tests take in projects.