Python Basics — Exceptions Overview
Exceptions Overview
When Python encounters a problem while running your code, it raises an exception. Understanding exceptions — what they are, how to read them, and the common types — is the first step toward writing robust programs.
Syntax Errors vs Exceptions
These are two fundamentally different categories of errors:
Syntax Errors (Parsing Errors)
Caught before your code runs, when Python can’t understand the structure:
# ❌ Missing colon
if True
print("hello")
# SyntaxError: expected ':'
# ❌ Unmatched parenthesis
print("hello"
# SyntaxError: unexpected EOF while parsing
# ❌ Invalid assignment
5 = x
# SyntaxError: cannot assign to literal here
Exceptions (Runtime Errors)
Caught while your code is running, when an operation fails:
# ❌ These are syntactically valid but fail at runtime
print(10 / 0) # ZeroDivisionError
int("abc") # ValueError
[1, 2, 3][10] # IndexError
Key difference: Syntax errors mean Python can’t even start running your code. Exceptions occur during execution of syntactically correct code.
Common Built-in Exceptions
| Exception | Cause | Example |
|---|---|---|
TypeError |
Wrong type in an operation | "3" + 5 |
ValueError |
Correct type, invalid value | int("abc") |
KeyError |
Missing dictionary key | {"a": 1}["b"] |
IndexError |
List index out of range | [1, 2][5] |
NameError |
Undefined variable | print(x) |
AttributeError |
Missing attribute/method | "hi".push("!") |
FileNotFoundError |
File doesn’t exist | open("nope.txt") |
ZeroDivisionError |
Division by zero | 10 / 0 |
ImportError |
Module can’t be imported | import fake_module |
StopIteration |
Iterator exhausted | next(iter([])) |
OverflowError |
Number too large | math.exp(1000) |
RecursionError |
Maximum recursion depth | Infinite recursion |
PermissionError |
No file access rights | open("/etc/shadow") |
OSError |
OS-level error | Disk full, network error |
Exception Hierarchy
Python exceptions form a class hierarchy. This is important because catching a parent class also catches all its children:
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── FileExistsError
├── TypeError
├── ValueError
│ └── UnicodeError
├── AttributeError
├── NameError
├── ImportError
│ └── ModuleNotFoundError
├── RuntimeError
│ └── RecursionError
└── StopIteration
Why it matters:
# Catching `LookupError` catches both IndexError and KeyError
try:
data = [1, 2, 3]
print(data[10])
except LookupError as e:
print(f"Lookup failed: {e}")
# Lookup failed: list index out of range
Reading Tracebacks
When an exception occurs, Python prints a traceback — a report showing exactly where and why the error happened:
def divide(a, b):
return a / b
def calculate():
result = divide(10, 0)
return result
calculate()
Traceback:
Traceback (most recent call last):
File "app.py", line 7, in <module>
calculate()
File "app.py", line 5, in calculate
result = divide(10, 0)
File "app.py", line 2, in divide
return a / b
~~^~~
ZeroDivisionError: division by zero
How to Read a Traceback
- Read from bottom to top — the last line shows the actual error
- Error type and message:
ZeroDivisionError: division by zero - The arrow (
~~^~~) points to exactly where the error occurred (Python 3.11+) - Call stack: Shows the chain of function calls that led to the error
- File and line numbers:
File "app.py", line 2tells you exactly where to look
Multi-Level Traceback Example
def read_config(filename):
with open(filename) as f:
return f.read()
def init_app():
config = read_config("missing_config.json")
return config
init_app()
Traceback (most recent call last):
File "app.py", line 9, in <module>
init_app()
File "app.py", line 6, in init_app
config = read_config("missing_config.json")
File "app.py", line 2, in read_config
with open(filename) as f:
^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'missing_config.json'
Understanding Error Messages
Error messages usually contain enough information to fix the problem:
# "unsupported operand type(s) for +: 'int' and 'str'"
# → You're trying to add an int and a str — convert one
# "list index out of range"
# → Your index exceeds the list length
# "name 'x' is not defined"
# → You used a variable before defining it (or typo)
# "'dict' object has no attribute 'add'"
# → Dicts don't have .add() — did you mean a set?
# "object is not subscriptable"
# → You used [] on something that doesn't support indexing
Python 3.10+ Improved Error Messages
Python 3.10+ gives much better suggestions:
# Python 3.10+
name = "Alice"
print(nmae)
# NameError: name 'nmae' is not defined. Did you mean: 'name'?
d = {"key": "value"}
d["ky"]
# KeyError: 'ky'. Did you mean: 'key'?
Quick Exploration
Try triggering these exceptions yourself in the Python REPL:
# Launch Python REPL and try:
>>> 10 / 0
>>> "hello"[100]
>>> int("not a number")
>>> {"a": 1}["b"]
>>> import nonexistent_module
Summary
- Syntax errors are caught before execution; exceptions happen during execution
- Python has a rich hierarchy of built-in exceptions
- Read tracebacks from bottom to top — the last line is the actual error
- Catching a parent exception class also catches all its children
- Python 3.10+ provides improved error messages with suggestions
- Understanding exception types helps you write targeted error handling
Next up: try/except/else/finally →