Python Basics — Exceptions Overview

4 minute read

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

  1. Read from bottom to top — the last line shows the actual error
  2. Error type and message: ZeroDivisionError: division by zero
  3. The arrow (~~^~~) points to exactly where the error occurred (Python 3.11+)
  4. Call stack: Shows the chain of function calls that led to the error
  5. File and line numbers: File "app.py", line 2 tells 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 →

Updated: