
Python Course
Exception Handling in Python
Every program you write will eventually encounter something unexpected — a user types letters where you expected a number, a file does not exist, a network connection drops, a database times out. 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 covers the full picture — from the basics of try/except to raising your own exceptions, chaining errors, and the production-quality patterns professional developers follow 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 and where.
If nothing catches that exception, the program terminates and Python prints a traceback — a chain of messages showing exactly which line failed, what function called it, and what type of error occurred.
Common built-in exception types you will encounter constantly:
- ValueError — right type, wrong value:
int("hello") - TypeError — wrong type entirely:
"5" + 5 - ZeroDivisionError — dividing any number by zero
- FileNotFoundError — opening a file that does not exist
- IndexError — accessing a list index out of range
- KeyError — accessing a dictionary key that does not exist
- AttributeError — calling a method that does not exist on an object
- NameError — using a variable that was never defined
- PermissionError — trying to read or write a file without permission
- OverflowError — a number calculation exceeds the maximum representable value
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. Execution then continues normally after the block.
# Basic try/except — safely convert user input to a number
def get_number(raw):
try:
n = int(raw) # this line might raise ValueError
print("You entered:", n)
return n
except ValueError:
print("That was not a valid number.")
return None
get_number("42") # succeeds
get_number("hello") # ValueError caught — no crash
get_number("3.14") # also ValueError — int() won't parse decimals
- Code inside
tryruns normally until an exception occurs — then Python immediately jumps toexcept. - If no exception occurs, the
exceptblock is skipped entirely. - The program continues after the try/except block in both cases — no crash.
- Only one
exceptblock runs per exception — Python stops at the first match.
Catching Multiple Exception Types
A single try block can raise different kinds of errors. Handle each one separately with multiple except clauses, or group them in a tuple when they need the same response.
def safe_divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
except TypeError:
print("Both inputs must be numbers.")
return None
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # ZeroDivisionError
print(safe_divide("x", 2)) # TypeError
# Group exceptions that need the same response
def parse_data(value, index, data):
try:
return int(value) + data[index]
except (ValueError, IndexError) as e:
print(f"Data error: {e}")
return 0
print(parse_data("abc", 0, [10])) # ValueError
print(parse_data("5", 9, [10])) # IndexError
- Python checks
exceptclauses top to bottom — always list specific exceptions before broad ones. except (ValueError, TypeError):handles both with the same block — clean when the response is identical.- Different errors often need different messages — keep them separate when the user needs specific guidance.
Accessing the Exception Object with as e
Using as e in your except clause binds the exception object to a variable. That object holds the error message and type — essential for logging and for writing informative messages without exposing raw Python tracebacks to users.
items = [10, 20, 30]
try:
val = items[9] # IndexError — index 9 does not exist
except IndexError as e:
print("Error message:", e)
print("Error type :", type(e).__name__)
# Using e in a realistic logging function
import datetime
def safe_open(path):
try:
with open(path, "r") as f:
return f.read()
except FileNotFoundError as e:
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
print(f"[{timestamp}] File error: {e}")
return ""
content = safe_open("missing_file.txt")
str(e)orprint(e)shows the human-readable error message Python generated.type(e).__name__gives the exception class name as a plain string — useful for logging systems.- In production, log the full exception but show a friendly message to the user — never expose raw tracebacks in a UI.
else and finally — The Full Structure
Python's try block has two optional extra clauses that give precise control over flow in every outcome:
- else — runs only if
trycompleted without any exception — keeps success logic separate from error-prone code. - finally — runs always, whether an exception occurred or not — the right place for cleanup like closing files, releasing database connections, or stopping timers.
def read_score(val):
try:
score = int(val)
except ValueError:
print(f" Invalid: '{val}' is not a number.")
else:
# only reaches here if try had no exception
if 0 <= score <= 100:
print(f" Score accepted: {score}")
else:
print(f" Score {score} is out of range (0-100).")
finally:
# always runs — success or failure
print(" --- validation complete ---")
for v in ["85", "abc", "150"]:
print(f"Testing '{v}':")
read_score(v)
elseonly runs whentrycompletes with zero exceptions — keep the main success logic here, not intry.finallyruns even if areturnis insidetry, and even if an unhandled exception propagates upward.- Order is always:
try→except(if error) →else(if no error) →finally(always). - The most common use of
finallyis closing resources — thoughwithstatements (Lesson 37) handle this more elegantly.
Raising Exceptions
You are not limited to catching exceptions Python raises — you can raise your own using the raise keyword. This lets you enforce your own rules and signal errors from inside your functions with clear, meaningful messages.
def set_age(age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}.")
if age < 0 or age > 120:
raise ValueError(f"Age {age} is out of valid range (0–120).")
return age
def register_user(name, age):
try:
validated_age = set_age(age)
print(f"Registered: {name}, age {validated_age}")
except (TypeError, ValueError) as e:
print(f"Registration failed: {e}")
register_user("Priya", 28) # valid
register_user("Kiran", -5) # ValueError
register_user("Arjun", "old") # TypeError
raise ExceptionType("message")is the standard syntax — pick the most specific built-in type that fits.- Raised exceptions propagate up the call stack until something catches them — if nothing does, the program terminates.
- Use
raisewhenever Python would not naturally raise an error but your application rules say the input is invalid.
Custom Exception Classes
For larger programs, define your own exception types by creating a class that inherits from Exception. Custom exceptions make error handling self-documenting and let callers catch your specific error without confusing it with built-in exceptions.
# Simple custom exception — inherits everything from Exception
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the account balance."""
pass
# Custom exception with extra attributes
class ValidationError(Exception):
"""Raised when input fails validation rules."""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
# Using both
def withdraw(balance, amount):
if amount <= 0:
raise ValidationError("amount", "Must be greater than zero.")
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw ${amount:.2f} — balance is only ${balance:.2f}."
)
return balance - amount
for amt in [50, 200, -10]:
try:
new_bal = withdraw(100.00, amt)
print(f"Withdrew ${amt:.2f} — new balance: ${new_bal:.2f}")
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")
except ValidationError as e:
print(f"Input error ({e.field}): {e.message}")
- Custom exceptions inherit from
Exception—passis enough for a basic one. - Override
__init__to add fields like error codes or field names — callers can then accesse.fielddirectly. - Always call
super().__init__(message)so the message appears normally when the exception is printed. - Name custom exceptions with the
Errorsuffix by Python convention:InsufficientFundsError, notInsufficientFunds.
Re-raising and Exception Chaining
Sometimes you want to catch an exception, do something with it (like log it), and then let it continue propagating. Use bare raise to re-raise the same exception unchanged. Use raise NewError(...) from original to chain exceptions — preserving the original cause while adding context.
# Re-raise with bare raise — preserves original traceback
def load_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
print(f"[LOG] Config missing: {e}")
raise # re-raises the same FileNotFoundError unchanged
# Exception chaining — raise new error and link to original cause
class ConfigError(Exception):
pass
def load_settings(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
raise ConfigError(f"Cannot load settings from '{path}'.") from e
try:
load_settings("app_settings.cfg")
except ConfigError as e:
print(f"Config error: {e}")
print(f"Caused by : {e.__cause__}")
- Bare
raisere-raises the current exception with the original traceback intact — essential for middleware and logging layers. raise NewError(...) from originalchains exceptions —e.__cause__holds the original exception for inspection.- Chaining is the professional pattern when you want to convert low-level exceptions (like
FileNotFoundError) into application-level ones (likeConfigError) without losing the root cause.
Exception Handling in Real Programs
This example simulates a data import pipeline that reads a CSV, validates each row, and collects all errors rather than stopping at the first one — the pattern used in real ETL tools and form validators.
class RowValidationError(Exception):
def __init__(self, row_num, field, issue):
self.row_num = row_num
self.field = field
self.issue = issue
super().__init__(f"Row {row_num}, {field}: {issue}")
def validate_row(row_num, row):
"""Validates one row of data. Returns cleaned dict or raises."""
name = row.get("name", "").strip()
price = row.get("price", "")
stock = row.get("stock", "")
if not name:
raise RowValidationError(row_num, "name", "cannot be empty")
try:
price = float(price)
if price < 0:
raise RowValidationError(row_num, "price", "cannot be negative")
except ValueError:
raise RowValidationError(row_num, "price", f"'{price}' is not a number")
try:
stock = int(stock)
except ValueError:
raise RowValidationError(row_num, "stock", f"'{stock}' must be an integer")
return {"name": name, "price": price, "stock": stock}
# Simulate incoming data with some bad rows
raw_data = [
{"name": "Laptop", "price": "1200", "stock": "15"},
{"name": "", "price": "450", "stock": "30"}, # bad name
{"name": "Keyboard", "price": "free", "stock": "100"}, # bad price
{"name": "Webcam", "price": "120", "stock": "many"}, # bad stock
{"name": "Monitor", "price": "8500", "stock": "5"},
]
valid_rows = []
error_log = []
for i, row in enumerate(raw_data, start=1):
try:
valid_rows.append(validate_row(i, row))
except RowValidationError as e:
error_log.append(str(e))
print(f"Valid rows : {len(valid_rows)}")
print(f"Errors : {len(error_log)}")
print("\nValid data:")
for r in valid_rows:
print(f" {r['name']:<12} ${r['price']:.2f} (stock: {r['stock']})")
print("\nErrors found:")
for err in error_log:
print(f" {err}")
- Collecting errors rather than stopping at the first one is the standard pattern for data import tools and form validators.
- The custom
RowValidationErrorcarries structured data (row_num,field) — callers can build reports without parsing strings. - This separates validation logic from display logic cleanly — each function does one job.
Best Practices
- Never use bare
except:— it silently catches everything includingKeyboardInterruptandSystemExit. Always name the exception type. - Keep
tryblocks small — wrap only the specific line that can fail. Smaller scope means fewer masked bugs. - Never suppress exceptions silently — an empty
exceptblock hides real problems. At minimum, log the error. - Use
finallyfor cleanup — or better, usewithstatements which handle cleanup automatically. - Raise the most specific type —
ValueErroris better thanException. Custom exceptions are better still. - Let unexpected exceptions propagate — only catch exceptions you know how to handle. An unhandled exception that surfaces is easier to debug than one silently swallowed.
Quick Reference Table
| Keyword / Concept | Purpose | When It Runs |
|---|---|---|
try | Wraps code that might raise an exception | Always — first |
except ExcType | Catches a specific exception type | Only on matching exception |
except (A, B) | Catches either of two exception types | Only on matching exception |
except ... as e | Binds exception object to variable | Inside except clause |
else | Success logic — runs after try with no error | Only when try had no exception |
finally | Cleanup — always executes | Always, even if unhandled |
raise ExcType("msg") | Triggers an exception manually | When you call it explicitly |
raise (bare) | Re-raises the current exception unchanged | Inside an except block |
raise New from original | Exception chaining with preserved cause | When converting exception types |
| Custom exception class | App-specific error type via class inheritance | When raised in your code |
Practice
What keyword wraps code that might raise an exception?
What exception type is raised when you call int("hello")?
Which clause runs only when the try block completes with no exception?
Which clause always runs — whether an exception occurred or not?
What built-in class must a custom exception inherit from?
What syntax binds the exception object to the variable e?
Quick Quiz
What happens if an exception is raised in a try block and no matching except clause exists?
Which clause is guaranteed to run regardless of whether an exception occurred?
Which is the correct way to raise a ValueError with a custom message?
What does a bare raise statement (no argument) do inside an except block?
Which is considered best practice in exception handling?
Which syntax chains a new exception while preserving the original cause?