Skip to main content

2.2.2 Exception Handling

Exception handling flowchart

Where this section fits

This section helps your program avoid crashing immediately when something goes wrong. Exception handling comes up again and again in file I/O, network requests, API calls, data cleaning, and model inference. The key is to learn how to anticipate errors, catch errors, and provide recoverable handling.

Learning objectives

  • Understand what exceptions are and why they need to be handled
  • Master the use of try/except/else/finally
  • Learn how to catch different types of exceptions
  • Write robust programs that do not crash easily

What is an exception?

An exception is an error that occurs while a program is running. A program without exception handling will crash immediately when it encounters an error:

# These lines will all crash the program
print(10 / 0) # ZeroDivisionError: division by zero
print(int("abc")) # ValueError: cannot convert
print([1, 2, 3][10]) # IndexError: list index out of range
print({"a": 1}["b"]) # KeyError: key does not exist

# If the program crashes, the code below will never run
print("This line will never be executed")

In real programs, errors are unavoidable — users may enter invalid data, files may not exist, and networks may disconnect. Exception handling lets you respond to these problems gracefully instead of letting the program crash.


Common exception types

Exception typeTrigger scenarioExample
ZeroDivisionErrorDivision by zero1 / 0
TypeErrorMismatched operation types"hello" + 5
ValueErrorInvalid valueint("abc")
IndexErrorList index out of range[1, 2][5]
KeyErrorDictionary key does not exist{"a": 1}["b"]
FileNotFoundErrorFile does not existopen("nonexistent.txt")
AttributeErrorAttribute does not exist"hello".foo()
NameErrorVariable is not definedprint(xyz)
ImportErrorImport failedimport nonexistent_module

Basic try / except usage

The logic of try/except is: try to run the code, and if something goes wrong, run an alternative plan.

try:
number = int(input("Please enter a number: "))
print(f"You entered: {number}")
except ValueError:
print("Invalid input! Please enter a number.")

print("The program continues running...") # This line runs whether or not there was an exception

Output behavior:

# Valid input
Please enter a number: 42
You entered: 42
The program continues running...

# Non-numeric input
Please enter a number: abc
Invalid input! Please enter a number.
The program continues running...

Key point: with try/except, the program will not crash because of an error.


Catching different types of exceptions

Catching multiple exceptions

def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Error: cannot divide by zero!")
return None
except TypeError:
print("Error: please pass numbers!")
return None

print(safe_divide(10, 3)) # 3.333...
print(safe_divide(10, 0)) # Error: cannot divide by zero! → None
print(safe_divide("10", 3)) # Error: please pass numbers! → None

Catching multiple exceptions (combined form)

try:
# Code that may fail
value = int(input("Please enter a number: "))
result = 100 / value
print(f"Result: {result}")
except (ValueError, ZeroDivisionError) as e:
print(f"An error occurred: {e}")
else:
print("Input was valid and division succeeded.")

Getting exception information

try:
number = int("abc")
except ValueError as e:
print(f"Exception type: {type(e).__name__}") # ValueError
print(f"Exception message: {e}") # invalid literal for int() with base 10: 'abc'

Catching all exceptions (use with caution)

try:
# Some code
result = risky_operation()
except Exception as e:
print(f"An unexpected error occurred: {type(e).__name__}: {e}")
else:
print(f"Operation succeeded: {result}")
Do not overuse except Exception

Catching all exceptions may seem convenient, but it can hide real bugs. You should try to catch specific exception types and use except Exception only as a last-resort fallback at the outermost level.

# Bad practice ❌
try:
do_something()
except: # Catches all exceptions, including KeyboardInterrupt
pass # And does nothing at all!

# Good practice ✅
try:
do_something()
except ValueError:
handle_value_error()
except FileNotFoundError:
handle_file_not_found()
except Exception as e:
logging.error(f"Unexpected error: {e}")

try / except / else / finally

