Python for Humans · Draft v0.1 · Oct 28, 2026 · Draft position: chapter 11 · Keep repeatable assertions beside the actual word-count and follower functions, then demonstrate regressions.

Testing

You should already know
Functions, return values, the word counter and the debugging process from chapter 10.
You'll build
Repeatable checks for count_words() and next_words(), attached to the programs they check.
You'll be able to
Choose small known cases, express expectations with assert and demonstrate that a check catches a regression.
You need
Your local Python setup and editor; the generator and the word-counter download from chapter 7.

We repaired the follower dictionary by checking a tiny input. What happens if a later edit brings the same mistake back?

We could inspect the dictionary by hand after every change. But remembering every case, and noticing every difference, becomes another job.

Let's keep the checks in Python code so we can run them again.

Turn an Expectation into a Check

Open your working my_generator.py. Immediately after the next_words() definition, before loading Shakespeare or building its follower mapping, add:

assert next_words('red blue red green') == {
    'red': ['blue', 'green'],
    'blue': ['red']
}

The braces span several lines for readability, just as they did in our generator's hand-written dictionary.

assert evaluates a condition. If it's true, execution continues. If it's false, Python raises AssertionError and stops this run unless something handles it.

Here, we compare the function's returned dictionary with an expected dictionary we worked out ourselves. Dictionary equality checks the keys and their values; the order in which the keys are displayed doesn't matter. The order and repeated entries inside each follower list do matter.

Save and run your generator with its usual terminal command. If the function is correct, you'll get its normal output. A passing assertion doesn't print a message of its own.

Make the Check Fail

Deliberately change the existing-key branch inside next_words() back to the bug from the debugging chapter:

                followers[previous] = [word]

Save and rerun. The assertion should fail before Python loads Shakespeare or generates a passage.

Read the traceback. It points to the assertion that expected both followers. Use a temporary print or the debugger if you need to inspect the actual result. We already know the explanation: assigning a new list loses the earlier follower.

Repair the branch with:

                followers[previous].append(word)

Save and rerun. The assertion passes again.

A check that continues passing when we deliberately break the behavior it is meant to cover deserves investigation. Perhaps it never ran, or perhaps its input doesn't expose that mistake.

As the checks grow, group them in functions. Replace the single top-level assertion with these definitions, keeping them after next_words():

def test_follower_mapping():
    assert next_words('red blue red green') == {
        'red': ['blue', 'green'],
        'blue': ['red']
    }


def test_repeated_followers():
    assert next_words('a b a b') == {
        'a': ['b', 'b'],
        'b': ['a']
    }


def test_short_texts():
    assert next_words('') == {}
    assert next_words('red') == {}
    assert next_words('red blue') == {'red': ['blue']}

The first case checks different followers of the same word. The second checks that a repeated transition stays repeated; our generator uses those entries to weight its choices. The short cases check that no pair is invented when there's no next word.

Before the normal file-reading and generation code, add explicit calls:

test_follower_mapping()
test_repeated_followers()
test_short_texts()
print('Follower checks passed.')

These are ordinary functions. A name starting with test_ doesn't make Python call it automatically when you run an ordinary script. The three call lines make the checks happen.

The file now runs in this order:

  1. Define next_words() and the test functions.
  2. Call the test functions and print the passed message.
  3. Load the text and generate a passage using the same next_words().

The test functions must be defined before their calls. All checks must finish before the passed message appears. A failing check stops the run before that message and the normal generation work.

Check the Word Counter Too

If you haven't kept your counter, download count_words_function.py from the functions chapter and put it in your course folder. Use Download .py below that code box.

Open it in your editor. It should contain count_words(), followed by your example calls or a Shakespeare analysis. Run it once before making changes to confirm the download works.

If your saved version reads Shakespeare, keep the text file alongside it and use encoding='utf-8' in the open() call, as in Keeping Your Work.

Our counter's rule is modest: split on whitespace, lowercase each word, strip .,!?;: from its ends, and skip a word if nothing remains. If you deliberately changed that rule earlier, decide which behavior you want to keep and make the expectations match that stated rule.

After the real count_words() definition, add:

