Three kinds of wrong, and only two of them tell you#
Every bug in a first Python course is one of three kinds, and knowing which one you are looking at decides what you do next:
| Kind | When it bites | What you see |
|---|---|---|
| Syntax error | before a single line runs | one message, and no output at all |
| Runtime error | part-way through | output up to that point, then a traceback |
| Logic error | never | nothing. The program finishes and the answer is wrong |
The third row is the one that costs marks. Python cannot know what you meant, so a program that runs cleanly and prints 56.67 where the answer is 80 looks exactly like a program that works.
Syntax errors point at the wrong line (on purpose)#
Three lines, one missing bracket:
name = "Ada"
print("Hello, " + name
print("Goodbye")
File "hello.py", line 2
print("Hello, " + name
^
SyntaxError: '(' was never closed
The mistake is that line 2 never closes its bracket, and Python says so precisely. What it cannot know is where you meant to close it — a bracket may legally span many lines, so it reads on, and on older Python versions the complaint surfaced against line 3 instead, which sends people hunting in the wrong place.
The habit worth building: when a syntax error names a line that looks fine, check the line above it. An unclosed bracket, quote, or a missing colon on the previous line accounts for most of them.
Two more that arrive constantly:
x = 5
if x = 5:
print("five")
File "cmp.py", line 2
if x = 5:
^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
= assigns a value; == asks whether two values are equal. A condition needs the
question, not the instruction. Modern Python guesses what you meant and says so in the message.
def f():
print("hi")
File "ind.py", line 2
print("hi")
^
IndentationError: expected an indented block after function definition on line 1
In Python, indentation is the block structure — there are no braces to disagree with it. A line
that belongs inside a def, if, or for must be indented past it, and every
line in the same block must be indented the same amount.
The named runtime errors#
These are worth knowing by name, because an exam will show you a broken line and ask which error it raises. Each message below was produced by running the code in the third column:
| Error | The message | What causes it |
|---|---|---|
NameError | name 'total' is not defined | using a variable before assigning it — often a typo in the name |
TypeError | can only concatenate str (not "int") to str | an operation between two types that do not combine that way |
ValueError | invalid literal for int() with base 10: '3.7' | right type, impossible value — int("3.7"), int("abc") |
ZeroDivisionError | division by zero | dividing by a count that turned out to be 0 |
IndexError | list index out of range | a position past the end — a list of 3 has no index 3 |
KeyError | 'b' | a dictionary key that is not there. Use .get() when it might be missing |
AttributeError | 'int' object has no attribute 'upper' | a method called on the wrong type — usually a number you thought was a string |
The distinction is whether the type is acceptable. int("abc") is a ValueError — a string is exactly what int() wants, this one just does not spell a number. int([1, 2]) is a TypeError — a list is not something it converts at all. Right kind of thing, wrong value: ValueError. Wrong kind of thing: TypeError.
Read a traceback from the bottom up#
A traceback looks like a wall of text. It is actually three pieces, and you read it backwards:
def average(values):
return total(values) / len(values)
def total(values):
s = 0
for v in values:
s = s + v
return s
scores = []
print(average(scores))
Traceback (most recent call last):
File "avg.py", line 11, in <module>
print(average(scores))
^^^^^^^^^^^^^^^
File "avg.py", line 2, in average
return total(values) / len(values)
~~~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero
- The last line is what went wrong.
ZeroDivisionError: division by zero. Read it before anything else — everything above is just how you got there. - The lowest
Fileblock is where it went wrong. Line 2, insideaverage. The^marks point at the exact operation: the division. - The blocks above are the trail that led there, oldest first. Line 11 called
average, which is where the empty list came from.
So: an empty list has length 0, and dividing by len(values) divides by zero. The fix is not on the
line that crashed — it is deciding what an average of nothing should be, and saying so before you divide.
That phrase at the top of every traceback is telling you the reading order. The deepest, most immediately relevant frame is at the bottom, next to the error message. Reading top-down sends you to the line that started the program rather than the line that broke.
The bug that raises nothing#
This program runs perfectly and prints a wrong answer:
scores = [90, 80, 70]
total = 0
for i in range(len(scores) - 1):
total = total + scores[i]
print("Average:", total / len(scores))
Average: 56.666666666666664
No traceback, no warning, exit status fine. The average of 90, 80 and 70 is 80. The loop ran over
range(2), so it added 90 and 80 and never reached 70, then divided that by 3.
The - 1 came from a real and reasonable fear — that range(len(scores)) would run
one past the end. It does not. range(3) produces 0, 1, 2, which are exactly the valid positions of a
three-item list. The guard against a bug you did not have created one you did.
What catches a logic error is arithmetic you can do in your head. Three scores, an obvious average of 80, and an answer of 56.67 — that gap is visible in a second, and it is invisible on a list of forty real numbers. Run every program once on data whose answer you already know.
Better still, drop the index entirely when you do not need it:
scores = [90, 80, 70]
total = 0
for s in scores:
total = total + s
print("Average:", total / len(scores))
Average: 80.0
No range, no len, no index — and therefore no off-by-one to get wrong.
How to actually debug#
- Read the message literally, all of it.
'int' object has no attribute 'upper'already tells you the value is anintwhen you expected astr. Most first-course bugs are solved by believing the message instead of skimming it. - Print the thing you are assuming. Not "it should be a number" —
print(type(x), x)on the line before the crash. The answer is usually the assumption, not the logic. - Halve the program. Comment out the second half. Still wrong? The bug is in the first half. Three or four halvings find almost anything, and it beats rereading from the top.
- Trace by hand with a table. One column per variable, one row per pass through the loop. Write the numbers down — doing it in your head is where the mistake hides, and it is the exact skill a trace-the-output exam question tests.
- Explain the line out loud. The sentence usually stops half-way, at the word you were unsure of. That word is the bug.
Where these go wrong#
- Hunting on the line the syntax error names. Check the line above first — unclosed bracket, unclosed quote, missing colon.
- Reading the traceback top-down. The error is the last line; the place is the lowest
Fileblock. - Writing
range(len(xs) - 1)"to be safe."range(len(xs))is already exactly the valid positions. The- 1silently drops the final item. - Confusing ValueError with TypeError. Wrong value of the right type is ValueError; wrong type altogether is TypeError.
- Assuming no traceback means no bug. Logic errors never raise anything. Test on data whose answer you already know.
- Fixing the crashing line. The traceback shows where it surfaced. The cause is often the line that supplied the value — an empty list, a string that should have been converted.
- Deleting code until the error stops. That removes the symptom. Halve, find, understand, then fix.
Test yourself in the free Kestrel Exams app
Topic-selectable practice — offline, no ads, no account.
Practice this topic →Frequently asked questions#
Why does my Python syntax error point at the wrong line?
Because brackets and quotes may legally span several lines, so when one is left open Python keeps reading and only complains when the following line cannot make sense. Modern Python reports '(' was never closed against the opening line, but the habit still applies: when a syntax error names a line that looks fine, check the line above it.
What is the difference between a TypeError and a ValueError?
A TypeError means the kind of thing is wrong — int([1, 2]) cannot work because a list is not something int() converts. A ValueError means the kind of thing is right but the particular value is impossible — int("abc") is a string, exactly what int() takes, it just does not spell a number.
How do you read a Python traceback?
Bottom-up. The last line names the error and the reason. The lowest File block is where it happened, with ^ marks under the exact operation. The blocks above are the chain of calls that led there, oldest first — which is what the header “most recent call last” is telling you.
What are the four named errors a first Python exam asks about?
Syntax errors (the program never runs), NameError (a variable used before it was assigned), TypeError (an operation between types that do not combine) and ValueError (the right type holding an impossible value). ZeroDivisionError, IndexError and KeyError come up nearly as often.
Why does my program run fine but give the wrong answer?
That is a logic error, and Python raises nothing for it because the code is valid — it just does not do what you meant. The classic is range(len(xs) - 1), which silently skips the last item. Catch these by running on data whose answer you already know.
Does range(len(my_list)) go past the end of the list?
No. range(3) produces 0, 1, 2 — exactly the valid positions of a three-item list. Subtracting one “to be safe” drops the final item. If you do not need the index, loop over the list directly with for item in my_list.
Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.