A complete exception-handling structure has four parts:

try:
# Code to try
file = open("data.txt", "r")
content = file.read()
except FileNotFoundError:
# Runs when an error occurs
print("File not found!")
else:
# Runs when no error occurs
print(f"File content: {content}")
finally:
# Runs whether or not an error occurs (usually for cleanup)
print("Operation complete")
ClauseWhen it runsPurpose
tryAlwaysPut code that may fail here
exceptOnly when an error occursHandle the error
elseOnly when no error occursPut success logic here
finallyAlways, regardless of errorsClean up resources (close files, disconnect connections)

Typical use of finally

file = None
try:
file = open("data.txt", "r")
data = file.read()
data = data.strip()
print(data)
except FileNotFoundError:
print("File not found")
finally:
if file:
file.close() # Always close the file, whether or not there was an error
print("File closed")
A better approach: the with statement

In the later "File Operations" section, you will learn the with statement. It can automatically close resources and is cleaner than finally.


Raising exceptions

In addition to handling exceptions, you can also raise exceptions proactively — when you detect an invalid state, you tell the caller that "something is wrong."

The raise statement

def set_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 150:
raise ValueError(f"Age {age} is invalid and should be between 0 and 150")
return age

# Normal use
print(set_age(25)) # 25

# Trigger an exception
try:
set_age(-5)
except ValueError as e:
print(f"Error: {e}") # Error: Age -5 is invalid and should be between 0 and 150

try:
set_age("twenty")
except TypeError as e:
print(f"Error: {e}") # Error: Age must be an integer

Custom exceptions

When built-in exception types are not enough, you can define your own:

class InsufficientFundsError(Exception):
"""Insufficient funds error"""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Insufficient funds: current balance {balance}, attempted withdrawal {amount}")

class BankAccount:
def __init__(self, balance=0):
self.balance = balance

def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(self.balance, amount)
self.balance -= amount
return self.balance

# Use it
account = BankAccount(1000)
try:
account.withdraw(1500)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")
print(f"Current balance: {e.balance}, requested amount: {e.amount}")

Practical patterns

Pattern 1: LBYL vs EAFP

The Python community prefers EAFP (Easier to Ask Forgiveness than Permission, try first and handle errors) over LBYL (Look Before You Leap, check first and then act):

# LBYL style (check before acting) — not very Pythonic
if key in my_dict:
value = my_dict[key]
else:
value = default_value

# EAFP style (act first, handle errors later) — more Pythonic
try:
value = my_dict[key]
except KeyError:
value = default_value

# Of course, dictionaries have an even better way
value = my_dict.get(key, default_value)

Pattern 2: Retry mechanism

import time

def fetch_data_with_retry(url, max_retries=3):
"""Fetch data with retries"""
for attempt in range(1, max_retries + 1):
try:
print(f"Attempt {attempt}...")
# Simulate a network request
import random
if random.random() < 0.5:
raise ConnectionError("Network connection failed")
return "Fetched data"
except ConnectionError as e:
print(f" Failed: {e}")
if attempt < max_retries:
wait = attempt * 2 # Increasing wait time
print(f" Retrying in {wait} seconds...")
time.sleep(wait)
else:
print(" All retries failed!")
raise # If the last retry fails, re-raise the exception

try:
data = fetch_data_with_retry("https://api.example.com")
print(f"Success: {data}")
except ConnectionError:
print("Failed to fetch data საბოლო")

Pattern 3: Safe user input

def get_number(prompt, min_val=None, max_val=None):
"""Safely get a number from user input"""
while True:
try:
value = float(input(prompt))
if min_val is not None and value < min_val:
print(f"Please enter a number no less than {min_val}")
continue
if max_val is not None and value > max_val:
print(f"Please enter a number no greater than {max_val}")
continue
return value
except ValueError:
print("Please enter a valid number!")

# Use it
age = get_number("Please enter your age: ", min_val=0, max_val=150)
print(f"Your age is: {age}")

