1. What Are Exceptions?
Exceptions are Python’s way of signaling that something went wrong at
runtime.
When an error occurs, Python raises an exception object. If it isn’t caught,
the program terminates with a traceback.
Common built-in exceptions:
SyntaxError, NameError, TypeError, ValueError, KeyError, IndexError,
IOError/OSError, ZeroDivisionError, etc.
2. The Exception Hierarchy
Python’s exceptions form a class hierarchy rooted at BaseException:
BaseException
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
├── ValueError
├── TypeError
└── …and many more…
Catch broadly by catching Exception (but avoid catching BaseException,
which also includes KeyboardInterrupt, SystemExit).
Catch narrowly by catching specific subclasses to avoid hiding bugs.
3. Basic try / except
try:
result = 10 / int(input("Divide 10 by: "))
except ZeroDivisionError:
print("Cannot divide by zero!")
except ValueError:
print("Please enter a valid integer.")
else:
print("Result is", result)
try block: code that may raise.
except <ExceptionType>: handles that type.
You can have multiple except clauses in order from specific to general.
except Exception: catches all standard exceptions.
4. else and finally
try:
f = open("data.txt")
data = f.read()
except OSError as e:
print("File error:", e)
else:
print("File contents:", data)
finally:
# Always runs, even if no exception or after handling one
f.close()
else runs if the try block succeeded (no exception).
finally always runs, even if an exception is uncaught or re-raised—ideal for
cleanup.
5. Catching Multiple Exceptions
try:
x = int(data["value"])
y = 10 / x
except (KeyError, ValueError):
print("Missing or invalid 'value' in data.")
except ZeroDivisionError:
print("'value' must not be zero.")
Use a tuple in except to handle several error types with one handler.
If you need different handling for each, separate them into multiple except.
6. Raising Exceptions
You can raise exceptions yourself using raise:
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"Insufficient funds: {amount} > {balance}")
return balance - amount
raise ExceptionType("message") throws a new exception.
Without arguments, raise re-raises the current exception inside an except
block.
7. Custom Exceptions
Define your own for clearer domain-specific errors:
class CropRecommenderError(Exception):
"""Base exception for our crop recommender system."""
pass
class InvalidSoilData(CropRecommenderError):
"""Raised when soil parameters are out of realistic range."""
pass
def recommend(n, p, k):
if not (0 <= n <= 200):
raise InvalidSoilData(f"Nitrogen {n} out of range")
# … rest of logic …
Inherit from Exception or a subclass.
Give meaningful names and docstrings.
8. Exception Chaining
When catching one exception and wanting to raise another, preserve
context:
try:
user = load_user(uid)
except DatabaseError as db_err:
raise RuntimeError("Failed to load user profile") from db_err
The from keyword links the new exception to the original, so tracebacks show
both.
9. Context Managers & Exceptions
Use the with statement to manage resources safely:
with open("log.txt", "a") as log:
log.write("Starting process\n")
# if an exception occurs, file is still closed properly
You can write your own by implementing __enter__ and __exit__:
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self
def __exit__(self, exc_type, exc, tb):
import time
print("Elapsed:", time.time() - self.start)
with Timer():
do_heavy_work()
In __exit__, returning True suppresses the exception; normally you let it
propagate.
10. Best Practices
Don’t catch “everything.”
Avoid bare except: or catching Exception unless truly necessary.
Handle only what you can recover from.
Let unexpected exceptions bubble up to fail fast.
Clean up resources.
Use with or finally to close files, network connections, locks, etc.
Log meaningful context.
Combine exceptions with logging:
import logging
logger = logging.getLogger(__name__)
try:
except Exception as e:
logger.exception("Processing failed for input %r", data)
raise
Validate inputs early.
Use guard clauses and custom exceptions rather than letting code fail deep
inside.
Document your exceptions.
In docstrings, list what exceptions a function may raise.
11. Debugging Tips
Read the traceback. It pinpoints where the error happened.
Use pdb (Python debugger) to step through code at the exception site:
python -m pdb your_script.py
Interactive exploration: Catch exceptions and inspect state.
Assertions (assert x > 0) are useful for sanity checks but can be disabled
with optimization flags—don’t rely on them for essential error handling.
12. Real-World Example: Safe HTTP Request
import requests
def fetch_json(url):
try:
resp = requests.get(url, timeout=5)
resp.raise_for_status() # raises HTTPError for 4xx/5xx
except requests.Timeout:
print("Request timed out")
except requests.HTTPError as http_err:
print("HTTP error:", http_err)
except requests.RequestException as req_err:
print("Network error:", req_err)
else:
return resp.json()
return None
Differentiates network timeouts, HTTP errors, and other request issues.
Uses else for the successful path and returns None on failure.
Summary
Understand Python’s exception hierarchy.
Use try/except/else/finally to structure error handling.
Raise and define custom exceptions for clear, domain-specific errors.
Chain exceptions to preserve original context.
Leverage context managers for resource safety.
Follow best practices: catch narrowly, clean up, and log.
Debug effectively by reading tracebacks and using tools like pdb.
With these tools and patterns in your toolkit, you’ll be well-equipped to write
Python code that handles errors gracefully, remains maintainable, and lets
you recover or fail fast as appropriate.