Python Course
Exception Handling
Every program you write will eventually run into something unexpected — a user types letters where you expected a number, a file doesn't exist, a network connection drops. Without a plan for those moments, your program crashes with an ugly error message and stops completely. Exception handling is Python's built-in system for catching those problems, responding to them gracefully, and keeping your program running.
This lesson builds from the ground up: what exceptions are, why they exist, how to catch and raise them, and how to write production-quality error-handling code used by professional developers every day.
What Is an Exception?
An exception is an event that disrupts the normal flow of a program. When Python encounters an operation it cannot complete — dividing by zero, opening a missing file, converting invalid input — it raises an exception object that carries information about what went wrong.
If nothing catches that exception, the program terminates and Python prints a traceback — a chain of messages showing exactly where the failure happened and what type of error it was.
Common built-in exception types you will see constantly:
- ValueError — right type, wrong value (e.g.,
int("hello")) - TypeError — wrong type entirely (e.g.,
"5" + 5) - ZeroDivisionError — dividing any number by zero
- FileNotFoundError — opening a file that does not exist
- IndexError — accessing a list index that is out of range
- KeyError — accessing a dictionary key that does not exist
- AttributeError — calling a method that doesn't exist on an object
- NameError — using a variable that was never defined
The try / except Block
The core tool is the try / except block. Code that might fail goes inside try. If an exception is raised, Python jumps immediately to the matching except block instead of crashing.
Why it exists: programs in the real world interact with data they cannot fully control — user input, files, APIs, databases. Exception handling lets you define what happens when things go wrong instead of leaving it to chance.
Real-world use: an online store checkout form catches a ValueError when a user accidentally types letters into the quantity field and shows a friendly message rather than a server error page.
# Basic try/except — catching a ValueError
# Ask the user for a number and safely convert it
raw = input("Enter a number: ") # get raw text from user
try:
n = int(raw) # attempt conversion to integer
print("You entered:", n) # runs only if conversion succeeds
except ValueError:
print("That wasn't a valid number.") # runs if int() failsYou entered: 42
If user types "hello":
That wasn't a valid number.
- Code inside
tryruns normally until an exception occurs - When an exception matches the
excepttype, that block runs instead - If no exception occurs, the
exceptblock is skipped entirely - The program continues after the try/except block in both cases
Catching Multiple Exception Types
A single try block can raise different kinds of errors. You can handle each one separately with multiple except clauses, or group them together inside a tuple.
Why it exists: different errors often require different responses. A missing file needs a different message than bad user input.
Real-world use: a data import tool catches FileNotFoundError to tell users the file path is wrong, and catches ValueError to tell them the file's contents are in the wrong format — two separate, helpful messages.
# Handling multiple exception types separately
def divide(a, b):
try:
result = a / b # could raise ZeroDivisionError
return result
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
except TypeError: # catches e.g. divide("x", 2)
print("Both inputs must be numbers.")
return None
print(divide(10, 2)) # normal case
print(divide(10, 0)) # ZeroDivisionError
print(divide("x", 2)) # TypeErrorCannot divide by zero.
None
Both inputs must be numbers.
None
- Python checks
exceptclauses top to bottom and runs the first match - Only one
exceptblock runs per exception — not all of them - You can group types:
except (ValueError, TypeError):handles both the same way - Always list specific exceptions before broad ones
Accessing the Exception Object
Using as e in your except clause binds the exception object to a variable. That object holds the error message and type, which you can log or display.
Why it exists: the exception object contains specific details about what went wrong — essential for debugging and for writing informative error messages in applications.
Real-world use: a payment processing service logs the exact exception message to a file so developers can diagnose failures without exposing technical details to customers.
# Using "as e" to capture the exception message
items = [10, 20, 30] # a list with three items
try:
val = items[9] # index 9 does not exist — IndexError
except IndexError as e:
print("Error caught:", e) # print the exception message
print("Type:", type(e).__name__) # print the exception class nameType: IndexError
eis just a conventional variable name — you can use any namestr(e)orprint(e)shows the human-readable error messagetype(e).__name__gives you the exception class name as a string- This is extremely useful for logging systems in production code
The else and finally Clauses
Python's try block has two optional extra clauses that give you precise control over what happens when things succeed and what always runs regardless.
- else — runs only if the
tryblock completed without raising any exception - finally — runs always, whether an exception occurred or not — perfect for cleanup
Why it exists: separating "success" logic into else keeps try blocks minimal and focused. finally guarantees cleanup — closing files, releasing locks, disconnecting from databases — even when errors occur.
Real-world use: a database query function uses finally to close the database connection whether the query succeeded or failed, preventing connection leaks.
# Full try / except / else / finally structure
def read_score(val):
try:
score = int(val) # attempt conversion
except ValueError:
print("Invalid input — not a number.")
else:
# only runs if try succeeded (no exception)
print("Score accepted:", score)
finally:
# always runs — success or failure
print("--- validation complete ---")
read_score("85") # succeeds
print()
read_score("abc") # fails--- validation complete ---
Invalid input — not a number.
--- validation complete ---
elseonly runs whentrycompletes with zero exceptionsfinallyruns in every case — including when an unhandled exception propagates upward- Even if a
returnstatement is insidetry,finallystill executes before returning - The order is always:
try→except(if error) →else(if no error) →finally
Raising Exceptions
You are not limited to catching exceptions Python raises — you can raise your own using the raise keyword. This lets you enforce rules and signal errors from inside your own functions.
Why it exists: sometimes Python won't raise an error even though the input is logically wrong for your application. Raising exceptions lets you define and communicate your own rules clearly.
Real-world use: an age verification function raises a ValueError if the submitted age is negative or above 120 — values that are technically valid integers but logically impossible.
# Raising exceptions to enforce business rules
def set_age(age):
if not isinstance(age, int): # must be an integer
raise TypeError("Age must be an integer.")
if age < 0 or age > 120: # must be a realistic range
raise ValueError(f"Age {age} is out of valid range (0-120).")
return age
try:
print(set_age(25)) # valid
print(set_age(-5)) # triggers ValueError
except ValueError as e:
print("ValueError:", e)
except TypeError as e:
print("TypeError:", e)ValueError: Age -5 is out of valid range (0-120).
raise ExceptionType("message")is the standard syntax- You can raise any built-in exception type with a custom message
- Raised exceptions propagate up the call stack until something catches them
- If nothing catches a raised exception, the program terminates with a traceback
Custom Exception Classes
For larger programs, you can define your own exception types by creating a class that inherits from Exception. This lets callers catch your specific error type without confusing it with built-in exceptions.
Why it exists: built-in exceptions are generic. Custom exceptions carry meaning specific to your application, making error handling self-documenting and easier to maintain.
Real-world use: a payment library raises InsufficientFundsError instead of a generic ValueError so the calling code can catch that exact condition and show the appropriate checkout message.
# Defining and raising a custom exception class
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the account balance."""
pass # inherits everything from Exception — no extra code needed
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw ${amount:.2f} — balance is only ${balance:.2f}."
)
return balance - amount # return new balance if successful
try:
new_bal = withdraw(50.00, 80.00) # attempt to overdraw
print("New balance:", new_bal)
except InsufficientFundsError as e:
print("Transaction failed:", e)- Custom exceptions inherit from
Exception(or another exception class) passis enough for a basic custom exception — the message comes fromraise- You can add attributes (like error codes) by overriding
__init__ - Name custom exceptions with the
Errorsuffix by Python convention
Re-raising Exceptions
Sometimes you want to catch an exception, do something with it (like log it), and then let it continue propagating up the call stack. Use a bare raise inside an except block to re-raise the current exception.
# Re-raising an exception after logging it
def load_config(path):
try:
f = open(path) # try to open a config file
data = f.read()
f.close()
return data
except FileNotFoundError as e:
print(f"[LOG] Config file missing: {e}") # log the problem
raise # re-raise same exception
try:
load_config("settings.cfg")
except FileNotFoundError:
print("Application cannot start without a config file.")Application cannot start without a config file.
- Bare
raise(no argument) re-raises the currently active exception unchanged - This preserves the original traceback — essential for debugging
- Useful in middleware, logging layers, and decorator functions
- Contrast with
raise NewError(...) from ewhich chains exceptions together
Exception Handling Best Practices
Professional developers follow a set of rules that keep exception handling clean, readable, and safe. Understanding these will immediately elevate your code quality.
- Never use bare
except:— it silently catches everything includingKeyboardInterruptandSystemExit, masking serious problems. Always name the exception type. - Keep
tryblocks small — only wrap the specific line(s) that can fail, not entire functions. Smaller scope means clearer intent and fewer masked bugs. - Don't suppress exceptions silently — an empty
exceptblock that does nothing hides real problems. At minimum, log the error. - Use
finallyfor resource cleanup — or better yet, usewithstatements (context managers, covered in Lesson 37) which handle cleanup automatically. - Raise the most specific exception type —
ValueErroris more useful thanException. Custom exceptions are more useful still.
# Best practice: small try block, specific exception, logging
import logging
def parse_price(text):
try:
price = float(text) # ONLY this line can fail
except ValueError:
logging.warning("Invalid price input: %s", text) # log it
return 0.0 # return safe default
return price
print(parse_price("19.99")) # valid price string
print(parse_price("free")) # invalid — returns 0.0 safely0.0
Summary Table
| Keyword / Concept | Purpose | When It Runs |
|---|---|---|
try |
Wraps code that might raise an exception | Always — first |
except |
Catches a specific exception type and handles it | Only when matching exception is raised |
else |
Runs success logic separate from try | Only when try completes with no exception |
finally |
Cleanup code — always executes | Always — even if exception is unhandled |
raise |
Triggers an exception manually | When you call it explicitly |
as e |
Binds exception object to a variable | Inside except clause |
| Custom Exception | App-specific error type via class inheritance | When raised explicitly in your code |
Practice Questions
Practice 1. What keyword do you use to wrap code that might raise an exception?
Practice 2. What exception type is raised when you call int("hello")?
Practice 3. Which clause in a try block runs only when no exception is raised?
Practice 4. What syntax do you use inside an except clause to bind the exception object to the variable e?
Practice 5. To create a custom exception class, what built-in class must it inherit from?
Quiz
Quiz 1. What happens if an exception is raised inside a try block and no matching except clause exists?
Quiz 2. Which clause is guaranteed to run regardless of whether an exception occurred?
Quiz 3. Which of the following is the correct way to raise a ValueError with a custom message?
Quiz 4. What does a bare raise statement (with no argument) do inside an except block?
Quiz 5. Which of the following is considered best practice when writing exception handling code?