When something goes wrong while a Python program is running — division by zero, a missing file, a number that didn’t parse — Python raises an exception. If the exception isn’t handled, the program stops and prints a traceback.
A first exception
result: float = 10 / 0
print(result)
Traceback (most recent call last):
File "main.py", line 1, in <module>
result: float = 10 / 0
~~~^~~
ZeroDivisionError: division by zero
A few things to notice:
- The exception has a type (
ZeroDivisionError). - It has a message (“division by zero”).
- The program stops before reaching
print(result). - The traceback shows the file and line where the error happened.
Common built-in exceptions
You’ll meet these often:
| Exception | When it happens |
|---|---|
ZeroDivisionError | Dividing by zero |
ValueError | A function got the right type but a bad value (int("hello")) |
TypeError | An operation on the wrong type ("hi" + 5) |
KeyError | Accessing a dict key that doesn’t exist |
IndexError | Accessing a list position that doesn’t exist |
AttributeError | Accessing a method or attribute that doesn’t exist |
FileNotFoundError | Opening a file that isn’t there |
NameError | Using a variable that hasn’t been defined |
ImportError / ModuleNotFoundError | Importing something that doesn’t exist |
You don’t have to memorise all of them — Python tells you which one when it happens.
Reading a traceback
The most important part of a traceback is usually at the bottom:
Traceback (most recent call last):
File "main.py", line 4, in <module>
process_data(data)
File "main.py", line 2, in process_data
return data["count"] / data["total"]
KeyError: 'total'
Read from the bottom up:
- What went wrong —
KeyError: 'total'(the dict doesn’t have a key called'total'). - Where — line 2 inside
process_data. - Who called that — line 4 in
<module>(the top-level script).
The chain of File "...", line N entries is called the call stack — it shows how the program got to the failing line.
Exceptions are objects
An exception is a regular Python object — a class. ZeroDivisionError and friends are classes; when one is raised, an instance is created and Python “throws” it up the call stack until something catches it.
e = ValueError("bad value")
print(type(e)) # <class 'ValueError'>
print(e.args) # ('bad value',)
print(str(e)) # bad value
This object-oriented design is why you can catch exceptions, inspect them, and even create your own (later lessons).
Errors at compile time vs runtime
We saw in Section 1 that Python parses your file before running it. Some errors are caught at parse time:
- Syntax errors — invalid code, like missing colons or brackets.
- Indentation errors — wrong indentation.
These are different. They aren’t exceptions — they’re caught before the program ever starts. You can’t “handle” them at runtime; you have to fix the code.
Exceptions are for things that go wrong during execution, when the code itself is valid but the world isn’t cooperating.
Why handle exceptions instead of letting them crash?
Sometimes letting the program stop is the right choice. A script that fails to load its config probably should exit.
But often you want to:
- Recover — show the user a friendly message and continue
- Retry — try again, perhaps with different input
- Log — record what failed without losing the rest of the work
- Clean up — close files, release resources, save partial results
That’s what try/except is for, and that’s the next lesson.
What’s next
You know what an exception is. Next — how to catch one with try/except.