Reading a Test -- Two New Words
In Lesson 3, you reviewed a fifteen-line SmartNotes module and caught a bug before running the code. You built trace tables, classified bugs as type errors or logic errors, and saw how PRIMM and Pyright work together. You can read code. You can find what is wrong with it.
The next chapter will ask you to do something different: write a test first, then let AI generate the code that makes it pass. Before you can write a test, you need to recognize what one looks like. That is all this lesson does: it adds two vocabulary words to your reading toolkit.
Think of it the way a medical student learns anatomy. Before performing surgery, you learn to point at a bone and name it. You do not need to understand the entire skeletal system. You need to recognize the part and know what it does. In this lesson, you will point at def and assert and know what they mean. That is enough. The deeper understanding comes in Phase 2, the next section of this course (functions), and Phase 3 (testing).
The Two Words
Here is a test. It looks like the code blocks from Lessons 1-3, with two new pieces at the top and bottom:
def test_greeting():
result: str = "Smart" + "Notes"
assert result == "SmartNotes"
Three lines. Two new words. Everything else you already know. Let us break it down.
Line 1: def test_greeting():
def means "define." It creates a function, which is a named block of code. Think of def test_greeting(): as a label on an envelope: "this envelope contains: a greeting check." The name test_greeting tells you what the check is about, and the (): at the end is part of the label's punctuation. You will learn why it looks that way in Phase 2 when you study functions properly. For now, recognize the pattern: def test_something(): means "here is a check called something."
Line 2: result: str = "Smart" + "Notes"
You know this. String concatenation from Lesson 1, Block 1. The + operator joins two strings end to end. result holds "SmartNotes".
Line 3: assert result == "SmartNotes"
assert means "I insist this is true." The == is a comparison operator: it asks "are these two values equal?" (A single = stores a value; a double == compares two values.) So assert result == "SmartNotes" insists that result equals "SmartNotes". If the answer is yes, the test passes silently: nothing happens, no output, no fanfare. If the answer is no, the test fails loudly: Python stops and tells you exactly what went wrong.
You do not need to understand everything about def right now. Functions are taught properly in Phase 2. For now, just recognize the pattern: def test_something(): means "here is a check called something." The indented lines below it (the lines that start with extra spaces) are the contents of that check. In Python, those leading spaces are called indentation, and they tell Python which lines belong inside the function. Remove them and the code breaks. You will learn the full rules in Phase 2. That is all you need for this lesson.
Two words. def labels the check. assert insists something is true. The code between them (the variables, the types, the arithmetic) is the same Python you have been reading since Lesson 1.
How to Run a Test
Save the test in a file called test_practice.py in your SmartNotes project. Then run:
$ uv run pytest test_practice.py -v
The -v flag means "verbose": it shows the name and result of each test.
What passing looks like:
test_practice.py::test_greeting PASSED
One line. The word PASSED. No drama. That is what a passing test looks like.
What failing looks like (if you changed "SmartNotes" to "Smart Notes" with a space):
test_practice.py::test_greeting FAILED
def test_greeting():
result: str = "Smart" + "Notes"
> assert result == "Smart Notes"
E AssertionError: assert 'SmartNotes' == 'Smart Notes'
The > arrow points to the line that failed. The E line shows what the test expected versus what it got. assert insisted that result equals "Smart Notes", but result was actually "SmartNotes", with no space. The test failed loudly.
That is enough about running tests. You will use uv run pytest throughout the next chapter. For now, the point is: you can check your predictions.
Your First Four Test Predictions
Four tests. Each one uses only vocabulary from Lessons 1-3: variables, types, arithmetic, strings, booleans, and comparisons. The only new parts are def and assert. Your job: predict whether each test passes or fails.
Test 1: String Concatenation
def test_greeting():
result: str = "Smart" + "Notes"
assert result == "SmartNotes"
Pause here. Predict: does this test pass or fail?
Answer: Pass. "Smart" + "Notes" produces "SmartNotes". The assert checks whether result equals "SmartNotes". It does. The test passes silently.
Investigate: This is the same string concatenation from Lesson 1, Block 1. The + joins strings end to end with no space between them. If you predicted it would fail because of a missing space, look again: "SmartNotes" (the expected value) has no space either. The assert matches.
Test 2: Floor Division (Deliberately Buggy)
This test contains a mistake on purpose. Part of reading tests is recognizing when the test itself has a bug.
def test_floor_division():
result: int = 10 // 3
assert result == 4
Pause here. Predict: does this test pass or fail?
Answer: Fail. 10 // 3 equals 3, not 4. Floor division drops the decimal. You learned this in Lesson 1, Block 2. The assert insists result equals 4, but result is 3. The assertion fails.
Investigate: 10 / 3 is 3.333.... Floor division // drops everything after the decimal, giving 3. The person who wrote this test made a mistake in the expected value: they wrote 4 instead of 3. When you run this test, pytest will show:
E assert 3 == 4
That single line tells you everything: the code produced 3, the test expected 4.
Python's // operator always floors toward negative infinity. For positive numbers, this is identical to integer division in C or Java. The distinction matters with negative values: Python gives -7 // 2 = -4, while C-style truncation gives -3.
Test 3: F-String Formatting
def test_price_label():
price: float = 29.99
quantity: int = 3
total: float = price * quantity
label: str = f"Total: ${total}"
assert label == "Total: $89.97"
Pause here. Predict: does this test pass or fail?
Answer: Pass. 29.99 * 3 equals 89.97. The f-string puts that value after Total: $, producing "Total: $89.97". The assert checks for exactly that string. It matches.
Investigate: Three operations from Lesson 1, Block 3, working together. First, float * int produces a float. Second, the f-string evaluates {total} and inserts the value. Third, the $ sign is literal text; it appears in the output as-is. If you predicted failure because of floating-point precision (maybe expecting 89.97000000000001), your instinct was sound (that does happen with some float calculations), but 29.99 * 3 happens to produce exactly 89.97.
Test 4: Boolean Logic
def test_both_conditions():
temperature: int = 25
is_hot: bool = temperature > 30
is_sunny: bool = True
both: bool = is_hot and is_sunny
assert both == True
Pause here. Predict: does this test pass or fail?
Answer: Fail. 25 > 30 is False, so is_hot is False. False and True is False because the and operator requires both sides to be True. The assert insists both equals True, but both is False. The test fails.
Investigate: This is Lesson 1, Block 4 with different values. In that block, the temperature was 35, making is_hot True. Here it is 25, making is_hot False. One number changed, and the entire chain of boolean logic flipped. The assert expected the temperature to be above 30, but it is not. When you run this test, pytest shows:
E assert False == True
What You Just Did
You read four test functions and predicted their outcomes. Two passed. Two failed. The code inside each test (the variables, the types, the arithmetic, the strings, the booleans) used nothing new. Every expression was something you already knew from Lessons 1-3. The only new parts were def (a label for the check) and assert (an insistence that something is true).
You applied PRIMM to test functions the same way you applied it to code blocks. Predict what the assert checks. Evaluate the expression. Decide: pass or fail. The method does not change. The code just has a wrapper around it.
Exercises
Exercise 1: Variable Reassignment in a Test
def test_updated_price():
price: float = 19.99
price = 24.99
discount: float = 5.0
final: float = price - discount
assert final == 14.99
Predict: pass or fail? Remember what you learned about variable reassignment in Lesson 2: when a variable appears on the left of =, it receives a new value and the old value is gone.
Exercise 2: Stale-Value Error
def test_word_count():
title_words: int = 8
body_words: int = 342
total: int = title_words + body_words
body_words = 410
assert total == 418
Predict: pass or fail? Trace the variables line by line. When does total get its value? When does body_words change?
Exercise 3: Boolean Logic in a Test
def test_passing_grade():
score: int = 72
threshold: int = 70
has_bonus: bool = False
is_passing: bool = score >= threshold and has_bonus
assert is_passing == True
Predict: pass or fail? Evaluate score >= threshold first, then combine with has_bonus using and. Remember from Lesson 1: the and operator requires both sides to be True.
Try With AI
Open Claude Code in your SmartNotes project and try these prompts.
Prompt 1: Generate a Passing Test
Generate a simple Python test function (using def and assert)
that tests a calculation using only variables with type
annotations, arithmetic operators, and assert. Use only str,
int, float, or bool types. No imports. Make the test pass.
Do NOT reveal whether it passes -- let me predict first.
Before running uv run pytest test_practice.py -v, read the test and predict: will it pass or fail? Evaluate the assert expression using what you know from Lessons 1-3. Then run it and compare.
What you're learning: You are applying PRIMM to AI-generated test code, the same workflow you will use in the next chapter. The AI writes the test, you read it and predict the outcome, then you verify. This is the prediction loop applied to a new kind of code.
Prompt 2: Generate a Failing Test
Generate a simple Python test function (using def and assert)
that tests a calculation using only variables with type
annotations, arithmetic operators, and assert. Use only str,
int, float, or bool types. No imports. Include a deliberate
mistake in the assert so the test FAILS. Do NOT tell me
which line is wrong -- let me find it.
Read the test. Find the line where the assert expects the wrong value. Predict what the correct value should be. Then run uv run pytest test_practice.py -v and check the error output. Does it match your prediction?
What you're learning: You are combining test reading with bug detection from Lesson 3. The failing assert is like the type bug and logic bug exercises: you find what is wrong by reading carefully, not by waiting for the crash.
Prompt 3: Predict a Multi-Step Test
Generate a Python test function that uses at least 3 variables
with type annotations, performs two or more arithmetic operations,
and ends with a single assert. Use only str, int, float, or bool
types. No imports. Do NOT tell me if it passes or fails.
Read the test. Build a trace table for the variables inside it. Predict: pass or fail? Then run uv run pytest test_practice.py -v and compare.
What you're learning: You are combining trace tables from Lesson 2 with test prediction from this lesson. Multi-step tests require tracking several values before evaluating the assert, which is exactly the skill you need when reviewing AI-generated test code.
PRIMM-AI+ Practice: Reading Tests
Predict [AI-FREE]
Ask Claude Code to generate a test for you to evaluate:
Generate a simple Python test function (using def and assert)
that tests a calculation using only variables with type annotations,
arithmetic operators, and assert. Use only str, int, float, or bool
types. No imports. Do NOT reveal whether it passes. Let me predict first.
Read the test. Evaluate the assert expression. Predict: pass or fail? Write your answer and a confidence score from 1-5 before running.
Run
Run uv run pytest test_practice.py -v. Compare the result to your prediction and record: prediction, confidence, actual result. Did you correctly evaluate the assert expression? If the test passed, was it silent? If it failed, does the E line show the mismatch you predicted?
Investigate
Now ask for a deliberately failing test:
Generate a test function with a deliberate mistake in the assert
so the test FAILS. Do NOT tell me which line is wrong.
Before running, write a "why this fails" note: find the wrong value by reading, not by running. Predict what the correct value should be. This is your trace artifact.
Then run uv run pytest and check: does the error output match your prediction?
Error Taxonomy: Is the bug a logic error (wrong expected value in the assert) or a specification error (the assert checks the wrong thing entirely)? The assert line is the specification that defines "correct." Getting the specification wrong is a different kind of bug than getting the calculation wrong.
Parsons Problem: Reconstruct a Test Function
Here are the lines of a pytest test function in scrambled order. Reconstruct the correct test.
One new detail: the -> None after the parentheses means "this function does not hand back a value." Tests check things; they do not produce a result for other code to use. You will learn return types fully in the next chapter. For now, treat -> None as part of the label's punctuation, like ():.
total: float = price * quantity
assert total == 89.97
quantity: int = 3
def test_calculate_total() -> None:
price: float = 29.99
Arrange these five lines into a valid test function with correct indentation. Then predict: will this test pass or fail? Write your answer and a confidence score.
Hint: Think about what must exist before it can be used. A variable cannot appear in a calculation before it has a value. And remember what def does from earlier in this lesson.
Modify
Take the failing test from the Investigate step and fix the assert value. Before running the fixed version, predict: will it pass now? Run it. Then modify the test further: change one of the variables inside the test and update the assert to match. Predict before running each modification.
Make [Mastery Gate]
Write your own test from scratch. Pick a simple calculation (any arithmetic you have learned in this chapter). Write the def test_...(): wrapper, the variables with type annotations, the calculation, and the assert. Predict whether your own test passes, with a confidence score. Then run it. If it fails, you have a bug in your own specification, and finding it teaches you more about tests than any prompt can.
Writing a test that passes on the first try, with a confidence score of 4 or 5 that matches reality, is your mastery gate for this lesson.
The assert line defines what "correct" means. That is Rung 3 of the Verification Ladder: tests as specification. Without the assert, you are hoping the code works. With the assert, you are verifying it. In the next chapter, you will write the test first and let AI write the code that makes it pass. The skill you just practiced (reading a test and knowing whether it specifies the right behavior) is the skill that makes Test-Driven Generation work.
- Confusing the test with the implementation: The test file calls the function; it does not define the function. If you see
assert double(3) == 6, the test is checking behavior, not creating it. - Ignoring the expected value: In
assert result == 42, the number 42 is the specification. That is the answer the function must produce. Read it as a requirement, not just a number. - Assuming one passing test means the function is correct: One test checks one case. A function that passes
double(3) == 6might still fail ondouble(0)ordouble(-1). Look for what the tests do NOT cover.
James counts on his fingers. "Four tests. Two passed, two failed. And the only new things were def and assert. Everything inside the tests was stuff I already knew."
"That was the point," Emma says. "Two vocabulary words. That is all a test adds to what you can already read."
James leans forward. "Here is what I keep thinking about. When I ran operations, we had checklists for every shipment. 'Does the weight match the manifest? Does the destination match the order?' Those checklists did not create the shipment. They verified it. That is what assert does. It does not create the answer. It checks whether the answer is what you said it should be."
Emma is quiet for a moment. "I had a test suite once that I trusted completely. Two hundred tests, all green. Then a customer reported a bug that none of them caught." She shakes her head. "I had tested the wrong thing. Every assert checked the right type, the right format, the right structure. None of them checked whether the actual number made business sense. I learned that a passing test only proves what you wrote in the assert, nothing more."
"So the assert is only as good as the person who wrote it," James says.
"Exactly. And in the next chapter, that person is you. You write the test first, five lines at most, and AI writes the code that makes it pass. Reading becomes doing."
Chapter-End Rubric: Self-Assessment
Before proceeding to the next chapter, score yourself honestly on five dimensions. This rubric is not a grade; it is a mirror. It shows you where you stand so you can direct your practice to the dimensions that need the most work.
Prediction Accuracy
How often were your predictions correct during this chapter? Could you predict the output of string concatenation, floor division, f-strings, and boolean logic before running the code?
| Developing | Competent | Fluent |
|---|---|---|
Predictions were often wrong; confused strings with numbers, // with /, or boolean and logic | Predictions were mostly correct for straightforward blocks; some errors on floor division or multi-operator expressions | Predictions were consistently accurate across all four block types; could predict output for novel combinations |
Trace Quality
Were your trace tables accurate and complete? Could you track variable values through 5-7 lines of code with reassignment without losing track of updated values?
| Developing | Competent | Fluent |
|---|---|---|
| Trace tables had stale-value errors; used original values instead of updated ones; needed AI to find divergence | Trace tables were mostly correct; occasional stale-value error on longer blocks; caught errors with AI comparison | Trace tables were accurate for all exercises including 7-line blocks; Parsons reconstruction was correct on first attempt |
Explanation Quality
Can you explain why each code block produces its output, not just what the output is? Can you explain in your own words what // does, how f-strings work, and why False and True is False?
| Developing | Competent | Fluent |
|---|---|---|
| Can state outputs but struggles to explain the mechanism; relies on AI for explanations | Can explain most operations in own words; some difficulty articulating the difference between // and / or string vs number + | Can teach each concept with examples; explains edge cases (negative floor division, floating-point precision) without prompting |
Bug-Finding Quality
In the code review lesson (Lesson 3), could you find the type error in the SmartNotes statistics calculator by reading, without running the code?
| Developing | Competent | Fluent |
|---|---|---|
| Could not find the bug before running; needed the crash message or AI hint | Found the bug after building a trace table; the type mismatch became visible during tracing | Spotted the str + int type error on first read; could explain exactly which line would crash and why |
Test-Reading Quality
In the test-reading lesson (Lesson 4), could you predict pass/fail for each test function? Could you reconstruct the Parsons test function correctly?
| Developing | Competent | Fluent |
|---|---|---|
| Predicted pass/fail incorrectly on more than one test; struggled with Parsons reconstruction | Predicted pass/fail correctly on most tests; Parsons reconstruction was correct; occasional error on edge-case tests | Predicted all four tests correctly with high confidence; Parsons reconstruction was immediate; could write own test functions |
If you scored Developing on any dimension, go back and generate new practice exercises with Claude Code: ask for code blocks to predict, longer code to trace, buggy code to review, or tests to evaluate, and work through the PRIMM-AI+ Practice cycle until the skill is solid. If you scored Competent across all five, you are ready for the next chapter, where reading becomes writing. If you scored Fluent, you are reading Python the way professional developers read AI-generated code, and that is exactly the skill this chapter was designed to build.