Exception Handling in Python
Reviewed & published by Brayan K
Learn to detect, catch, and manage errors gracefully so your programs never crash unexpectedly.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- What exceptions are and why programs crash without handling
- How to use try / except to catch errors gracefully
- How to catch specific exception types (ValueError, KeyError, etc.)
- How to capture the error message with as e
- The else and finally blocks and when to use them
- How to raise your own exceptions with raise
What Are Exceptions?
When something goes wrong in your Python code — like dividing by zero or opening a missing file — Python raises an exception. Without handling, your program crashes.
# This code will crash without exception handling
result = 10 / 0 # ZeroDivisionError!
print("This line never runs")Why Handle Exceptions?
| Without Exception Handling | With Exception Handling |
|---|---|
| Program crashes completely | Program continues running |
| Confusing error messages | User-friendly messages |
| Data might be lost | Data is saved safely |
| Resources left open | Resources cleaned up properly |
The try-except Block
Wrap risky code in a try block. If an error occurs, the except block handles it.
| Syntax | What It Does |
|---|---|
| try: | Code that might fail goes here |
| except: | Runs if an error occurs in try |
try:
result = 10 / 0
print("Result:", result)
except:
print("Oops! Something went wrong.")
print("Program continues running!")
# ✅ Expected output:
# Oops! Something went wrong.
# Program continues running!Catching Specific Exceptions
Always catch specific exception types. This is the professional way.
# Catching a specific exception type
try:
number = int("hello") # Can't convert "hello" to a number
except ValueError:
print("That's not a valid number!")
# This only catches ValueError, not other errors
# ✅ Expected output:
# That's not a valid number!Common Exception Types
Here are the exceptions you'll encounter most often:
| Exception | When It Happens | Example |
|---|---|---|
| ValueError | Wrong value type | int("abc") |
| TypeError | Wrong operation for type | "5" + 3 |
| ZeroDivisionError | Dividing by zero | 10 / 0 |
| IndexError | Invalid list index | my_list[100] |
| KeyError | Missing dictionary key | my_dict["name"] |
| FileNotFoundError | File doesn't exist | open("missing.txt") |
| NameError | Variable not defined | print(xyz) |
# Try different exceptions
my_list = [1, 2, 3]
try:
print(my_list[10]) # IndexError
except IndexError:
print("Index out of range!")
# Try this with a dictionary
my_dict = {"name": "Alice"}
try:
print(my_dict["age"]) # KeyError
except KeyError:
print("Key not found!")
# ✅ Expected output:
# Index out of range!
# Key not found!Catching Multiple Exceptions
You can handle different exceptions differently:
user_input = "abc" # Try changing to "0" or "5"
try:
number = int(user_input)
result = 100 / number
print("Result:", result)
except ValueError:
print("Please enter a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
print("Program continues...")
# ✅ Expected output:
# Please enter a valid number!
# Program continues...Or handle them together if the response is the same:
try:
risky = 10 / int("zero")
except (ValueError, ZeroDivisionError):
print("Invalid input - please try again!")
# ✅ Expected output:
# Invalid input - please try again!Getting the Error Message
Use as to capture the error details:
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error occurred: {e}")
# e contains the error message
# This is helpful for logging and debugging
# ✅ Expected output:
# Error occurred: division by zeroThe else Block
The else block runs only if NO exception occurred:
| Block | When It Runs |
|---|---|
| try: | Always tries to run |
| except: | Only if error in try |
| else: | Only if NO error in try |
number = "5" # Try changing to "abc"
try:
result = int(number)
except ValueError:
print("Invalid number!")
else:
print(f"Success! You entered: {result}")
print(f"Doubled: {result * 2}")
# ✅ Expected output:
# Success! You entered: 5
# Doubled: 10The finally Block
The finally block always runs, whether there was an error or not. Perfect for cleanup.
print("Starting operation...")
try:
result = 10 / 2 # Try changing to 10 / 0
print(f"Result: {result}")
except ZeroDivisionError:
print("Cannot divide by zero!")
finally:
print("Cleanup complete!") # Always runs
print("Done!")
# ✅ Expected output:
# Starting operation...
# Result: 5.0
# Cleanup complete!
# Done!The Complete Structure
All four blocks together:
try:
# Risky code here
except SomeError:
# Handle the error
else:
# Runs if no error
finally:
# Always runs (cleanup)filename = "data.txt"
try:
# Simulate file operation
print(f"Trying to open {filename}...")
# file = open(filename) # Would cause FileNotFoundError
data = "Sample data" # Simulated success
except FileNotFoundError:
print("File not found!")
data = None
else:
print("File read successfully!")
print(f"Data: {data}")
finally:
print("Operation complete - resources cleaned up.")
# ✅ Expected output:
# Trying to open data.txt...
# File read successfully!
# Data: Sample data
# Operation complete - resources cleaned up.Worked Example: Watch Every Block Run
Reading the four blocks in a diagram is one thing; watching them fire in a real program is another. This function narrates itself. Run it and follow the order of the printed lines — that order is the whole lesson in one screen.
def divide(a, b):
"""Divide a by b, narrating every block as it runs."""
try:
result = a / b # the risky line - it may explode
except ZeroDivisionError as e: # 'as e' captures the exception OBJECT
print(" except :", e) # printing it shows Python's own message
return None
except TypeError as e: # a second, different problem, handled separately
print(" except :", e)
return None
else:
# 'else' runs only when the try block finished without an error.
print(" else : no error, result is", result)
return result
finally:
# 'finally' runs no matter what - even after a return statement.
print(" finally : always runs, error or not")
print("10 / 2")
print("returned:", divide(10, 2)) # the happy path: try -> else -> finally
print("10 / 0")
print("returned:", divide(10, 0)) # try -> except (ZeroDivisionError) -> finally
print("10 / 'x'")
print("returned:", divide(10, "x")) # try -> except (TypeError) -> finally
# ✅ Expected output:
# 10 / 2
# else : no error, result is 5.0
# finally : always runs, error or not
# returned: 5.0
# 10 / 0
# except : division by zero
# finally : always runs, error or not
# returned: None
# 10 / 'x'
# except : unsupported operand type(s) for /: 'int' and 'str'
# finally : always runs, error or not
# returned: NoneRaising Exceptions
Use raise to trigger an exception yourself. This is useful for validating data.
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative!")
if age > 150:
raise ValueError("Age seems unrealistic!")
return age
# Test it
try:
my_age = set_age(-5) # Try 25 or 200
except ValueError as e:
print(f"Error: {e}")
# ✅ Expected output:
# Error: Age cannot be negative!🎯 Your Turn: Catch It, Then Raise It
This function has to do both jobs: survive text that is not a number, and refuse an age that makes no sense. Three blanks are missing. Fill them in, run it, and match the expected output.
# 🎯 YOUR TURN — replace each ___ and run it.
def read_age(raw):
try:
age = int(raw) # int("abc") cannot work - what does Python raise?
except ___ as e: # 👉 name that exception type here
print("not a number:", e)
return None
if age < 0:
___ ValueError("age cannot be negative") # 👉 the keyword that throws an error yourself
return age
print(read_age("34")) # a clean conversion
print(read_age("abc")) # goes down the except branch, then returns None
try:
read_age("-5") # this one raises, so the caller has to catch it
except ValueError ___ err: # 👉 how do you capture the exception object?
print("caught:", err)
# ✅ Expected output:
# 34
# not a number: invalid literal for int() with base 10: 'abc'
# None
# caught: age cannot be negativeCommon Mistakes to Avoid
| Mistake | Problem | Fix |
|---|---|---|
| except: | Catches everything, hides bugs | except ValueError: |
| Large try block | Hard to know what failed | Put only risky code in try |
| Empty except block | Error is silently ignored | At least log the error |
| Catching too high | except Exception catches all | Be specific about types |
| Not cleaning up | Files/connections left open | Use finally or with |
# BAD - Catches everything, hides bugs
try:
result = 10 / my_variable # NameError hidden!
except:
print("Something went wrong")
# GOOD - Specific exceptions
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
# NameError will still crash (and we'll notice the bug!)
# ✅ Expected output:
# Something went wrong
# Cannot divide by zeroPractical Example: Safe Number Input
A function that safely gets a number from user input:
def get_number(prompt, default=0):
"""Safely get a number, return default if invalid."""
user_input = input(prompt) # In practice, this would be real input
try:
return float(user_input)
except ValueError:
print(f"Invalid input, using default: {default}")
return default
# Simulate with different inputs
test_inputs = ["42", "3.14", "hello", ""]
for test in test_inputs:
print(f"Input: '{test}'")
try:
result = float(test) if test else 0
print(f" → Got: {result}")
except ValueError:
print(f" → Invalid, using default: 0")Practical Example: Calculator
A safe calculator that handles errors:
def safe_calculate(a, b, operation):
"""Perform calculation with error handling."""
try:
if operation == "+":
return a + b
elif operation == "-":
return a - b
elif operation == "*":
return a * b
elif operation == "/":
return a / b
else:
raise ValueError(f"Unknown operation: {operation}")
except ZeroDivisionError:
return "Error: Cannot divide by zero"
except TypeError:
return "Error: Invalid number type"
# Test the calculator
print(safe_calculate(10, 5, "+")) # 15
print(safe_calculate(10, 5, "/")) # 2.0
print(safe_calculate(10, 0, "/")) # Error message
print(safe_calculate(10, 5, "^")) # Unknown operationPractical Example: Data Processor
Process a list of data, handling errors without stopping:
def process_data(data_list):
"""Process each item, skip invalid ones."""
results = []
errors = []
for i, item in enumerate(data_list):
try:
# Try to convert to number and double it
result = float(item) * 2
results.append(result)
except (ValueError, TypeError) as e:
errors.append(f"Item {i}: {item} - {e}")
return results, errors
# Test with mixed data
data = ["10", "5.5", "hello", "20", None, "15"]
results, errors = process_data(data)
print("Successful results:", results)
print("Errors encountered:")
for error in errors:
print(f" - {error}")
# ✅ Expected output:
# Successful results: [20.0, 11.0, 40.0, 30.0]
# Errors encountered:
# - Item 2: hello - could not convert string to float: 'hello'
# - Item 4: None - float() argument must be a string or a real number, not 'NoneType'🎯 Mini-Challenge: Survive a Messy File
Real data is never clean. You are given five rows that should all be numbers — two of them are not. Your program must add up the good ones, report the bad ones, and never crash. Only the brief is given; the code is yours.
# 🎯 MINI-CHALLENGE: total the good rows, survive the bad ones
#
# 1. Start with: rows = ["12", "7", "oops", "", "5"]
# 2. Keep a running total (starts at 0) and a skipped counter (starts at 0)
# 3. Loop over rows. Inside a try, convert the row with int() and add it to
# the total.
# 4. Catch the ValueError. In the handler, print skipped: 'oops'
# (the row's own text inside single quotes) and add 1 to the skipped counter.
# 5. After the loop, print the total and then the skipped count.
#
# ✅ Expected output:
# skipped: 'oops'
# skipped: ''
# total: 24
# skipped count: 2
# your code hereSummary: Quick Reference
| Keyword | Purpose | Example |
|---|---|---|
| try: | Code that might fail | try: x = 1/0 |
| except Type: | Handle specific error | except ValueError: |
| except Type as e: | Capture error message | except ValueError as e: |
| else: | Runs if no error | else: print("OK") |
| finally: | Always runs (cleanup) | finally: file.close() |
| raise | Trigger an exception | raise ValueError("!") |
Key Takeaways:
- • Always catch specific exception types
- • Use finally for cleanup that must always happen
- • Use else for code that should only run on success
- • Raise exceptions early when you detect invalid data
- • Never use bare except: in production code
Wrapping Up
You now know how to protect your programs from crashing and handle errors gracefully. Exception handling is essential for building reliable, professional Python applications.
Lesson done — your programs are now crash-proof!
try/except, else, finally, raise — you know the full exception handling toolkit. This is what separates fragile scripts from professional, production-ready code.
Practice quiz
Why does try/except exist?
- To make code run faster
- To catch errors so the program doesn't crash
- To define new variables
- To import modules
Answer: To catch errors so the program doesn't crash. try/except lets you handle errors gracefully so a problem doesn't crash the whole program.
Which exception is raised by int('hello')?
- TypeError
- KeyError
- ValueError
- IndexError
Answer: ValueError. Converting a non-numeric string with int() raises ValueError.
Which exception does accessing my_dict['age'] raise when that key is missing?
- KeyError
- IndexError
- NameError
- ValueError
Answer: KeyError. A missing dictionary key raises KeyError.
What does 10 / 0 raise?
- ValueError
- ZeroDivisionError
- ArithmeticError only
- Nothing, it returns infinity
Answer: ZeroDivisionError. Dividing by zero raises ZeroDivisionError.
When does the else block of a try statement run?
- Always
- Only if an exception occurred
- Only if NO exception occurred in try
- Never
Answer: Only if NO exception occurred in try. The else block runs only when the try block completed without raising an exception.
When does the finally block run?
- Only on success
- Only on error
- Always, whether or not an error occurred
- Only if there is no except block
Answer: Always, whether or not an error occurred. finally always runs — it's ideal for cleanup like closing files or connections.
How do you capture the error message in a variable named e?
- except ValueError -> e:
- except ValueError as e:
- except ValueError(e):
- except e = ValueError:
Answer: except ValueError as e:. Use 'except ValueError as e:' to bind the exception object to e.
Why is a bare 'except:' discouraged?
- It is slower
- It catches ALL errors and can hide bugs
- It is invalid syntax
- It only catches ValueError
Answer: It catches ALL errors and can hide bugs. A bare except catches everything, even unexpected errors, which can silently hide bugs.
How do you trigger an exception yourself for invalid data?
- throw ValueError('bad')
- raise ValueError('bad')
- error ValueError('bad')
- except ValueError('bad')
Answer: raise ValueError('bad'). The raise keyword triggers an exception, e.g. raise ValueError('Age cannot be negative!').
How can you handle ValueError and ZeroDivisionError the same way in one except?
- except ValueError, ZeroDivisionError:
- except (ValueError, ZeroDivisionError):
- except ValueError or ZeroDivisionError:
- except [ValueError, ZeroDivisionError]:
Answer: except (ValueError, ZeroDivisionError):. Group multiple exception types in a tuple: except (ValueError, ZeroDivisionError):.
Continue this course
- Previous: File Handling
- Next: Modules and Packages — Organise code into reusable files and import the Python standard library
- Quick reference: Python cheat sheet › Exceptions
- From the blog: 10 Python Tips Every Beginner Should Know · Error Handling Best Practices in Python