def test_word_cleanup():
    assert count_words('Dragon, pizza! DRAGON.') == {
        'dragon': 2,
        'pizza': 1
    }
    assert count_words('... ! ;') == {}


def test_empty_text():
    assert count_words('') == {}
    assert count_words('   ') == {}


def test_independent_calls():
    assert count_words('dragon dragon') == {'dragon': 2}
    assert count_words('pizza') == {'pizza': 1}

Then add these calls after the test definitions, before the normal sample calls or file reading:

test_word_cleanup()
test_empty_text()
test_independent_calls()
print('Word-count checks passed.')

Run the file from your terminal:

Windows macOS / Linux
python count_words_function.py python3 count_words_function.py

Use py instead of python on Windows if that's your established command.

The tests exercise the same helper that the rest of the program uses. There's no second copy of count_words() inside the test functions.

The last test makes two calls with different inputs. It checks that one call's counts don't leak into the next call. Each call should build its own dictionary.

Try it: Temporarily remove word = word.lower() from the real helper. Save and run. Which assertion fails? Explain why, then restore the line and confirm that the checks pass.

Choose Expectations Independently

This assertion always compares a value with itself:

actual = count_words('dragon dragon')
assert actual == actual

It doesn't establish that the count is right. Calling the same function again to calculate the expected answer would have the same weakness: both calls could make the same mistake.

For a tiny input, work out the expected answer by hand. Keep that expectation in the test so a future change has something independent to disagree with.

Small cases also help us identify what a failure means. Checking the exact count of a word in the entire Shakespeare file would entangle the cleanup rule, the input file and its edition. A four-word input lets us focus on one behavior.

What Passing Tests Tell Us

The checks pass for the cases we've chosen. That gives us evidence about those cases, not proof that the program handles every possible input.

For example, our word-count tests don't establish that all punctuation is removed. That isn't even our rule: strip('.,!?;:') leaves an apostrophe inside a word alone. A test should describe the behavior we want, rather than silently demanding a much bigger program.

The generator checks focus on its deterministic follower mapping. They don't require a particular randomly generated passage. Different valid passages can come from the same correct mapping.

assert is for checking our code while developing and testing it. Python can omit assertions when run in optimized mode. Keep using ordinary commands here, and keep explicit validation such as raise ValueError for rules the program must enforce during normal use.

Your Turn

Predict Which Check Fails

Suppose the counter still lowercases and strips punctuation, but stops skipping words that become empty strings. Which input in test_word_cleanup() would expose that change? What extra key would you expect?

Explain the difference before trying the change. Restore the intended behavior afterward.

Test a Different Program

Return to the museum ticket function you wrote in chapter 7. Its rule was 5 for each visitor older than 11, with everyone else admitted free.

In a local scratch file, keep your real ticket_total() function and write named tests for:

Call the tests explicitly. Work out the expected costs before running them. Then deliberately change the age rule so an eleven-year-old is charged, and demonstrate that a test catches it. Repair the rule afterward.

Compare possible expectations after trying
def test_ticket_boundary():
    assert ticket_total([11]) == 0
    assert ticket_total([12]) == 5


def test_ticket_empty_group():
    assert ticket_total([]) == 0


def test_ticket_independent_calls():
    assert ticket_total([12, 30]) == 10
    assert ticket_total([8]) == 0


test_ticket_boundary()
test_ticket_empty_group()
test_ticket_independent_calls()
print('Ticket checks passed.')

Place these after your actual ticket_total() definition. A version that prints the cost instead of returning it will fail: its caller receives None, rather than the expected number.

Find a Missing Case

Choose one of our helpers. Name a behavior the current checks don't cover, explain why it matters, and add one small input with an independently chosen expectation. If it exposes a bug, use the investigation process before changing the code.

What I've Learned

Explain why a file containing test definitions might check nothing when run. Then describe one deliberate bug your checks caught and one limitation they still have.

You now have repeatable checks beside the actual functions they exercise. Keep them in my_generator.py and count_words_function.py; Modules and several files will separate them into shared code and a test file. You'll still run the checks with ordinary Python and explicit function calls.

Next, we'll use these checked functions with personal text files and save results we can reopen.