When Python encounters a problem it cannot continue past, it stops and prints an error message. That message is not a punishment — it is a precise report telling you exactly what went wrong, on exactly which line, and often why. Learning to read error messages is one of the most valuable debugging skills you can develop. A programmer who reads the error carefully fixes the bug faster than one who stares at the code and guesses.
This article is the starting point for the Debugging section of the knowledge base. It focuses on one skill: reading what Python tells you when it stops. Later articles in this section cover the three types of error in more depth, strategies for finding bugs that do not produce error messages, and how to use print() statements as a debugging tool.
| Level | What it covers | When to read it |
|---|---|---|
| Basic idea | The structure of a Python error message; the four things to read and in which order | Right now. |
| At a deeper level | The most common error types and what each one means; how to read a traceback with multiple frames | Once you have seen a few error messages in your own programs. |
| At the deepest level | How Python produces error messages; what the traceback reveals about the call stack | When you are comfortable reading errors and want to understand what is happening underneath. |
1 The Structure of a Python Error Message
Every Python error message has the same structure. Once you know it, any error — no matter how alarming it looks — can be read in a few seconds.
Here is a complete error message:
File "main.py", line 5, in <module>
total = total + score
NameError: name 'score' is not defined
Read it in this order:
- The last line. This is the error type and the message. It is the most important part. Read it first.
- The line number. The number after
linetells you where Python was when the error occurred. - The code snippet. The line of your code that was running at the moment of the error.
- The file name. Usually you only have one file, but when you have multiple files this tells you which one.
In the example above: the error is a NameError, meaning Python encountered a name it did not recognise. The message says 'score' is not defined. This happened on line 5 of main.py. The line of code was total = total + score. Now you know exactly where to look and what to look for: something is wrong with the variable score on or before line 5.
The word "Traceback" at the top is easy to ignore — do not. It announces that what follows is a record of where Python was when the error happened. The phrase "most recent call last" tells you the order: the first entry in the list is the outermost call (usually your main program), and the last entry is the innermost call (the exact line that crashed).
For a simple program with no functions, there is only one entry in the traceback and the structure is straightforward. When functions are involved, there can be several entries. The rule is always the same: read the last entry first, because that is the line that actually crashed.
The natural instinct is to read the error message from the top. Resist this. The most useful information — the error type, the message, and the exact crashing line — is at the bottom. Start there, understand what went wrong, then work upward through the traceback only if you need to understand how the program got to that point.
Python generates error messages by catching exceptions — objects that represent error conditions — and printing their type, message, and the current state of the call stack. The call stack at the moment of the error is what the traceback shows: each entry is one frame on the stack, from the outermost (the script entry point) to the innermost (the crashing line). The frames are printed in order from oldest to most recent, which is why "most recent call last" means the bottom entry is the crash site.
Understanding this also explains why the line number is sometimes one line before the actual mistake. Python reports the line where it detected the problem, not always the line where the mistake was made. A missing closing parenthesis on line 4 might only be detected on line 5 when Python tries to parse the next statement and finds something unexpected. The line number is where to start looking, not necessarily where to stop.
2 The Three Types of Error
Python errors fall into three categories. Knowing which category you are dealing with tells you where to look and what kind of fix to expect.
| Type | When it happens | What it means | Example |
|---|---|---|---|
| Syntax error | Before the program runs at all | The code is not valid Python — a missing colon, unclosed bracket, or misspelled keyword | Forgetting the : at the end of an if statement |
| Runtime error | While the program is running | Something went wrong during execution — dividing by zero, accessing a missing key, calling a method on the wrong type | Trying to use a variable that was never assigned a value |
| Logic error | The program runs without crashing but produces wrong results | The code is valid Python and runs successfully, but it does not do what you intended | Using < instead of <= in a condition |
Syntax errors and runtime errors both produce error messages. Logic errors produce no error message at all — the program simply gives wrong output. This article focuses on syntax and runtime errors. Logic errors are covered in the Finding Logic Errors article.
The distinction between syntax and runtime errors matters because they require different responses.
A syntax error means the program could not even start. Python reads through your file before running anything, checking that the code is valid Python syntax. If it finds a problem, it stops immediately and reports the location. Nothing in your program has run yet. The fix is always structural: a missing or extra character, a misspelled keyword, an unclosed bracket.
A runtime error means the program started and ran successfully up to a point, then encountered a situation it could not handle. This is more nuanced: some of your code ran correctly before the crash. The line reported in the error is where execution stopped — but the cause might be earlier, where a variable was given a bad value or a list was built incorrectly.
Python's syntax error messages are sometimes off by one line — the error is detected when Python reaches the next line and cannot make sense of what it finds. If you look at the reported line and see nothing wrong, check the line immediately above it. A missing colon, unclosed parenthesis, or unclosed string on the line above is the most common cause.
Python's execution model has two distinct phases. In the parse phase, Python reads the source code and converts it to bytecode — a lower-level set of instructions. Syntax errors are detected here. In the execution phase, Python runs the bytecode. Runtime errors are raised here, when an instruction encounters an invalid state.
This is why a syntax error can appear in a function body that is never called: the entire file is parsed before any of it runs. Conversely, a runtime error inside a function that is never called in a particular run will never appear — the problematic bytecode exists but is never executed. A program can contain code paths with runtime errors that never surface during testing if those paths are never reached.
3 The Most Common Error Types
Python's error types are specific — each name tells you something about what went wrong. You do not need to memorise all of them. You need to recognise the ones you will encounter most often.
| Error type | What it means | Most common cause |
|---|---|---|
SyntaxError | The code is not valid Python syntax | Missing colon after if, for, def, or while; unclosed bracket or string; misspelled keyword |
IndentationError | The indentation is inconsistent or missing | Forgetting to indent after a colon; mixing tabs and spaces; incorrect dedent |
NameError | A variable or function name is used before it is defined | Typo in a variable name; using a variable before assigning it; calling a function that does not exist yet |
TypeError | An operation is applied to a value of the wrong type | Adding a string and an integer; calling a method on None; passing the wrong number of arguments to a function |
ValueError | An operation receives the right type but an invalid value | Passing a non-numeric string to int(); calling list.remove() with a value that is not in the list |
IndexError | A list index is outside the valid range | Accessing list[5] when the list has fewer than 6 items; off-by-one in a loop |
KeyError | A dictionary key does not exist | Reading from a dictionary with a key that was never added; typo in a key string |
ZeroDivisionError | Division or modulo by zero | A denominator variable that was set to zero or never set; user input of zero not validated |
AttributeError | An object does not have the attribute or method being accessed | Calling a list method on a string; calling a string method on an integer; using a variable that contains None as if it were an object |
Here is each error type with a concrete example — the code that causes it, the message Python produces, and the diagnosis.
SyntaxError
if x > 5
print("big")if x > 5
^
SyntaxError: expected ':'
Diagnosis: the colon is missing at the end of the if statement. Python even tells you what it expected.
NameError
print(mesage)
Diagnosis: mesage is a typo — the variable is probably called message. The name has never been assigned.
TypeError
age = input("Enter age: ")
next_year = age + 1Diagnosis: input() always returns a string. Adding the integer 1 to a string is not valid. Fix: age = int(input("Enter age: ")).
IndexError
items = ["a", "b", "c"] print(items[3])
Diagnosis: the list has three items at indexes 0, 1, and 2. Index 3 does not exist.
KeyError
student = {"name": "Alex", "grade": 9}
print(student["age"])Diagnosis: the key "age" was never added to the dictionary. The message tells you exactly which key was missing.
ZeroDivisionError
total = 100 count = 0 average = total / count
Diagnosis: count is 0. Division by zero is undefined. Fix: check that count is not zero before dividing.
AttributeError
def get_name():
print("Alex")
result = get_name()
print(result.upper())Diagnosis: get_name() prints instead of returning, so result is None. None does not have an upper() method. The message 'NoneType' object has no attribute is almost always a sign that a function returned None when it should have returned a value.
When you see AttributeError: 'NoneType' object has no attribute '...', your first question should be: where did None come from? Look at the variable being used on the crashing line and trace backward to where it was assigned. Almost always, it came from a function that forgot to return a value, or from a method that modifies in place and returns None (like list.sort()).
All of these error types are Python exceptions — classes that inherit from a base class called Exception. When Python encounters an error condition, it creates an instance of the appropriate exception class (for example, NameError("name 'score' is not defined")) and raises it. If nothing catches it, Python's default handler prints the traceback and terminates the program.
The exception hierarchy is one reason why error messages are specific. ZeroDivisionError is a subclass of ArithmeticError, which is a subclass of Exception. Python raises the most specific applicable class, which is why the message says ZeroDivisionError rather than just ArithmeticError. More specific messages give you more information.
In later courses you will learn to catch exceptions deliberately using try/except — this allows you to handle error conditions in your program rather than letting them crash it. For Grade 9, the focus is on reading exceptions rather than catching them.
4 Reading a Multi-Frame Traceback
When a program has functions, a crash can happen deep inside a chain of function calls. The traceback shows every step of that chain — every function that was active when the crash occurred — so you can see how the program got there.
The reading rule is still the same: start at the bottom. The bottom entry is the line that actually crashed. The entries above it show the path that led there.
Here is a program with three functions:
def calculate_average(scores):
return sum(scores) / len(scores)
def process(data):
result = calculate_average(data)
return result
def run():
numbers = []
print(process(numbers))
run()Running this produces:
File "main.py", line 11, in <module>
run()
File "main.py", line 9, in run
print(process(numbers))
File "main.py", line 5, in process
result = calculate_average(data)
File "main.py", line 2, in calculate_average
return sum(scores) / len(scores)
ZeroDivisionError: division by zero
Reading from the bottom:
- The error:
ZeroDivisionError: division by zero. Division by zero happened. - The crash site: line 2, inside
calculate_average, on the linereturn sum(scores) / len(scores). The list must be empty, makinglen(scores)equal to zero. - How we got there: reading upward —
calculate_averagewas called from line 5 insideprocess, which was called from line 9 insiderun, which was called from line 11 in the main program.
The diagnosis: numbers was created as an empty list on line 8 and passed into process and then into calculate_average. Dividing by len([]) — which is 0 — caused the crash. The fix is to check that the list is not empty before calculating the average.
The bottom of the traceback shows where Python stopped. The cause of the problem is often higher up — where a bad value was created or a wrong decision was made. In this example, the crash is in calculate_average but the root cause is in run, where an empty list was passed in without checking. Read the crash site to understand what went wrong. Read upward to understand why.
Each entry in the traceback corresponds to one frame on the call stack at the moment of the crash. The traceback is a snapshot of the stack printed from bottom to top in terms of depth — the outermost frame first (line 11 in <module>), the innermost last (line 2 in calculate_average).
The <module> label in the first frame means the code at module level — the lines of your script that are not inside any function. This is the entry point of the program.
Professional debugging tools like pdb (Python's built-in debugger) and VS Code's graphical debugger allow you to inspect the call stack interactively at the moment of a crash — not just read a printed snapshot. You can move between frames, inspect variable values in each frame, and step forward or backward through execution. These tools make the same information that the traceback summarises available in an explorable form.
5 A Process for Responding to an Error Message
When you see an error message, do not immediately change any code. Read the message first. The order matters: read, diagnose, fix — not fix, then read.
- Read the last line. What is the error type? What does the message say?
- Find the line number. Go to that line in your code.
- Read the code snippet shown in the traceback. Does it match what you expect that line to say?
- Diagnose before touching anything. Write one sentence describing what you think the problem is. "I think this is failing because
scorewas never assigned a value." If you cannot write that sentence, you do not understand the error yet — go back to step 1. - Make one change to address your diagnosis. Run the program again.
- If the same error appears, your diagnosis was wrong. Read the message again — it may have changed slightly, which is new information.
Step 4 — writing a diagnosis before touching code — is the step most beginners skip. It feels slower. It is actually faster, because it prevents the most common debugging trap: making random changes until something stops crashing.
Random changes are dangerous for two reasons. First, they can mask the original error without fixing it — the program stops crashing for a different reason, and the underlying bug is still there. Second, they introduce new code that you did not fully understand, which can cause new bugs.
A written diagnosis forces you to commit to a theory. If your fix works, your theory was right and you have learned something. If it does not, you know your theory was wrong and you can form a different one. Either way you are learning.
When you ask a classmate, a teacher, or an AI for help with an error, copy the complete error message — all of it, including the traceback. "I got a NameError" tells someone very little. The full message, including the file, line number, code snippet, and error text, gives them everything they need to understand the problem. The same applies when asking in Mode 2: paste the full error, describe what you already tried, and describe what you think the problem might be. That context is what separates a useful AI response from a generic one.
The process above is a simplified version of what experienced programmers call the scientific method of debugging, described in detail by David Agans in his book Debugging. The steps are: understand the system, make the bug reproducible, see it fail, quit thinking and look, divide and narrow the problem, change one thing at a time, keep a record. The common thread is that debugging is an empirical process — you form hypotheses and test them — not a process of intuition and random edits.
For beginners, "quit thinking and look" is particularly important. The error message is evidence. Your mental model of what the program should be doing is a hypothesis. When the evidence and the hypothesis conflict, trust the evidence. The program does what you wrote, not what you meant.
6 What Python Cannot Tell You
Error messages are powerful, but they have limits. Python can only tell you where execution stopped and what kind of problem it encountered. It cannot tell you why you made the mistake, whether your logic is correct, or whether your program does what you intended.
A program with no error messages can still be completely wrong. If your loop runs one time too few, or your condition uses < instead of <=, or your function returns the wrong value in one specific case — Python will not report any of these. They are logic errors, and they are invisible to the error message system.
Error messages tell you about crashes. Logic errors require a different set of tools: trace tables, test cases, and careful reading. Those are covered in other articles in this section.
There is also a class of errors that Python reports on the wrong line. Consider:
result = (
first_value +
second_value
third_value
)third_value
SyntaxError: invalid syntax
Python reports the error on line 4, where third_value appears. But the actual mistake is on line 3: a missing + between second_value and third_value. The reported line is where Python noticed the problem, not where the problem began. When a syntax error seems to point at a line that looks completely fine, check the line above it.
- A Python error message always ends with the same two-part structure. What are the two parts, and which one should you read first?
- Name the three types of error in Python. For each one, describe when it is detected and give one example of what causes it.
Read this error message and answer the questions below.
Traceback (most recent call last):
File "grades.py", line 12, in <module>
print(average(scores))
File "grades.py", line 4, in average
return total / count
TypeError: unsupported operand type(s) for /: 'str' and 'int'- What type of error is this?
- On which line did the program crash?
- What was the program trying to do on that line?
- What does the error message tell you about the values involved?
- Write a one-sentence diagnosis: what do you think the cause is?
- A student gets a
NameError: name 'Score' is not definedbut insists they definitely definedscoreearlier in the program. What is the most likely cause, and what should they check? - You see this message:
AttributeError: 'NoneType' object has no attribute 'append'. Without seeing the code, describe the two most likely causes and how you would investigate each one. - Explain in your own words why you should write a diagnosis before making any change to your code. What goes wrong when you skip this step?
- A student's program runs without any error messages but prints the wrong answer. Which type of error is this? Can Python's error message system help find it? What should the student do instead?