This lesson covers the error kind that produces no message: forming a hypothesis about what a program is doing, breaking complex expressions into temporary variables, correcting a faulty mental model, and knowing when and how to ask for help.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Appendix A — Debugging
§A.3 Semantic errors, pp. 198-200
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 198-200 — the pages these objectives are drawn from
Warm-up
The other two kinds tell you something.
Discussion prompt
A program runs to completion and prints the wrong number. What information does the interpreter give you about what went wrong?
Hint: How much did it say?
Answer:
None at all. Every step was legal, so nothing was reported — the program did exactly what it says, and what it says is not what was meant.
Which means the usual first move is unavailable: there is no message to read and no line number to look at.
The book puts it precisely: the interpreter provides no information about what is wrong, because only you know what the program is supposed to do.
Concept
In some ways, semantic errors are the hardest to debug, because the interpreter provides no information about what is wrong. Only you know what the program is supposed to do.
The first step is to make a connection between the program text and the behaviour you are seeing. You need a hypothesis about what the program is actually doing — and one of the things that makes that hard is that computers run so fast.
Figure (svg): Two columns contrasting what the interpreter supplies for other errors with what it supplies here
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 198-198
Section
Section 1
Concept
The first step is to make a connection between the program text and the behaviour you are seeing. You need a hypothesis about what the program is actually doing.
Each question turns it doesn't work into something specific enough to test — which is what a hypothesis is, and it is what the interpreter would have given you for the other two kinds of error.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 198-198
Picture it
Something missing, something extra, or something different.
Figure (svg): Three diagnostic questions with what each one directs you to check
And the third has its own remedy: read the documentation for the functions you call, and try them out by writing simple test cases and checking the results.
Worked example
The questions convert a complaint into a hypothesis.
# 'the total is wrong'
# question 1: is the adding code running at all?
print('adding', x) # inside the loop
# question 2: is it running more often than it should?
# question 3: does sum do what I think on this input?| Stage | What you have | Note |
|---|---|---|
| the complaint | not testable | the total is wrong |
| a question | narrows it | which section? |
| a print | answers it | a hypothesis, tested |
Start from the observation.
Why: The total is wrong is a symptom, not a hypothesis — nothing about it can be tested.
Pick a question.
Why: Each one names a section of code and something checkable about it: is it running, is it running too often, does it do what you think.
Test it.
Why: A well-placed print answers the question, and the answer either confirms the hypothesis or eliminates it.
Figure (svg): The state of the program after each line of Worked example from it doesn't work to a test, drawn as a ladder with one rung per traced line
A testable claim in place of a complaint. That conversion is what the interpreter does for you with the other two error kinds, and here you have to do it yourself.
Verify: Note why the speed is a problem.
Why: One of the things that makes forming a hypothesis hard is that computers run so fast — the behaviour you want to observe is over before you can see it. A print is how you slow a moment down enough to look at, which is why the technique is so central here.
Prediction
The program ran to completion.
# the program runs, prints a number,
# and the number is wrong| Information | Available? | Note |
|---|---|---|
| a message | none | nothing was illegal |
| a location | none | no exception |
| what you have | the output | and what it should have been |
Predict first
What information does the interpreter provide?
Correct: None — the interpreter provides no information about what is wrong.
Why: Every step was legal, so there was nothing to report. The reason is the one the book gives: only you know what the program is supposed to do, which is information the program does not contain — and it is why this kind of error needs a different approach entirely.
Worked example
The book is pragmatic about this.
# 'You will often wish that you could slow the program
# down to human speed, and with some debuggers you can.
# But the time it takes to insert a few well-placed
# print statements is often short compared to setting
# up the debugger, inserting and removing breakpoints,
# and stepping the program to where the error is.'| Tool | What it costs | Note |
|---|---|---|
| a debugger | more powerful | and more setup |
| a few prints | less powerful | and immediate |
| the choice | by cost | not by sophistication |
Note what a debugger offers.
Why: The ability to slow the program down to human speed, which is exactly what you want.
Note what it costs.
Why: Setting it up, inserting and removing breakpoints, and stepping the program to where the error is occurring.
Note the comparison.
Why: The time to insert a few well-placed print statements is often short by comparison — so the simpler tool wins on total cost rather than on capability.
Figure (svg): Two columns weighing print statements against a debugger
A recommendation based on effort rather than power. The word well-placed is doing work: a few prints in the right place beat many in the wrong one.
Verify: Ask when the debugger wins.
Why: When the program is large, the state is complex, or the same prints would have to be inserted and removed repeatedly — which is precisely when the setup cost is amortised. The book's earlier note mentions pdb for tracking down exceptions, since it lets you examine the state immediately before the error.
Trap
A program gives the wrong answer, so changes are made and it is run again, repeatedly.
Try things until something works
Why: Each run feels like progress, and occasionally something changes.
Without a hypothesis, each run tests nothing — the book names this random walk programming, the attempt to program by writing every possible program and choosing the one that does the right thing.
Form a hypothesis, then test it.
Use the three questions to make one
Why: Something missing, something extra, or something different.
Then place a print that would confirm or eliminate it
Why: So the run produces information either way.
A run that could not have surprised you has told you nothing. The questions exist to produce claims specific enough that a single print settles them.
Discrimination
Three shapes of wrongness.
Sort into buckets
For each symptom, which of the three questions fits?
Faded example
Before any technique.
Fill in the blanks
# you need a hypothesis about what the program
# is actually doing
Why: The first step is to make a connection between the program text and the behaviour you are seeing, which means having a claim specific enough to test. Without one, each run produces no information, which is what the book calls random walk programming.
Explain it to yourself
The book names it explicitly.
Discussion prompt
One of the things that makes forming a hypothesis hard is that computers run so fast. Explain why that matters.
Hint: What would you like to watch?
Answer:
Because the behaviour you want to explain happens between two moments you can see: the input and the output. Everything in between is over before you could observe it.
So the hypothesis has to be about something invisible, which is much harder than describing something you watched. You are reasoning about a process rather than reporting one.
Which is what a print statement fixes — it makes one moment visible at human speed. That is also why a debugger is appealing and why the book still prefers prints: both solve the same problem, and one of them costs less to set up.
Section
Section 2
Concept
One of the problems with using print statements for debugging is that you can end up buried in output. There are two ways to proceed: simplify the output, or simplify the program.
And the observation that makes this more than housekeeping: often the process of finding the minimal test case leads you to the bug.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 197-198
Picture it
Less output, or less program.
Figure (svg): Two columns comparing simplifying the output with simplifying the program
The right-hand column has a side effect the left does not: the process of finding the minimal test case leads you to the bug surprisingly often.
Worked example
The search is itself a diagnosis.
# 'Often the process of finding the minimal test case
# leads you to the bug. If you find that a program works
# in one situation but not in another, that gives you a
# clue about what is going on.'| Stage | What it produces | Note |
|---|---|---|
| a case that fails | and one that works | the difference |
| the difference | is the clue | about what is going on |
| minimising | isolates that difference | by construction |
Note what minimising involves.
Why: Repeatedly removing something and asking whether the failure survives — which produces pairs of nearly identical cases, one working and one not.
Note what those pairs are.
Why: Each one isolates a single difference that turns a working program into a failing one.
Note the consequence.
Why: If you find that a program works in one situation but not in another, that gives you a clue about what is going on — so the search produces evidence as a by-product.
Figure (svg): A flowchart showing minimisation producing a working and a failing case that differ by one thing
A technique whose process is as valuable as its result. That is unusual, and it is why the book recommends minimising even when the output is manageable.
Verify: Connect it to lesson 11c's advice.
Why: Reduce n to the smallest value that manifests the error, and then increase it gradually as you find and correct errors — the same technique for large datasets. Both are bisection, and both work because the boundary between working and failing is where the bug lives.
Prediction
Beyond a smaller test case.
# you reduce a failing input until it is minimal| What you get | Why it helps | Note |
|---|---|---|
| the smaller case | easier to read | the obvious benefit |
| the pairs along the way | work / fail | the clue |
| the difference | points at the bug |
Predict first
What else does the process of minimising a test case produce?
Correct: Clues — if you find that a program works in one situation but not in another, that gives you a clue about what is going on.
Why: Every step of minimising produces two nearly identical cases with different outcomes, and each such pair isolates one difference that matters. That is why the book says the process often leads you to the bug — the result is a by-product of the search rather than the point of it.
Worked example
A change that should not matter, and does.
# 'Similarly, rewriting a piece of code can help you
# find subtle bugs. If you make a change that you think
# shouldn't affect the program, and it does, that can
# tip you off.'| Observation | What it implies | Note |
|---|---|---|
| a neutral change | should change nothing | by your understanding |
| it changes something | your understanding is wrong | somewhere |
| the tip-off | where to look |
Make a change you believe is neutral.
Why: Reorganising, renaming, splitting a function — anything that should leave the behaviour identical.
Watch for a difference.
Why: If the behaviour changes, one of your beliefs about the code is wrong, and the change you made points at which.
Note why this works.
Why: It tests your model rather than the program, which is exactly what a semantic error requires.
Figure (svg): The state of the program after each line of Worked example rewriting as a diagnostic, drawn as a ladder with one rung per traced line
A change that is informative whether or not it helps. This is the same logic as the next idea's mental-model point, applied as a technique.
Verify: Ask what it means when nothing changes.
Why: That your understanding of that part was right — which is a small confirmation rather than nothing. Eliminating a correct belief is progress in a search where the difficulty is that you cannot tell which of your beliefs is wrong.
Trap
Prints accumulate through a debugging session until the output is thousands of lines.
Keep everything, in case it is needed
Why: Removing one might mean adding it back.
You end up buried in output, in which the interesting line is invisible — so the technique stops working through sheer volume rather than through any fault in it.
Prune as you go.
Remove or comment out print statements that aren't helping
Why: Which is the book's first suggestion.
Or combine them, or format the output so it is easier to understand
Why: Which keeps the information and reduces the volume.
This is lesson 11c's problem in another form: a technique that works perfectly at small scale and fails at large one, where the fix is to reduce the scale rather than to abandon the technique.
Sorting
Two directions, and the book distinguishes them.
Sort into buckets
For each action, which kind of simplification is it?
Faded example
The simplest input that still fails.
Fill in the blanks
# give it the simplest input that causes the problem
Why: Minimising makes the output readable and produces clues along the way — every time something has to stay for the failure to survive, you have learned that it matters. That is why the book says the process of finding the minimal test case often leads you to the bug.
Real world
Isolating a fault by removing things.
Discussion prompt
Think of a fault someone found by disconnecting things one at a time. What did the process tell them beyond which part was broken?
Hint: What happened when they put something back?
Answer:
Which combinations matter. Removing one thing and finding the fault persists is different from removing it and finding the fault gone — and both are informative.
So the search produces a map as well as an answer: by the end, you know which parts are involved and which are irrelevant, which is more than the location alone.
Which is why the book says the process often leads you to the bug. The minimal case is the destination, and the pairs of working and failing versions along the way are the evidence.
Section
Section 3
Concept
Writing complex expressions is fine as long as they are readable, but they can be hard to debug. It is often a good idea to break a complex expression into a series of assignments to temporary variables.
# one expression
self.hands[i].addCard(self.hands[self.findNeighbor(i)].popCard())
# three assignments
neighbor = self.findNeighbor(i)
pickedCard = self.hands[neighbor].popCard()
self.hands[i].addCard(pickedCard)| Version | What you can check | Note |
|---|---|---|
| the one-liner | nothing to inspect | no intermediate values |
| the three lines | two named intermediates | each printable |
| the names | additional documentation | neighbor, pickedCard |
The explicit version is easier to read because the variable names provide additional documentation, and it is easier to debug because you can check the types of the intermediate variables and display their values.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 199-199
Picture it
Readability and inspectability, separately.
Figure (svg): Two columns comparing a nested expression with the same computation split into steps
Which is the same argument as lesson 19a's about comprehensions: a form with nowhere to put a print is a form you cannot inspect.
Worked example
An expression that means something other than it looks like.
# translating x / (2 pi) into Python:
y = x / 2 * math.pi
# multiplication and division have the same precedence
# and are evaluated from left to right,
# so this computes x pi / 2
y = x / (2 * math.pi) # correct| Expression | What it computes | Note |
|---|---|---|
| / and * | the same precedence | left to right |
| x / 2 * pi | (x / 2) * pi | not what was meant |
| parentheses | make it explicit | and correct |
Note the rule.
Why: Multiplication and division have the same precedence and are evaluated from left to right.
Note what that produces.
Why: The expression computes x times pi over 2 rather than x over 2 pi — a wrong answer with no error.
Note the fix and its second benefit.
Why: A good way to debug expressions is to add parentheses to make the order of evaluation explicit. Not only will the program be correct, it will also be more readable for other people who haven't memorised the order of operations.
Figure (svg): A panel showing a precedence bug producing a wrong result with no error
A one-character-class difference that changes the meaning entirely. Whenever you are not sure of the order of evaluation, use parentheses.
Verify: Check the two forms on a value you can compute.
Why: With x = 2, the wrong version gives about 3.14 and the right one about 0.318 — an order of magnitude apart, which is the kind of discrepancy that gets noticed. A subtler precedence bug can differ by a small amount and survive casual checking, which is why parentheses are worth adding pre-emptively.
Prediction
Division and multiplication, unparenthesised.
y = x / 2 * math.pi| Step | What happens | Note |
|---|---|---|
| / and * | same precedence | left to right |
| first | x / 2 | |
| then | times pi | (x / 2) * pi |
Predict first
What does this expression compute?
Correct: x times pi, divided by 2 — multiplication and division have the same precedence and are evaluated from left to right.
Why: The intended expression, x over 2 pi, needs parentheses: x / (2 * math.pi). This is a semantic error of the purest kind — every step is legal, the arithmetic is performed correctly, and the answer is wrong. A good way to debug expressions is to add parentheses to make the order of evaluation explicit.
Worked example
The same problem, in the one place it is worst.
# no chance to see the value
return self.hands[i].removeMatches()
# now you can display it
count = self.hands[i].removeMatches()
return count| Version | What you can check | Note |
|---|---|---|
| returning directly | the value never has a name | nothing to print |
| assigning first | a named value | printable |
| the extra line | costs nothing | at run time |
Note the problem.
Why: If you have a return statement with a complex expression, you don't have a chance to print the result before returning.
Apply the same fix.
Why: Again, you can use a temporary variable.
Note what it enables.
Why: Now you have the opportunity to display the value of count before returning.
Figure (svg): The state of the program after each line of Worked example a return you cannot inspect, drawn as a ladder with one rung per traced line
One extra line, and the returned value becomes inspectable. The pattern is the same as splitting any expression, applied where the value would otherwise leave the function unseen.
Verify: Ask whether to remove the temporary afterwards.
Why: Not necessarily — the variable name provides additional documentation, which is a benefit independent of debugging. count says what the returned number means, where the expression only says where it came from.
Trap
An expression is written from memory of the operator precedence rules, without parentheses.
Rely on knowing the rules
Why: They are fixed and learnable.
A wrong recollection produces a wrong answer with no error at all — and division and multiplication sharing a precedence is exactly the case people misremember, because the written formula groups them differently.
Add parentheses when you are not sure.
Whenever you are not sure of the order of evaluation, use parentheses
Why: Which is the book's own rule.
And they help the reader too
Why: It will be more readable for other people who haven't memorised the order of operations.
The cost is two characters and the benefit is that the code says what it means rather than relying on a shared memory. That is the same reasoning as naming a temporary variable.
Faded example
The denominator is the whole product.
Fill in the blanks
y = x / **(2 * math.pi)**
Why: Without the parentheses, division and multiplication share a precedence and evaluate left to right, so x / 2 * math.pi computes x pi / 2. Whenever you are not sure of the order of evaluation, use parentheses — and they make the code readable for people who have not memorised the order of operations.
Comparison
Fill the blanks. The computation is identical.
Comparison matrix
| Question | One expression | Temporary variables |
|---|---|---|
| How many lines? | one | one per intermediate step |
| Can you print the middle? | no — the values have no names | yes — each is named |
| What do the names add? | nothing; there are none | additional documentation |
| Can you check the types? | no | yes, on each intermediate |
The book gives both benefits: easier to read because of the names, and easier to debug because of the values.
Explain it
The arithmetic looks right.
Discussion prompt
A classmate translated a formula into Python and the result is wrong by a factor. Suggest what to check first.
Hint: How does the written formula group things?
Answer:
The order of evaluation. A written formula groups things visually — a denominator is everything under the line — and Python groups by precedence, which is not the same.
The classic case is division and multiplication, which share a precedence and evaluate left to right: x / 2 * pi computes x pi / 2 rather than x over 2 pi.
So the fix is parentheses around anything the written form groups, and the general rule is the book's: whenever you are not sure of the order of evaluation, use them. A factor-sized error is exactly the signature.
Section
Section 4
Concept
In order to program, you need a mental model of how programs work. If you write a program that doesn't do what you expect, often the problem is not in the program; it's in your mental model.
# the fix:
# break the program into its components
# (usually the functions and methods)
# and test each component independently
# once you find the discrepancy between your model
# and reality, you can solve the problem| Part | What is true | Note |
|---|---|---|
| the program | does what it says | always |
| your expectation | may be wrong | about what it says |
| testing components | finds the discrepancy |
That is a strong claim and a useful one: it redirects the search from the code to the beliefs about the code, which is where a persistent semantic error usually lives.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 199-199
Picture it
The program and your understanding of it.
Figure (svg): Two columns locating a discrepancy either in the program or in the model of it
And the second is harder to find precisely because you are using the faulty model to search — which is why testing components independently works where re-reading does not.
Worked example
Break it into components and check each.
# you believe: this function returns a sorted list
>>> t = [3, 1, 2]
>>> result = t.sort()
>>> result
None # the belief was wrong| Stage | What happens | Note |
|---|---|---|
| the belief | sort returns the sorted list | plausible |
| the test | one line in the interpreter | immediate |
| the result | None | the model is corrected |
Name the belief.
Why: Something you assume about a function or a method — often about what it returns, or whether it modifies its argument.
Test it in isolation.
Why: Break the program into its components — usually the functions and methods — and test each independently.
Correct the model.
Why: Once you find the discrepancy between your model and reality, you can solve the problem.
Figure (svg): The state of the program after each line of Worked example testing a belief, drawn as a ladder with one rung per traced line
A belief tested in one line, and corrected. This is the sort-returns-None trap from lesson 10b, found by testing rather than by reading.
Verify: Ask why reading would not have found it.
Why: Because the line t.sort() looks exactly right if you believe sort returns a list — the code is consistent with the wrong model, so re-reading confirms it. Only running the piece in isolation puts your belief against reality, which is what the technique is for.
Prediction
The code has been read carefully and looks right.
t = [3, 1, 2]
result = t.sort()
print(result[0])
# TypeError: 'NoneType' object is not subscriptable| Aspect | What is true | Note |
|---|---|---|
| the code | reads as correct | if sort returns a list |
| the belief | sort returns a list | wrong |
| the reality | sort returns None |
Predict first
What is the problem here?
Correct: A belief about what sort returns — often the problem is not in the program; it's in your mental model.
Why: The code is perfectly consistent with the belief that sort returns the sorted list, which is why re-reading confirms it. Only running the piece in isolation — checking what t.sort() actually returns — puts the belief against reality. That is why the book recommends testing components rather than reading them.
Worked example
The advice that prevents the problem.
# 'Of course, you should be building and testing
# components as you develop the program. If you
# encounter a problem, there should be only a small
# amount of new code that is not known to be correct.'| Practice | What it gives you | Note |
|---|---|---|
| incremental development | each piece tested | as it is written |
| when a problem appears | the untested code is small | a bounded search |
| without it | everything is suspect | an unbounded one |
Note the practice.
Why: You should be building and testing components as you develop the program — chapter 4's incremental development, restated as a debugging strategy.
Note what it buys.
Why: If you encounter a problem, there should be only a small amount of new code that is not known to be correct.
Note the alternative.
Why: Writing everything and then testing means the bug could be anywhere, and every belief about every part is unverified.
Figure (svg): A flowchart contrasting a bounded search after incremental testing with an unbounded one
A bounded search instead of an unbounded one. The same advice appears in lesson Aa: if you are building the program incrementally, the error will be in the last line you added.
Verify: Check it against the deliberate-error test's role.
Why: Both narrow the search before it begins — one by confirming you are running the right file, and this one by confirming most of the code is already known good. Debugging is much easier when the space of possible causes is small, and both techniques shrink it.
Trap
A developer reads a section of code repeatedly, confirming each time that it is correct.
Check the code against your understanding
Why: Which is what reading is for.
If the understanding is the faulty part, every reading confirms it — the code is consistent with the wrong model, so reading cannot find the discrepancy. The confidence increases and the bug does not move.
Test the components against reality.
Run each function on a small input and check the result
Why: Which puts the belief against the behaviour.
Especially functions from other modules
Why: Where the book says to read the documentation and try them out with simple test cases.
The distinguishing symptom is confidence: reading that keeps confirming the code is correct, on a program that keeps being wrong, is reading against a faulty model. Testing is the only thing that can contradict it.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
Survives elimination: C
Why: C fails exactly when the model is at fault: the code is consistent with the faulty belief, so every reading confirms it. That is why the recommended technique is testing rather than reading — only running a component can contradict what you believe about it.
Faded example
Break it up and check each piece.
Fill in the blanks
# break the program into its components and
# test each one independently
Why: Testing a component on its own puts your belief about it against what it actually does, which is the only way to find a discrepancy in the model — reading cannot, because the code is consistent with the belief. And building and testing as you go means only a small amount of new code is ever unverified.
Socratic
The code is the same in both cases.
Discussion prompt
Explain why a faulty mental model is harder to find than a faulty line of code.
Hint: What are you using to search?
Answer:
Because you are searching with the faulty model. Every line you read is interpreted through it, so a line that means something other than you think will look correct every time.
A faulty line, by contrast, is wrong relative to a correct understanding — so reading with attention can find it, which is why that technique works at all.
Which is why the remedy is different in kind: not reading harder but testing, because a component run in isolation reports what it actually does rather than what you believe. That is the only source of information the model cannot contaminate.
Section
Section 5
Concept
First, try getting away from the computer for a few minutes. The book lists the symptoms of staying too long — frustration and rage, superstitious beliefs and magical thinking, and random walk programming.
The joke about computers emitting waves that affect the brain is carrying real advice: frustration and magical thinking are recognisable states, and the recommended response to them is to stop rather than to continue.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 200-200
Picture it
Three states that indicate it is time to stop.
Figure (svg): Three symptoms of being stuck too long with what each indicates
And the third is the one to catch earliest: writing every possible program and choosing the one that does the right thing is a state you can be in for a long time without noticing.
Worked example
The book is specific about what to do first.
# before you bring someone else in:
# the program should be as simple as possible
# you should be working on the smallest input
# that causes the error
# you should have print statements in the
# appropriate places, producing comprehensible output
# you should understand the problem well enough
# to describe it concisely| Preparation | Why | Note |
|---|---|---|
| simplify first | the minimal case | which may find it |
| prints in place | and readable | so evidence exists |
| a concise description | the hardest part | and often revealing |
Simplify the program and the input.
Why: As simple as possible, and the smallest input that causes the error — which is the minimising from earlier, and it may resolve the problem before you ask.
Have evidence ready.
Why: Print statements in the appropriate places, and the output they produce should be comprehensible.
Be able to describe it concisely.
Why: Which requires understanding the problem well enough to state it — and stating it is the rubberducking that sometimes answers the question.
Figure (svg): Two columns comparing an unprepared request for help with a prepared one
Four preparations, each of which is useful even if nobody is asked. That is the point: doing them often removes the need.
Verify: Note the overlap with rubberducking.
Why: Understanding the problem well enough to describe it concisely is lesson 13c's technique — if you explain the problem to someone else, you sometimes find the answer before you finish asking the question. Preparing to ask and rubberducking are the same activity, which is why the preparation so often makes the asking unnecessary.
Sorting
Before bringing someone in.
Sort into buckets
For each, is it part of the book's preparation?
Worked example
Three specific things.
# 1. If there is an error message, what is it and
# what part of the program does it indicate?
# 2. What was the last thing you did before this
# error occurred? The last lines you wrote, or
# the new test case that fails?
# 3. What have you tried so far, and what have
# you learned?| Item | What it gives them | Note |
|---|---|---|
| the message | and where | the evidence |
| what changed last | bounds the search | the most useful question |
| what you tried | avoids repetition | and shares the eliminations |
Give them the evidence.
Why: The error message, if there is one, and what part of the program it indicates.
Give them the boundary.
Why: What was the last thing you did before this error occurred — which is the same question lesson 13c listed under ruminating.
Give them your eliminations.
Why: What have you tried so far, and what have you learned — so they do not repeat work you have already done.
Figure (svg): The state of the program after each line of Worked example what to tell them, drawn as a ladder with one rung per traced line
Three items that turn a request into a briefing. The third is the one people skip, and it is what stops the helper starting from nothing.
Verify: Note the closing instruction.
Why: When you find the bug, take a second to think about what you could have done to find it faster — next time you see something similar, you will be able to find it more quickly. That converts one bug into a technique, which is why the appendix ends with it.
Trap
An hour into a bug, with rising frustration, the developer keeps changing things and running the program.
Keep working until it is fixed
Why: Stopping feels like giving up.
The book lists exactly this state and its symptoms: frustration and rage, superstitious beliefs, and random walk programming — the attempt to program by writing every possible program and choosing the one that does the right thing. None of them finds bugs.
Get up and go for a walk.
Then think about it away from the machine
Why: What is it doing, what could cause that, and when did it last work?
And accept that it sometimes takes time
Why: Some of the best places to find bugs are trains, showers, and in bed.
The advice is practical rather than sentimental: the recognisable symptoms mean you have stopped reasoning, and continuing in that state produces changes rather than progress.
Prediction
After frustration has set in.
# symptoms: frustration and rage, superstitious
# beliefs, random walk programming| Stage | What to do | Note |
|---|---|---|
| the symptoms | recognisable from inside | which is the point |
| the recommendation | get up and go for a walk | |
| then | think about it calmly | away from the machine |
Predict first
What does the book recommend?
Correct: Get away from the computer for a few minutes, and think about it when you are calm.
Why: The symptoms are listed because they are recognisable from inside: frustration and rage, superstitious beliefs, and random walk programming. None of them finds bugs, and the book notes that some of the best places to find bugs are trains, showers, and in bed, just before you fall asleep.
Faded example
After the bug is found.
Fill in the blanks
# take a second to think about what you could
# have done to find it faster
Why: Next time you see something similar, you will be able to find the bug more quickly — which converts one debugging session into a technique. That is why the appendix ends with it, and it leads directly into the book's last line: the goal is not just to make the program work, but to learn how to make the program work.
Real world
A problem solved by not working on it.
Discussion prompt
Think of something you solved after stopping — the answer arriving on a walk, in the shower, or the next morning. What had changed?
Hint: Not the problem.
Answer:
Not the problem, and not what you knew about it. What changed is that you stopped following the same path through it, which is what fixation does — the same wrong assumption gets re-applied every time.
The book names the places: trains, showers, and in bed, just before you fall asleep. They have in common that you cannot act, so the only available move is to think differently.
Which makes stepping away a technique rather than a rest. The symptoms it treats — frustration, magical thinking, random walk programming — are all states in which more effort produces less.
Comparison
Fill the blanks. The techniques do not transfer.
Comparison matrix
| Question | Syntax and runtime | Semantic |
|---|---|---|
| What does the interpreter give you? | a message and a location | nothing at all |
| What is the first move? | read the message | form a hypothesis about what it is doing |
| What is the main tool? | the message and the traceback | well-placed print statements |
| Where might the fault be? | in the program | in the program, or in your mental model |
The bottom row is the difference that matters most: only one kind of error can be caused by believing something untrue about correct code.
Pattern
Six steps, and the fifth is the one people skip.
Step 5 is not a concession. The symptoms the book lists are states in which further effort produces changes rather than progress, and recognising them is a skill.
Python documentation — Errors and Exceptions Errors and Exceptions
Check
Compared with the other two kinds.
Check your understanding
Why are semantic errors the hardest to debug?
Answer: A
Why: Every step of the program was legal, so nothing is reported — and what it should have done is information the program does not contain. That is why the first step is to form a hypothesis rather than to read a message.
Check
Two operators of equal precedence.
y = x / 2 * math.pi| Part | What applies | Note |
|---|---|---|
| / and * | the same precedence | |
| evaluation | left to right | |
| the result | (x / 2) * pi |
Check your understanding
What does this compute?
Answer: A
Why: Multiplication and division have the same precedence and are evaluated from left to right, so the division happens first. Writing x / (2 * math.pi) makes the intended grouping explicit — and whenever you are not sure of the order of evaluation, use parentheses.
Check
The code has been read many times.
Check your understanding
You keep reading a section of code and it keeps looking correct, but the program is wrong. What does the book suggest?
Answer: A
Why: If you write a program that doesn't do what you expect, often the problem is not in the program; it's in your mental model — and reading cannot find that, because the code is consistent with the faulty belief. Testing each component independently puts the belief against reality.
Real world
Something that works exactly as built and not as intended.
Discussion prompt
Think of a set of instructions followed precisely with the wrong outcome. Where was the fault?
Hint: Not in the following.
Answer:
In the instructions, or in what whoever wrote them believed about the situation — never in the following, which was exact.
Which is the shape of every semantic error: the program did what it says, and what it says is not what was meant. Nothing in the execution can report that, because the execution was correct.
And the harder version is the book's point about the mental model: when the instructions match a wrong belief, re-reading them confirms them. Only testing against reality can find the discrepancy.
Commit first
Answer, then rate your confidence.
Predict first
You have read a section of code many times and it looks correct every time, but the program still produces the wrong answer. What does the book suggest?
Correct: The problem may be in your mental model — break the program into components and test each independently.
Why: This is the appendix's strongest claim about semantic errors, and it explains a frustrating experience precisely: in order to program, you need a mental model of how programs work, and if you write a program that doesn't do what you expect, often the problem is not in the program, it's in your mental model. The reason re-reading fails is structural rather than a matter of attention — you are reading through the faulty model, so code that means something other than you believe will look correct every single time. A line like result = t.sort() is perfectly consistent with the belief that sort returns the sorted list, and no amount of careful reading contradicts a belief the code agrees with. The only source of information the model cannot contaminate is running a component in isolation and seeing what it actually does. That is why the recommended technique is testing rather than reading, and why the book adds that you should be building and testing components as you develop the program — so that when a problem appears, there is only a small amount of new code that is not known to be correct.
Explain it
A program that runs perfectly and does the wrong thing.
Discussion prompt
A classmate's program produces the wrong answer with no error message and they do not know where to start. Give them the first three moves.
Hint: There is no message, so something has to replace it.
Answer:
Form a hypothesis. Ask which of the three shapes it is: something that should be happening is not, something is happening that should not, or an effect is not what they expected. Each names a section of code and something checkable about it.
Scale it down. Give the program the simplest input that causes the problem — and watch for clues, because a case that works next to one that fails tells you what the difference is.
Then test the components rather than reading them. If the code keeps looking correct, the fault may be in what they believe about it, and only running a piece in isolation can contradict that.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The three questions are what replace the error message you do not have, and each converts a complaint into something testable. Minimising has the unusual property that the process finds bugs as often as the result does. The precedence case is the purest semantic error in the book — legal, arithmetically correct, and wrong — and the fix is two characters. And the mental-model claim is the one worth carrying furthest, because it explains why re-reading correct-looking code can fail indefinitely.
Connect it up
One page, from memory.
Draw it
Write the three diagnostic questions and, beside each, what it directs you to check. Underneath, write the nested expression and its split-into-temporaries version, marking what each intermediate name buys. Then write x / 2 * math.pi and what it actually computes. Finally write in one sentence why re-reading cannot find a faulty mental model, and list the three symptoms that mean it is time to step away.
Recap
Three pages, and the appendix is finished: the error kind with no message.
| If you remember one thing | It is this |
|---|---|
| From semantic errors | Only you know what the program is supposed to do. |
| From minimising | The search produces clues, not just a smaller case. |
| From expressions | A value with no name is a value you cannot inspect. |
| From precedence | Division and multiplication are equal, and go left to right. |
| From the mental model | Reading confirms a faulty belief; only testing contradicts it. |
The last two lessons cover appendix B: what it means for one algorithm to be faster than another, the order of growth of the operations you have been using all along, and how a hashtable makes dictionary lookup take about the same time however large the dictionary gets.
Think Python, 2nd edition — Allen B. Downey §A.3, pp. 198-200 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.