Your Python program crashes. A file is missing, a list index is out of bounds, a network request times out. Every serious program encounters these situations. The question isn’t whether errors happen — it’s whether your code handles them gracefully. Python’s exception handling is one of the cleanest in any programming language. Here’s everything you need to know.
In this guide you will learn:
- The difference between syntax errors and runtime exceptions
- How to use try/except to catch and handle exceptions
- The else and finally clauses and when to use them
- How to raise your own custom exceptions
- Real output from running each code example
Page Contents
Syntax Errors vs Exceptions
Python has two categories of errors. Syntax errors (parsing errors) are problems with the program’s structure — Python can’t even parse the code. Exceptions are runtime errors that occur during execution — the code is valid Python, but something went wrong when it ran.
x = [1, 2, 3] print(x[10]) # IndexError: list index out of range
The try/except Block
The basic pattern: try to run some code, and if a specific exception occurs, handle it.
# Catch a ZeroDivisionError
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Caught: {type(e).__name__}: {e}")
Output
Caught: ZeroDivisionError: division by zero
# Catch a KeyError from a dictionary
try:
d = {'a': 1}
print(d['nonexistent'])
except KeyError as e:
print(f"Caught: {type(e).__name__}: {e}")
Output
Caught: KeyError: 'nonexistent'
# Catch a FileNotFoundError
try:
with open('/nonexistent/file.txt') as f:
content = f.read()
except FileNotFoundError as e:
print(f"Caught: {type(e).__name__}: {e}")
Output
Caught: FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent/file.txt'
Catching Multiple Exceptions
def get_item(data, key, index):
try:
value = data[key]
return value[index]
except (KeyError, IndexError, TypeError) as e:
print(f"Error accessing data: {type(e).__name__}: {e}")
return None
# Test with different errors
print(get_item({'a': [1, 2, 3]}, 'a', 5)) # IndexError
print(get_item({}, 'b', 0)) # KeyError
print(get_item('not a dict', 'key', 0)) # TypeError
Output
Error accessing data: IndexError: list index out of range Error accessing data: KeyError: 'b' Error accessing data: TypeError: string indices must be integers
The else Clause
The else block runs only if no exception was raised in the try block. The key difference from simply placing code after the try/except is that else runs only on success — if an exception is caught and handled, the else block is skipped entirely. This keeps success logic cleanly separated from error handling.
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero")
else:
print(f"Division succeeded: {result}")
return result
finally:
print("Cleanup done")
print(divide(10, 2))
Output
Division succeeded: 5.0 Cleanup done 5.0
The finally Clause
The finally block always executes — ideal for cleanup:
def read_config(filename):
f = None
try:
f = open(filename, 'r')
return f.read()
except FileNotFoundError:
print(f"Config file '{filename}' not found")
return {}
finally:
if f:
f.close()
print("File handle closed")
result = read_config('/etc/app/config.json')
print(f"Result: {result}")
Output
Config file '/etc/app/config.json' not found
File handle closed
Result: {}
try/except/else/finally — Full Pattern
import json
def parse_json_safe(json_string):
try:
data = json.loads(json_string)
except json.JSONDecodeError as e:
print(f"JSON decode error: {e}")
return None
else:
print("JSON parsed successfully")
return data
finally:
print("parse_json_safe() finished")
print("=== Valid JSON ===")
parse_json_safe('{"name": "Alice", "age": 30}')
print("n=== Invalid JSON ===")
parse_json_safe('not valid json at all')
Output
=== Valid JSON === JSON parsed successfully parse_json_safe() finished === Invalid JSON === JSON decode error: Expecting value: line 1 column 1 (char 0) parse_json_safe() finished
Raising Custom Exceptions
For domain-specific errors, define custom exception classes that carry meaningful context. This makes error handling more expressive and helps debugging in larger applications.
class InvalidAgeError(Exception):
def __init__(self, age, message=None):
self.age = age
self.message = message or f"Age cannot be negative: {age}"
super().__init__(self.message)
def set_age(age):
if age 150:
raise InvalidAgeError(age, f"Age {age} is unreasonably high")
return age
try:
set_age(-5)
except InvalidAgeError as e:
print(f"Caught custom exception: {e}")
print(f"Invalid age value: {e.age}")
try:
set_age(200)
except InvalidAgeError as e:
print(f"Caught custom exception: {e}")
print(f"Invalid age value: {e.age}")
Output
Caught custom exception: Age -5 is negative Invalid age value: -5 Caught custom exception: Age 200 is unreasonably high Invalid age value: 200
Common Built-in Exceptions
| Exception | When It’s Raised |
|---|---|
| ZeroDivisionError | Division or modulo by zero |
| IndexError | Sequence index out of range |
| KeyError | Dictionary key not found |
| FileNotFoundError | File does not exist |
| TypeError | Operation on incompatible types |
| ValueError | Argument has wrong value (correct type, wrong value) |
| AttributeError | Object has no attribute |
| NameError | Variable or function name not defined |
| ImportError | Module import failed |
Best Practices
- Be specific — catch the exact exception type, not a bare
except:. Bare except catches everything includingKeyboardInterruptand SystemExit, which masks bugs. - Keep try blocks small — only wrap the code that might raise. A large try block makes it hard to know which line caused the exception.
- Don’t suppress errors silently — at minimum, log the error or re-raise it. Silently swallowing exceptions makes debugging painful.
- Use finally for cleanup — close files, release locks, close database connections. The finally block runs even if an exception is raised.
- Use else for success-only code — the else block runs only when no exception occurred, keeping success logic separate from error handling.
- Define custom exceptions — for domain-specific errors, create custom exception classes that carry meaningful context.
Summary
Python’s exception handling is expressive and clean. The try/except/else/finally pattern covers every scenario. Custom exceptions let you define domain-specific errors that carry meaningful context. Python’s official exception documentation lists all built-in types. Mastering exception handling is the difference between scripts that crash and programs that fail gracefully.
If you’re new to Python, start with Introduction to Python Programming to build a solid foundation. For deeper context on writing robust Python code, see Python Debugging Techniques to learn how to trace and fix exceptions in real programs.

