Handle Errors with Try Except Finally
Learn to handle errors with try, except, and finally in Python. This lesson covers the core concept, step-by-step implementation, practical exercise, and common troubleshooting.
Focus: handle errors with try, except, and finally
Every Python program eventually hits an unexpected situation — a missing file, invalid user input, or a network timeout. Without a safety net, one bad data point can crash your whole script. In this lesson, you'll learn to handle errors with try, except, and finally — Python's structured way to catch and respond to runtime problems without ugly exit messages or lost work.
The problem this lesson solves
Imagine you write a simple calculator that divides two numbers provided by a user. If someone enters 10 and 0, Python raises a ZeroDivisionError and your program stops immediately:
x = 10
y = 0
result = x / y # raises ZeroDivisionError
print(result) # never reached
Output (stderr):
Traceback (most recent call last):
File "calc.py", line 3, in <module>
result = x / y
ZeroDivisionError: division by zero
This is ungraceful — it leaks internal details to the user and leaves the program in an unpredictable state. The same problem occurs with file operations (FileNotFoundError), network calls (ConnectionError), or parsing invalid data (ValueError). Without error handling, you can't build robust, user-friendly applications.
Core concept / mental model
Think of error handling as airbags for your code. Normal execution is the smooth road. When a collision happens (an exception), the airbags deploy — that's the except block. After the crash, the finally block is like the cleanup crew that always shows up, regardless of whether an accident occurred.
In Python, an exception is an object that represents an error or unexpected event. The try block holds the code that might fail. You attach one or more except blocks that specify how to handle specific exception types. Optionally, a finally block runs after everything else — perfect for releasing resources like file handles or network connections.
Pro tip: Python's exception hierarchy is large. Always catch the most specific exception first, then fall back to a general
Exceptionas a safety net.
How it works step by step
Basic structure of try/except/finally
try:
# Code that might raise an exception
risky_operation()
except SomeException:
# What to do if that specific exception occurs
handle_error()
finally:
# Always runs, even if an exception happens
cleanup()
Step 1 — The try block
Python executes every line inside try sequentially. If everything succeeds, it skips all except blocks and jumps to finally (if present).
Step 2 — An exception occurs
If an exception is raised inside try (either by Python or via raise), normal execution stops immediately. Python looks for a matching except block.
Step 3 — Exception matching
Python checks each except block in order. If the exception object is an instance of the listed exception class (or a subclass), that block runs. If no match is found, the exception propagates up the call stack and may crash the program.
Step 4 — The finally block
Regardless of what happened in try or except, the finally block always executes — even if you use return, break, or continue inside the try block.
Hands-on walkthrough
Example 1: Basic file read with error handling
def read_config(filename):
try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError:
print(f"Error: {filename} not found. Using defaults.")
return "default settings"
finally:
print("File read attempt complete.")
content = read_config("missing.yaml")
print("Content:", content)
Output:
Error: missing.yaml not found. Using defaults.
File read attempt complete.
Content: default settings
Example 2: Catching multiple exception types
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Division by zero is not allowed.")
result = None
except TypeError:
print("Both arguments must be numbers.")
result = None
else:
# Runs only if no exception occurred
print("Division succeeded.")
finally:
print("Operation finished.")
return result
print(safe_divide(10, 2))
print("---")
print(safe_divide(10, 0))
print("---")
print(safe_divide(10, "a"))
Output:
Division succeeded.
Operation finished.
5.0
---
Division by zero is not allowed.
Operation finished.
None
---
Both arguments must be numbers.
Operation finished.
None
Pro tip: Use
elseafter yourexceptblocks to run code that should execute only when the try block succeeds — it's cleaner than putting that code insidetryitself.
Example 3: Ensuring resource cleanup with finally
import sqlite3
def fetch_users():
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM users")
return cursor.fetchall()
except sqlite3.DatabaseError as e:
print(f"Database error: {e}")
return []
finally:
cursor.close()
conn.close()
print("Database connection closed.")
print(fetch_users())
Output (if file doesn't exist):
Database error: unable to open database file
Database connection closed.
[]
The finally block guarantees the connection is closed, even if the database doesn't exist.
Example 4: Raising and re-raising exceptions
def validate_age(age):
try:
age = int(age)
if age < 0:
raise ValueError("Age cannot be negative")
print(f"Valid age: {age}")
except ValueError as e:
print(f"Input error: {e}")
# re-raise after logging
raise
finally:
print("Validation complete.")
try:
validate_age("-5")
except ValueError:
print("Caught re-raised exception.")
Output:
Input error: Age cannot be negative
Validation complete.
Caught re-raised exception.
Compare options / when to choose what
| Use Case | Best Practice | Alternative/Why |
|---|---|---|
| Catching known errors (e.g., division by zero) | Specific except block | Bare except: swallows KeyboardInterrupt — avoid it |
| Logging all exceptions without crashing | try / except Exception / finally |
sys.excepthook for global handler |
| Temporary debugging | try / except with raise inside |
Use warnings.warn() for non-critical issues |
| Cleaning up resources (files, sockets) | Always use finally or context manager (with) |
with statement is cleaner when applicable |
Pro tip: Favor context managers (
with open(...) as f) over manualtry/finallyfor resource cleanup — they handlefinallyinternally.
Troubleshooting & edge cases
Mistake 1: Catching too broad an exception
try:
risky()
except: # bare except catches KeyboardInterrupt, SystemExit
pass
Fix: Catch Exception (or a specific type).
Mistake 2: Ignoring exception details
try:
x = int(inp)
except ValueError:
pass # silently swallows error
Fix: Log or display a meaningful message.
Mistake 3: Using finally to mask exceptions
def f():
try:
raise ValueError("original")
finally:
raise TypeError("mask")
Result: The TypeError replaces the original — you lose the root cause.
Mistake 4: Forgetting that finally runs even on return
def demo():
try:
return 1
finally:
print("I run before the return!")
Output: I run before the return! then function returns 1.
What you learned & what's next
You can now handle errors with try, except, and finally — protecting your programs from crashes, providing user-friendly messages, and guaranteeing cleanup. You understand the difference between specific and generic exception handling, and how to use else and finally effectively.
Next lesson: We'll explore custom exceptions — how to define your own error types to make your APIs more expressive and your debugging faster.
Practice recap
Mini challenge: Write a function safe_input(prompt, type_cast) that repeatedly asks for input until the user provides a valid value of the expected type. Use try/except inside a while loop to catch ValueError from type casting. Return the parsed value once successful.
Common mistakes
- Catching bare
except:— this catchesKeyboardInterruptandSystemExit, making programs impossible to stop gracefully. Always specifyExceptionor a concrete type. - Swallowing exceptions silently (empty
exceptblock) — you lose debugging information. At minimum, log the error or re-raise. - Putting too much code inside
try— only wrap the smallest region that may fail. This prevents unintended exception handling from unrelated bugs. - Assuming
exceptblock code never fails — if your cleanup code raises an exception, it can mask the original error.
Variations
- Use
contextlib.suppressto intentionally ignore specific exceptions without an emptyexceptblock:with suppress(FileNotFoundError): os.remove('temp'). - Use
sys.excepthookto set a global unhandled exception handler for all exceptions that reach the top level. - Use
tracebackmodule to capture and log full stack traces without crashing.
Real-world use cases
- Reading configuration files: catch
FileNotFoundErrorand fall back to default values instead of crashing the startup process. - Network API calls: use
try/exceptto catchConnectionErrorandTimeoutError, then retry or show a friendly error message. - Data validation in web forms: wrap
int()orfloat()conversions intry/except ValueErrorto return user-friendly validation errors.
Key takeaways
- Use
tryto guard code that may raise exceptions; only wrap the minimal risky section. - Catch specific exception types (e.g.,
FileNotFoundError) before the generalExceptionto avoid masking unexpected bugs. - Always include a
finallyblock for cleanup actions (closing files, releasing locks) — it runs even ifreturnorbreakis used. - Use
elseafterexceptblocks to execute code only when no exception occurred — keepstryclean. - Never use a bare
except:— you'll catchKeyboardInterruptandSystemExit, making your program unresponsive.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.