Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

argument
A value you put inside the parentheses when you call a function, which the function then receives.
block
A group of lines that belong together under one opening line, such as the body of an if or a loop.
branch
A path a program can take when a condition sends it one way or another.
call site
The place in the code where a function is called, as opposed to where it is defined.
class
A template that bundles data with the code that works on it; each copy has its own values.
dictionary
A collection of key and value pairs, where a key is used to look up its value.
exception
An error that stops the normal flow of a program unless some code catches it.
function
A named piece of code that does one job and can be called whenever you need it.
function object
A function treated as a value, so it can be stored in a variable, passed to another function, or returned.
indentation
The spaces at the start of a line that show which block it belongs to.
integer
A whole number with no decimal point, such as 3, 0 or -12.
list
An ordered collection of values that you can grow, shrink and reach into by position.
parameter
A name in a function definition that stands for the value the caller will supply.
return value
The result a function hands back to the code that called it.
scope
The part of a program where a name can be used; a name made inside a function is not visible outside it.
traceback
The report Python prints when a program fails, listing the calls that led to the error.
variable
A name that refers to a value, so you can use that value again later or change it.

Python from the ground up

Testing your code

Writing down what should be true, so a change that breaks it says so — the habit that separates working code from code that worked once.

Part 15 of 15

Advanced16 min readBack to Courses

How do you know it works?

Running a program and reading the output proves that one run worked. It does not prove the function is right — only that the values you happened to try gave the answers you happened to expect.

A test writes the expectation down so the machine can check it, on every run, without you. In Python the smallest test is one line: assert followed by something that must be true.

Three lines that say what the function means

def add(a, b):
    return a + b

assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0

print("All tests passed.")

A failed assert is information

When the condition is false, Python stops at that line with an AssertionError. That is the point: a test that can fail is what tells you when a later change breaks something that used to work.

So the tests worth writing first are the awkward ones. Empty input, zero, one item, a negative number, a word with a space in it — the cases you would not bother trying by hand are exactly where the bugs live.

The habit matters more than the tool

Real projects use a test runner — pytest is the common one — which finds every test, runs them together, and prints a report. Underneath, most of its tests are the same idea: call the function, assert what should come back.

Write the test while the function is fresh, next to the line that defines it. A test written a week later tests what you remember, not what you decided.

Prove the initials

The initials function only returns the first letter. Fix it so it returns the first letter of every word, then prove it with three asserts: a two-word name, a single word, and a three-word name.

Output
Click "Run" to execute your code

A test names a behaviour

The name of a test is the sentence a failure prints. test_discount_of_zero_leaves_price_alone tells you what broke before you open the file; test_1 tells you nothing. Renaming tests is cheap, and it turns a red run into a diagnosis.

Keep one claim per test where you can. Two asserts in one function are fine when they describe the same behaviour, but a test that checks three unrelated things fails as a unit, and the first failure hides the others.

Tests are documentation that runs

A test is an example of the function being used, with the expected answer written down. Unlike a comment, it cannot drift silently: change the function and the example either still holds or fails. That is why a well-tested module is easier to read than a heavily commented one.

It also gives you permission to change the implementation. When the behaviour is pinned by tests, a rewrite is a calculation rather than a gamble: run the suite, and the parts that still behave the same are still correct.

None of this requires a framework. A file of asserts you run before you commit is already the habit; pytest only makes the report nicer.

Do not test the language, though. assert 2 + 2 == 4 checks Python, not your program, and it will never fail for a reason you can fix. Test the decisions your code makes: the branch, the calculation, the default.

Do not test private details either, such as the exact wording of an error you may reword next month. Test the promise the function makes to its caller. A test coupled to the implementation breaks when you improve the code, which teaches the wrong lesson about changing it.

A test you cannot make fail is not testing anything. Change an expected value once and watch it break — if it does not, the test is not running.

A test is a claim you can run

An assert is a sentence about one call: given these inputs, this is the answer. Keep each claim small enough that a failure names the behaviour, not just the line. Three asserts that check three cases are more useful than one long assert that checks everything at once.

Test the function, not the printing. A function that returns its answer can be checked directly; one that prints can only be checked by reading the output. That is one more reason return is the habit to prefer.

A suite is a list of small claims

A test file collects these claims, often as small functions with one claim each. Naming them test_something is what lets a runner such as pytest find them without being told where to look.

Run the whole file after every change. One failing test names one behaviour; a suite of them turns “I think it still works” into a fact you can check in a second.

Three testing mistakes: comparing with is where == is meant, which asks whether two things are the same object rather than the same value; testing only the happy path, where bugs are least likely; and writing a test that depends on today’s date or a random number, so it passes this morning and fails tomorrow.

Try these next

  • Add an assert for an empty string and decide what initials("") should return.
  • Change one expected value on purpose, run the file, and read the AssertionError.
  • Write two asserts for the same function with different capitalisation.

Prove the discount

The function applies a percentage discount, but it subtracts the percentage instead of the money. Fix it so 10 percent off 200 gives 180, then prove it with asserts for a normal price, a zero discount and a full discount.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What happens when an assert condition is false?