Comprehensive example: a safe grade management system

class GradeManager:
def __init__(self):
self.students = {}

def add_student(self, name, score):
"""Add a student's score"""
if not isinstance(name, str) or not name.strip():
raise ValueError("Student name cannot be empty")
if not isinstance(score, (int, float)):
raise TypeError(f"Score must be a number, got: {type(score).__name__}")
if not 0 <= score <= 100:
raise ValueError(f"Score {score} is out of range (0-100)")

self.students[name] = score
print(f"✅ Added successfully: {name} - {score} points")

def get_average(self):
"""Get the average score"""
if not self.students:
raise RuntimeError("No student data available, cannot calculate average")
return sum(self.students.values()) / len(self.students)

def get_student(self, name):
"""Look up a student's score"""
if name not in self.students:
raise KeyError(f"Cannot find student: {name}")
return self.students[name]

# Use it
gm = GradeManager()

# Safely add students
test_data = [
("Zhang San", 85),
("Li Si", 92),
("Wang Wu", "excellent"), # Type error
("Zhao Liu", 150), # Range error
("", 80), # Empty name
("Qian Qi", 78),
]

for name, score in test_data:
try:
gm.add_student(name, score)
except (ValueError, TypeError) as e:
print(f"❌ Add failed: {e}")

# Query
print(f"\nAverage score: {gm.get_average():.1f}")

try:
print(gm.get_student("Sun Ba"))
except KeyError as e:
print(f"Lookup failed: {e}")

Hands-on exercises

Exercise 1: Safe calculator

def safe_calculator(inputs=None):
"""A safe calculator that can handle invalid input and division by zero."""
inputs = iter(inputs or ["10", "0", "/", "n"])

while True:
try:
a = float(next(inputs) if inputs else input("First number: "))
b = float(next(inputs) if inputs else input("Second number: "))
op = next(inputs) if inputs else input("Operator (+, -, *, /): ")

if op == "+":
result = a + b
elif op == "-":
result = a - b
elif op == "*":
result = a * b
elif op == "/":
result = a / b
else:
raise ValueError(f"Unsupported operator: {op}")

print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero.")
except ValueError as error:
print(f"Invalid input: {error}")
except StopIteration:
break

again = next(inputs, "n") if inputs else input("Continue? (y/n): ")
if again.lower() != "y":
break

safe_calculator()

Exercise 2: File reader

def read_file_safely(filename):
"""Safely read file contents."""
try:
with open(filename, "r", encoding="utf-8") as file:
return file.read()
except FileNotFoundError:
print(f"File not found: {filename}")
except PermissionError:
print(f"No permission to read: {filename}")
except OSError as error:
print(f"Read failed: {error}")
return None

content = read_file_safely("test.txt")
if content:
print(content)

Exercise 3: Batch type conversion

def convert_to_numbers(data_list):
"""Convert strings to numbers; keep errors for later inspection."""
numbers = []
errors = []
for item in data_list:
try:
numbers.append(float(item))
except ValueError:
numbers.append(None)
errors.append(f"{item} cannot be converted")
return numbers, errors

values, errors = convert_to_numbers(["10", "20.5", "abc", "30", "xyz"])
print(values)
print(errors)

Summary

SyntaxPurposeWhen to use
tryWrap code that may failAnywhere errors may occur
exceptCatch and handle exceptionsFor specific exception types
elseRuns when no exception occursSuccess logic
finallyAlways runsResource cleanup
raiseRaise an exception proactivelyWhen input is invalid or state is wrong
Custom exceptionsCreate business-specific exceptionsWhen built-in exceptions are not descriptive enough
Core idea

The essence of exception handling is: anticipate possible problems and prepare a response plan. A good program is not one that never makes mistakes, but one that can handle them gracefully when they happen — by giving users friendly messages, recording error information, or retrying automatically. This is an important difference between professional developers and beginners.