Errors, Exceptions, and Debugging
Errors are not a side topic in programming; they are how a running program tells you which assumption failed. Halvorsen's textbook distinguishes syntax errors from exceptions, shows examples such as missing quotes and division by zero, and introduces try, except, and finally. It also has a short debugging chapter that frames debugging as the process of finding and resolving defects.
The practical goal is not to prevent every error message. The goal is to make failures local, understandable, and recoverable when recovery is possible. Syntax errors must be fixed before the program runs. Exceptions may indicate invalid input, missing files, unavailable resources, or programmer mistakes. Debugging is the disciplined process of reducing the distance between observed behavior and intended behavior.
Definitions
A syntax error is invalid Python grammar. The interpreter cannot run the program until it is fixed:
print("Hello"
An exception is an error detected while a syntactically valid program runs. Examples include ZeroDivisionError, TypeError, ValueError, FileNotFoundError, KeyError, and IndexError.
A traceback is the stack of calls that led to an exception. Read it from bottom for the exception type and message, then move upward to find which of your lines triggered the failure.
A try block contains code that may raise an exception. An except block handles selected exception types. An else block runs if no exception occurred. A finally block runs whether or not an exception occurred.
try:
value = float(text)
except ValueError:
value = None
else:
print("conversion succeeded")
finally:
print("conversion attempted")
To raise an exception is to signal a failure explicitly:
raise ValueError("window must be positive")
A debugger lets a programmer pause execution, inspect variables, step through code, and resume. Editors such as Spyder, VS Code, PyCharm, and Visual Studio provide integrated debuggers; Python also includes pdb.
Key results
The first key result is that exception types matter. Catching bare except: hides too much, including programming mistakes and keyboard interrupts. Catch the narrow exception you expect:
try:
number = float(text)
except ValueError:
...
The second result is that exceptions should be handled at the level that can do something useful. A low-level parsing function may raise ValueError; a user-interface function may catch it and print a helpful message. Catching too early often loses context.
The third result is that finally is for cleanup, not ordinary success logic. Context managers often make finally unnecessary for files because with handles cleanup directly.
The fourth result is that assert is for internal sanity checks and tests, not user input validation in production code. Python can run with assertions disabled using optimization flags.
The fifth result is that debugging is faster when you can reproduce the failure. Capture the input, the expected behavior, the actual behavior, and the smallest code path that shows the issue. Random changes without a hypothesis waste time.
The sixth result is that logging is often better than scattered print() statements in larger programs. The standard logging module can include timestamps, severity levels, module names, and output destinations.
A seventh result is that an exception message should help the next person act. ValueError("bad") is technically an error, but ValueError("window must be positive") tells the caller what rule was violated. Include the invalid value when it is safe and useful, especially at boundaries such as command-line arguments or input files. Avoid messages that expose secrets such as passwords, tokens, or private data.
An eighth result is that debugging benefits from narrowing. If a script fails after reading a file, transforming data, plotting, and saving output, do not inspect all of it at once. First confirm that the file was read. Then confirm the parsed data shape. Then confirm the transformation on one row. Then confirm the plot input. Each confirmation reduces the search space. This is the same reason tests are valuable: they make assumptions executable.
Finally, not every exception should be recovered from. A command-line tool may catch invalid user input and show a friendly message. A library function should often raise a clear exception and let the caller decide. A program that silently "recovers" from corrupt data may produce a wrong answer, which is worse than stopping with a useful traceback.
Visual
| Exception | Common cause | Better response |
|---|---|---|
ValueError | Text cannot be converted to number | Ask again or reject record |
TypeError | Operation on incompatible type | Fix caller or validate input |
KeyError | Missing dictionary key | Use .get(), defaults, or validate schema |
IndexError | Sequence index out of range | Check length or loop directly |
FileNotFoundError | Missing path | Show path, create file, or fail clearly |
ZeroDivisionError | Divisor is zero | Validate denominator |
Worked example 1: handle invalid numeric input
Problem: ask for a temperature and convert it to Fahrenheit, but do not crash when the user types invalid text.
Initial code:
text = input("Celsius: ")
celsius = float(text)
fahrenheit = celsius * 9 / 5 + 32
print(fahrenheit)
If the user enters abc, float("abc") raises ValueError.
Method:
- Wrap only the risky conversion in
try. - Catch
ValueError. - Keep the successful calculation outside or in the
elseblock. - Print a clear message for invalid input.
Work:
text = input("Celsius: ")
try:
celsius = float(text)
except ValueError:
print(f"{text!r} is not a valid number")
else:
fahrenheit = celsius * 9 / 5 + 32
print(f"{fahrenheit:.1f} F")
Step-by-step for input 20:
float("20")succeeds and returns20.0.- The
exceptblock is skipped. - The
elseblock computes68.0. - Output is
68.0 F.
Step-by-step for input abc:
float("abc")raisesValueError.- The
except ValueErrorblock runs. - The
elseblock does not run. - Output is
'abc' is not a valid number.
Checked answer: valid input calculates; invalid input is reported without a traceback.
Worked example 2: debug an off-by-one error
Problem: a function should return the average of every three consecutive readings, but it misses the final window.
Buggy code:
def moving_average(values):
result = []
for start in range(0, len(values) - 3):
window = values[start:start + 3]
result.append(sum(window) / 3)
return result
For values = [10, 20, 30, 40], expected windows are [10, 20, 30] and [20, 30, 40], so expected averages are [20.0, 30.0].
Method:
- Print or inspect
startvalues. - For length
4,range(0, len(values) - 3)becomesrange(0, 1). - That produces only
0. - The last valid start index is
len(values) - 3, which is1. - Because
rangeexcludes the stop value, the stop must belen(values) - 3 + 1.
Fixed code:
def moving_average(values):
result = []
for start in range(0, len(values) - 3 + 1):
window = values[start:start + 3]
result.append(sum(window) / 3)
return result
Check:
- For length
4, range isrange(0, 2), producing0and1. start = 0gives average(10 + 20 + 30) / 3 = 20.0.start = 1gives average(20 + 30 + 40) / 3 = 30.0.
Checked answer: [20.0, 30.0].
Code
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
def safe_mean(values):
if not values:
raise ValueError("cannot compute mean of empty data")
return sum(values) / len(values)
datasets = [[1, 2, 3], [], [10, 20]]
for data in datasets:
try:
average = safe_mean(data)
except ValueError as error:
logging.warning("skipping dataset %r: %s", data, error)
else:
logging.info("mean of %r is %.2f", data, average)
The snippet uses explicit validation, a narrow exception handler, and logging with severity levels.
The loop is intentionally written so one bad dataset does not stop later datasets from being processed. That is a recovery policy, and it belongs at the level that owns the batch. The safe_mean function itself does not decide to skip anything; it raises a clear exception. This separation keeps the calculation honest and lets different callers choose different policies. A command-line report might skip bad input, while a test or scientific analysis might stop immediately.
Common pitfalls
- Catching every exception with bare
except:and hiding real bugs. - Placing too much code inside
try, making it unclear which operation failed. - Printing an error but continuing with invalid data.
- Using
assertfor user-facing validation. - Ignoring tracebacks instead of reading the bottom exception type and relevant file line.
- Debugging by changing many things at once. Change one hypothesis at a time.
- Swallowing exceptions in libraries that should let callers decide how to handle failure.