Errors
"Errors" occur when a line of code is impossible to run, so the program stops and displays a message with information on what went wrong and where.
"Bugs" are the general term for errors or any mistake in your code, like logic errors.
"Exceptions" are Python's formal term for the type of error that was raised, like KeyError or ValueError.
They are part of programming, and happen constantly. Based on the kind of error, there are different methods to identify and fix them:
Kinds of errors:
Syntax errors
The code doesn't follow Python's grammar rules, so it can't read or run the file. These errors must be fixed directly. These are often incorrect punctuation, spacing, or typos.
| Read error message | handle with try/except | Debugging strategies | Debugger tool | Testing | |
|---|---|---|---|---|---|
| Ways to fix syntax errors | Points to what Python couldn't read |
| Kind of syntax error | Happens when | Check for |
|---|---|---|
IndentationError |
Incorrect indentation |
|
SyntaxError |
The code isn't valid Python |
|
TabError |
Tabs and spaces mixed in the same block of indentation |
|
Runtime errors
A runtime error crashes when a line of code is impossible to execute. It is grammatically correct so is able to read the file and start running, until it encounters something it can't do so it stops and gives you a specific error name.
Think about what programming concepts the failing line is using (data type, loop, conditional, etc), and revisit that page on this site to confirm you're applying it correctly.
| Kind of runtime error | Happens when | Check for |
|---|---|---|
AssertionError |
An assert statement's condition was False |
|
AttributeError |
Calling a method or attribute that doesn't exist on that object |
|
FileNotFoundError |
Trying to open a file that doesn't exist at that path |
|
ImportError |
Importing a name that doesn't exist in a module that was found |
|
IndexError |
Looking up an index that doesn't exist — in a list, tuple, or string |
|
KeyError |
Looking up a dict key that doesn't exist |
|
ModuleNotFoundError |
Importing a module that can't be found |
|
NameError |
Using a variable that hasn't been assigned yet |
|
RecursionError |
A function calls itself too many times without ever reaching a base case |
|
TypeError |
Using a value the wrong way for its type, or calling a function with the wrong number of arguments |
|
UnboundLocalError |
A local variable used before it's assigned |
|
ValueError |
The argument is the right type, but not a valid value for what's being done with it |
|
ZeroDivisionError |
Dividing by zero |
|
Logic errors
A logic error is a bug Python doesn't notice, it finishes running but gives you an unexpected result because the reasoning itself was inaccurate.
Think about what programming concepts you are using (data types, loops, conditionals, etc.) and revisit those pages on this site to confirm you're applying them correctly.
| Read tracebacks/errors | handle with try/except | Debugging strategies | Debugger tool | Testing | |
|---|---|---|---|---|---|
| Ways to fix logic errors? | No error message is shown |
No error is raised |
Helps you find exactly where the code's behavior diverges from what you expected |
See what is happening line by line |
State your expected output, so the mistake gets caught automatically next time |
Fixing errors:
Reading a syntax error message
Red text instead of your expected output? Here's how to read it.
File "hello.py", line 1
if True
^
SyntaxError: expected ':'
The code never ran, but Python still points at the problem. Read it from the bottom up:
- The last line names the problem (
SyntaxError: expected ':') — this is usually the most useful part. - The line above it points at the file and line number, with a
^marking roughly where Python gave up.
Fix the issue there, save, and run again. Errors are a normal part of writing code — even experienced programmers see them constantly.
That pointer isn't always exactly where the mistake is — an unclosed bracket or quote, for example, can get reported many lines later, once Python finally runs out of file without finding the closing character. See Isolate the problem for narrowing down a case like that.
Reading a traceback
A runtime error follows the same bottom-up pattern — but since the program actually started running, Python can show a full traceback: don't be intimidated by the wall of text.
Traceback (most recent call last):
File "hello.py", line 2, in <module>
NameError: name 'name' is not defined
The last line and the file and line above it still matter most, same as before.
Longer tracebacks show one File line per function call involved — your code calling a function, which calls another function, and so on. Start at the bottom of the traceback to identify the exception. Then read upward through the stack to understand how your program got there, looking first at the lines in your own code.
Traceback (most recent call last):
File "hello.py", line 5, in <module>
File "hello.py", line 3, in describe
File "/usr/lib/python3.11/random.py", line 449, in choice
IndexError: list index out of range
Debugging strategies
These general techniques help close the gap between what you think the code does and what it's actually doing.
Read it out loud
Read your code line by line, out loud, saying in plain English what each line does and why. This is often called rubber duck debugging: putting each line into words forces you to state assumptions you'd otherwise skim past while reading silently.
if length < 1 and length > 20: # "If length is under 1 and length is over 20..."
print("that length doesn't look right") # "impossible condition, should use `or` instead of `and`!"
Print debugging
print(type(length), length) # confirm what a value actually is, not what you assumed it was
Sprinkle print() calls between the lines you suspect, showing a variable's value (and type(), if you're not sure) at that exact point in the run. This narrows down where your assumption about the code stopped matching reality — especially useful when nothing crashes and you're just staring at a wrong final answer, so there's no traceback pointing anywhere. Delete the print() calls once you've found the problem.
Isolate the problem
Comment out or delete sections of code until you find the smallest version that still shows the problem. Especially useful for syntax errors you can't obviously spot, since the pointer Python gives you isn't always exactly where the mistake is.
Flag as TODO/FIXME
# TODO: handle the case where length_ft is negative
length_ft = 4.5
# FIXME: math incorrect
def to_inches(length_ft):
return length_ft * 10
Not every problem gets fixed the moment you spot it — sometimes you're mid-debugging something else and don't want to lose track of it. Marking a comment TODO creates a reminder for yourself to "come back to this." FIXME is the same idea for something you know is actively broken rather than just unfinished.
Collecting TODO/FIXME comments in each editor
Some editors collect every TODO/FIXME in a project into one scannable list.
Built-in TODO tool window (View → Tool Windows → TODO, or Alt+6) collects every TODO/FIXME in the project into one scannable list.
No built-in aggregator, but an extension like Todo Tree adds one.
No built-in equivalent — it still works as a plain comment, just without an aggregated list.
No built-in equivalent — it still works as a plain comment, just without an aggregated list.
Debugger tool
A debugger is a tool built into most code editors that lets you pause a running program and look around, instead of only seeing what it printed after the fact. Pause your code mid-run to inspect what's happening and inspect variables — instead of only reading print() outputs at the end.
- Set breakpoints. A breakpoint marks a specific line where you want the program to pause while debugging, so you can inspect it. You can set as many as you want — set these before you start running. Click in the margin next to a line number to set one; click the same spot again to remove it — the red dot toggles off.
- Run in debug mode. Look for a "Debug" button instead of the regular Run button. Your program will run normally until it hits the first breakpoint, then pauses there.
-
Use the controls at a breakpoint. Once paused, these controls move you through your code:
Control What it does Use it when Inspect variables Shows the current value of every variable while paused You want to watch exactly when a variable becomes wrong, instead of guessing Step Into Jumps inside the function being called, so you can watch it run line by line You want to see exactly what a function does Step Over Runs the current line, then pauses on the next one, without entering any function it calls You trust the function works and don't need to see inside it Step Out Finishes the current function, then pauses back where it was called from You stepped into a function but have seen enough and want to jump back out Continue/Resume (▶) Runs until the next breakpoint, or finishes if there are none left You're done inspecting the current pause point and want to jump ahead Stop debugging Ends the debug session entirely You're done, instead of stepping or continuing all the way through Where debugging controls are in each editor
Where to find the debugger, and what it calls things, varies by editor.
- Debug button: Bug icon in the main toolbar
- Step controls: Inline in the main toolbar
- Stop button: Same toolbar
- Where output shows: Same Shell panel as a normal run
- Inspecting variables: Always-visible Variables panel
You don't need to set any breakpoints — Thonny's debugger pauses at every step by default, which is great for watching exactly how a program runs the first time.
- Debug button: "Run and Debug" in the sidebar, or the dropdown next to the Run button
- Step controls: A floating toolbar
- Stop button: Red square, same floating toolbar
- Where output shows: Separate "Debug Console" panel
- Inspecting variables: Variables section in the Run and Debug sidebar
- Debug button: Debug menu in the Shell window (turn on before running)
- Step controls: A separate popup window
- Stop button: "Quit" button, same popup window
- Where output shows: Same Shell window as a normal run
- Inspecting variables: Same popup Debug Control window
Most basic of the four.
- Debug button: Bug icon next to the Run button
- Step controls: The bottom Debug tool window
- Stop button: Red square, same tool window
- Where output shows: Same "Debug" tool window
- Inspecting variables: Same tool window, or hover over a variable in the editor
Detect errors with testing
A test is a small script that checks your code's behavior automatically, so the mistake gets caught the moment it's introduced.
def get_length(species, lengths):
return lengths.get(species)
def test_missing_species_returns_none():
lengths = {"ball python": 4.5, "burmese python": 12}
assert get_length("reticulated python", lengths) is None
pytest is the standard tool for this in Python — a function starting with test_ is one check, and inside it assert states what should be true. Running the file reports exactly which checks passed and which failed, the same way python reports which line of your code raised an error.
Tests are especially good at catching logic errors — where the only way to notice something's wrong is comparing the actual output against what you expected. A test does that comparison automatically, instead of relying on you to notice by eye.
They're also useful for runtime errors — a test can exercise an edge case you wouldn't normally hit every time (an empty input, a missing key, a zero divisor), and pytest.raises() even lets you assert that a specific exception should fire, so you catch both "this crashes when it shouldn't" and "this doesn't crash when it should."
Handling errors:
Catch with try/except
try/except lets your program handle runtime errors and then continue without crashing.
try:
[run this block of code first] # only the line(s) that could cause the error
except [error name]: # i.e. KeyError, ValueError, etc
[if the try block caused the specified error, then continue and run this code]
Handle it with try/except
- The failure is genuinely outside your control — a file that might not exist, a network call, user input you can't fully validate ahead of time
- The failure is an expected, normal outcome — not a mistake
- You have real alternative logic to run instead, like a fallback value or a retry — not just silencing the error
Fix the code instead
- You don't know what is causing the error
- It is in your control to fix the error
- Just wanting to make errors stop — often a sign there is a bug
Catch specific exceptions
Catch the exact exception you expect (except ValueError:) instead of a bare except: — a bare except also silently swallows errors you didn't anticipate, including a typo in your own code, and even catches things like a keyboard interrupt (Ctrl+C) that usually shouldn't be caught at all.
try:
length_ft = float(user_input)
except ValueError: # only catches what you actually expect
print("invalid input")
List several exception types in one except to handle them the same way. Separate except blocks work too, if each error type needs different handling — Python checks them top to bottom and runs the first one that matches.
try:
length = float(lengths[species])
except (KeyError, TypeError): # handle these the same way
print("couldn't look up that species")
except ValueError: # separate for different handling
print("length on record isn't a number")
finally
finally is an optional block that always runs after try/except, whether or not an exception happened — used for cleanup that has to happen either way, like closing a file.
try:
length = lengths[species] # attempted first
except KeyError:
print("no length on record") # runs only on a KeyError
finally:
print("lookup attempt finished") # always runs
Optionalelse block that runs if try succeeded
else runs only if try succeeded, and won't trigger the except block if it fails — useful for keeping code that should only run on success out of the try block itself, so a bug in it doesn't get wrongly caught by the same except.
try:
length = lengths[species] # attempted first
except KeyError:
print("no length on record") # runs only on a KeyError
else:
print(f"found it: {length} ft") # runs only if try succeeded
For efficiency, use try/except when success is the common case
| Time | Space | |
|---|---|---|
try succeeds |
O(1) | — |
try fails |
O(1) (larger constant, same class) | — |
A try block that succeeds costs almost nothing — Python doesn't pay for exception handling until an exception is actually raised. When one is raised, unwinding to the matching except has real overhead, more than an if check would. That makes try/except (checking after — sometimes called EAFP, "easier to ask forgiveness than permission") cheap for something expected to usually succeed, like the float() conversion above, and comparatively expensive as a substitute for an if check on something that fails often — checking first (LBYL, "look before you leap") avoids paying for exceptions that are more the rule than the exception.
See Efficiency for why this distinction matters.
Run a try/except example
A case where try/except is the right tool — converting a value that might not be a valid number:
raw_length = "n/a"
print("trying to read the length")
try:
length = float(raw_length)
print(f"length: {length} ft")
except ValueError:
print(f"couldn't read '{raw_length}' as a number")
Raise an exception
Raise triggers an exception yourself, instead of waiting for one to happen naturally — useful for stopping bad input or state before it causes a more confusing error later.
def set_length(length_ft):
if length_ft < 0:
raise ValueError("length can't be negative")
return length_ft
Whoever calls the code with raise can then put it inside a try/except and handle it gracefully:
try:
set_length(-2)
except ValueError as e:
print(e)
Assert a condition
assert raises an AssertionError if a condition is False — the same idea as raise, but meant for checking your own assumptions while you're still writing and testing the code, not for validating things that need to be checked every time the program is run. Catching a wrong assumption immediately, with a traceback pointing at it, is easier to debug than discovering it later as a logic error.
assert [boolean expression] # raises AssertionError if condition is False
assert [boolean expression], [message] # can add an optional message
assert length_ft > 0, "length should be positive"
If length_ft is -1, that line raises AssertionError, with message as the text:
Traceback (most recent call last):
File "lengths.py", line 1, in <module>
AssertionError: length should be positive
Like any other exception, AssertionError can be caught with try/except:
try:
assert length_ft > 0, "length should be positive"
except AssertionError as e:
print(e)