Python Try Except: How to Handle Exceptions the Right Way
Updated on
Your Python script reads a file, parses the data, and sends it to an API. It works perfectly on your machine. Then it runs on a server where the file path is wrong, the JSON is malformed, or the network is down. The program crashes with a traceback, and the entire pipeline stops. This is the problem that try/except solves. Instead of letting errors kill your program, you catch them, handle them, and keep running.
What Are Exceptions in Python?
An exception is an event that disrupts the normal flow of a program. When Python encounters an operation it cannot perform -- dividing by zero, accessing a missing dictionary key, opening a file that does not exist -- it creates an exception object and stops execution. If nothing catches that exception, the program terminates and prints a traceback.
# This crashes the program
result = 10 / 0Output:
Traceback (most recent call last):
File "example.py", line 2, in <module>
result = 10 / 0
ZeroDivisionError: division by zeroExceptions are different from syntax errors. A syntax error means Python cannot parse your code at all. An exception happens during execution, after the code has been parsed successfully.
Basic Try/Except Syntax
The try/except block lets you attempt code that might fail and define what happens if it does.
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero.")Output:
Cannot divide by zero.The program does not crash. Python executes the code inside try. When ZeroDivisionError occurs, execution jumps to the except block. Everything after the try/except continues normally.
You can also capture the exception object to inspect the error message:
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error: {e}")Output:
Error: division by zeroCatching Specific Exceptions
Python has dozens of built-in exception types. Catching the right one makes your code precise. Here are the most common exceptions you will handle.
ValueError
Raised when a function receives an argument with the right type but an inappropriate value.
try:
number = int("not_a_number")
except ValueError as e:
print(f"Invalid value: {e}")Output:
Invalid value: invalid literal for int() with base 10: 'not_a_number'TypeError
Raised when an operation is applied to an object of the wrong type.
try:
result = "hello" + 42
except TypeError as e:
print(f"Type error: {e}")Output:
Type error: can only concatenate str (not "int") to strFileNotFoundError
Raised when you try to open a file that does not exist.
try:
with open("nonexistent_file.txt", "r") as f:
content = f.read()
except FileNotFoundError:
print("File not found. Check the file path.")KeyError
Raised when you access a dictionary key that does not exist.
data = {"name": "Alice", "age": 30}
try:
email = data["email"]
except KeyError as e:
print(f"Missing key: {e}")Output:
Missing key: 'email'IndexError
Raised when you access a list index that is out of range.
items = [10, 20, 30]
try:
value = items[5]
except IndexError:
print("Index out of range.")Multiple Except Blocks
You can handle different exception types with separate except blocks. Python checks them in order and executes the first match.
def parse_config(raw_value):
try:
parts = raw_value.split(":")
key = parts[0]
value = int(parts[1])
return {key: value}
except IndexError:
print("Config format error: missing colon separator.")
except ValueError:
print("Config format error: value is not a number.")
except AttributeError:
print("Config format error: input is not a string.")
parse_config("timeout:30") # Returns {'timeout': 30}
parse_config("timeout") # Config format error: missing colon separator.
parse_config("timeout:abc") # Config format error: value is not a number.
parse_config(12345) # Config format error: input is not a string.You can also catch multiple exceptions in a single except block using a tuple:
try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Invalid input: {e}")The Else Block
The else block runs only when no exception occurs in the try block. It separates the "happy path" code from the error-handling code.
try:
number = int("42")
except ValueError:
print("That is not a valid number.")
else:
print(f"Successfully parsed: {number}")Output:
Successfully parsed: 42The else block is useful because any exception raised inside it is not caught by the preceding except blocks. This prevents accidentally silencing bugs in your success-path code.
filename = "data.txt"
try:
f = open(filename, "r")
except FileNotFoundError:
print(f"File '{filename}' does not exist.")
else:
content = f.read()
f.close()
print(f"Read {len(content)} characters.")If open() fails, the except block handles it. If open() succeeds, the else block reads the file. Any error during f.read() is not caught here -- it propagates up, which is the correct behavior.
The Finally Block
The finally block always runs, whether an exception occurred or not. It is the right place for cleanup code: closing files, releasing locks, disconnecting from databases.
f = None
try:
f = open("data.txt", "r")
content = f.read()
except FileNotFoundError:
print("File not found.")
finally:
if f is not None:
f.close()
print("File handle closed.")The finally block runs even if the try block returns a value or raises an unhandled exception.
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
finally:
print("Division attempted.")
result = divide(10, 3)
# Output: Division attempted.
# result = 3.3333333333333335
result = divide(10, 0)
# Output: Division attempted.
# result = NoneThe Full Try/Except/Else/Finally Pattern
Here is how all four blocks work together:
import json
def load_config(filepath):
"""Load and parse a JSON config file."""
f = None
try:
f = open(filepath, "r")
data = json.load(f)
except FileNotFoundError:
print(f"Config file '{filepath}' not found.")
return {}
except json.JSONDecodeError as e:
print(f"Invalid JSON in '{filepath}': {e}")
return {}
else:
print(f"Config loaded successfully with {len(data)} keys.")
return data
finally:
if f is not None:
f.close()
print("File handle released.")
config = load_config("settings.json")| Block | When It Runs | Purpose |
|---|---|---|
try | Always (first) | Contains the code that might raise an exception |
except | Only when a matching exception occurs | Handles the error |
else | Only when no exception occurs in try | Runs the success-path logic |
finally | Always (last) | Cleanup code that must run regardless |
Common Built-in Exceptions Reference
Python includes a hierarchy of built-in exceptions. Here are the ones you will encounter most often.
| Exception | When It Occurs | Example |
|---|---|---|
Exception | Base class for most exceptions | Parent of all non-system-exiting exceptions |
ValueError | Wrong value for the correct type | int("abc") |
TypeError | Wrong type for an operation | "text" + 5 |
KeyError | Missing dictionary key | d["missing"] |
IndexError | List index out of range | [1,2,3][10] |
FileNotFoundError | File does not exist | open("no.txt") |
ZeroDivisionError | Division or modulo by zero | 1 / 0 |
AttributeError | Object lacks the attribute | None.append(1) |
ImportError | Module import fails | import nonexistent |
OSError | OS-level operation fails | Disk full, permission denied |
StopIteration | Iterator has no more items | next() on exhausted iterator |
RuntimeError | Generic runtime error | Catch-all for miscellaneous failures |
All of these inherit from BaseException. In practice, you should catch Exception or its subclasses, never BaseException directly (which includes SystemExit and KeyboardInterrupt).
Raising Exceptions with raise
You can raise exceptions explicitly when your code detects invalid conditions. This is how you enforce preconditions and signal errors to the caller.
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 > 150:
raise ValueError(f"Age must be between 0 and 150, got {age}")
return age
# Valid usage
print(set_age(25)) # 25
# Invalid usage
try:
set_age(-5)
except ValueError as e:
print(e) # Age must be between 0 and 150, got -5
try:
set_age("thirty")
except TypeError as e:
print(e) # Age must be an integer, got strYou can also re-raise the current exception inside an except block using raise without arguments. This is useful when you want to log an error and then let it propagate.
import logging
try:
result = 10 / 0
except ZeroDivisionError:
logging.error("Division by zero encountered")
raise # re-raises the original ZeroDivisionErrorCustom Exception Classes
For larger projects, define your own exception classes. Custom exceptions make error handling more readable and let callers catch specific failure modes.
class ValidationError(Exception):
"""Raised when input data fails validation."""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"Validation failed on '{field}': {message}")
class DatabaseConnectionError(Exception):
"""Raised when the database connection fails."""
pass
# Usage
def validate_email(email):
if "@" not in email:
raise ValidationError("email", "Must contain @ symbol")
if "." not in email.split("@")[1]:
raise ValidationError("email", "Domain must contain a dot")
return email
try:
validate_email("userexample.com")
except ValidationError as e:
print(e) # Validation failed on 'email': Must contain @ symbol
print(e.field) # email
print(e.message) # Must contain @ symbolCustom exceptions should inherit from Exception, not BaseException. Group related exceptions under a common base class to let callers catch broad or narrow categories:
class AppError(Exception):
"""Base exception for this application."""
pass
class ConfigError(AppError):
pass
class NetworkError(AppError):
pass
# Caller can catch all app errors or specific ones
try:
raise NetworkError("Connection timed out")
except AppError as e:
print(f"Application error: {e}")Best Practices for Python Exception Handling
1. Never Catch Bare Exceptions
A bare except: catches everything, including KeyboardInterrupt and SystemExit. This hides bugs and makes debugging a nightmare.
# BAD - catches everything, hides real bugs
try:
do_something()
except:
pass
# GOOD - catches specific exceptions
try:
do_something()
except ValueError as e:
logging.warning(f"Invalid value: {e}")If you must catch a broad range, use except Exception instead. This still lets KeyboardInterrupt and SystemExit propagate.
2. Keep Try Blocks Small
Put only the code that might raise the exception inside the try block. Large try blocks make it unclear which line caused the error.
# BAD - too much code in try
try:
data = load_data()
cleaned = clean_data(data)
result = analyze(cleaned)
save_results(result)
except Exception as e:
print(f"Something failed: {e}")
# GOOD - narrow try blocks
data = load_data()
cleaned = clean_data(data)
try:
result = analyze(cleaned)
except ValueError as e:
print(f"Analysis failed: {e}")
result = default_result()
save_results(result)3. Log Exceptions, Do Not Silence Them
Swallowing exceptions with pass creates invisible bugs. Always log or report the error.
import logging
try:
process_record(record)
except ValueError as e:
logging.error(f"Failed to process record {record['id']}: {e}")4. Use Specific Exception Types
Catch the most specific exception type possible. This prevents accidentally handling errors you did not anticipate.
| Approach | Catches | Risk Level |
|---|---|---|
except: | Everything including SystemExit | Very High |
except Exception: | All standard exceptions | High |
except ValueError: | Only ValueError | Low |
except (ValueError, TypeError): | Two specific types | Low |
5. Clean Up Resources in Finally or Use Context Managers
For file handles, database connections, and locks, always use finally or (better yet) a with statement.
# Prefer context managers for resource cleanup
with open("data.txt", "r") as f:
content = f.read()
# File is automatically closed, even if an exception occursReal-World Examples
Reading and Parsing a JSON File
import json
def read_json_config(filepath):
"""Read a JSON configuration file with proper error handling."""
try:
with open(filepath, "r") as f:
config = json.load(f)
except FileNotFoundError:
print(f"Config file not found: {filepath}")
return None
except PermissionError:
print(f"No permission to read: {filepath}")
return None
except json.JSONDecodeError as e:
print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
return None
else:
print(f"Loaded config with keys: {list(config.keys())}")
return config
config = read_json_config("app_config.json")
if config:
db_host = config.get("database_host", "localhost")Making HTTP API Calls
import urllib.request
import urllib.error
import json
def fetch_user(user_id):
"""Fetch user data from an API with retry logic."""
url = f"https://jsonplaceholder.typicode.com/users/{user_id}"
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
with urllib.request.urlopen(url, timeout=5) as response:
data = json.loads(response.read().decode())
return data
except urllib.error.HTTPError as e:
if e.code == 404:
print(f"User {user_id} not found.")
return None
print(f"HTTP error {e.code} on attempt {attempt}")
except urllib.error.URLError as e:
print(f"Network error on attempt {attempt}: {e.reason}")
except json.JSONDecodeError:
print("API returned invalid JSON.")
return None
print(f"All {max_retries} attempts failed.")
return None
user = fetch_user(1)
if user:
print(f"Found user: {user['name']}")Processing CSV Data
import csv
def process_sales_data(filepath):
"""Process a CSV file with robust error handling."""
results = []
try:
with open(filepath, "r", newline="") as f:
reader = csv.DictReader(f)
for row_num, row in enumerate(reader, start=2):
try:
amount = float(row["amount"])
quantity = int(row["quantity"])
results.append({
"product": row["product"],
"total": amount * quantity,
})
except KeyError as e:
print(f"Row {row_num}: Missing column {e}")
except ValueError as e:
print(f"Row {row_num}: Invalid number - {e}")
except FileNotFoundError:
print(f"File not found: {filepath}")
except PermissionError:
print(f"Cannot read file: {filepath}")
return resultsTry/Except vs If/Else: When to Use Each
Python follows the EAFP principle: "Easier to Ask Forgiveness than Permission." This contrasts with the LBYL approach: "Look Before You Leap."
# LBYL (Look Before You Leap) - using if/else
if "email" in user_data:
email = user_data["email"]
else:
email = "unknown"
# EAFP (Easier to Ask Forgiveness) - using try/except
try:
email = user_data["email"]
except KeyError:
email = "unknown"| Criteria | if/else (LBYL) | try/except (EAFP) |
|---|---|---|
| Best when | The check is cheap and the error is common | The error is rare or the check is expensive |
| Performance (no error) | Slightly slower (extra check every time) | Slightly faster (no check overhead) |
| Performance (error occurs) | Same | Slower (exception creation has overhead) |
| Race conditions | Possible (state can change between check and use) | None (atomic operation) |
| Readability | Clear for simple conditions | Better for operations that can fail in multiple ways |
| File operations | if os.path.exists(path) -- file could be deleted between check and open | try: open(path) -- handles the actual failure |
| Dictionary access | if key in dict -- simple and fast | try: dict[key] -- or just use dict.get(key, default) |
Use try/except when:
- The failure case is rare (exceptions are optimized for the "no error" path).
- The check itself is as expensive as the operation (e.g., checking if a file exists, then opening it).
- Multiple things can go wrong in a sequence of operations.
- Race conditions are a concern (especially with files and network resources).
Use if/else when:
- The condition is cheap to check and failures are common.
- You are validating user input before processing.
- The logic reads more clearly as a conditional.
Debugging Exceptions Faster with RunCell
When you work in Jupyter notebooks, exceptions can break your analysis flow. You hit a KeyError in row 50,000 of a DataFrame, or a TypeError surfaces three cells deep in a pipeline. Tracking down the root cause means scrolling through tracebacks and inspecting variables manually.
RunCell (opens in a new tab) is an AI agent that runs directly inside Jupyter. It reads the full traceback, inspects the variables in your current scope, and suggests a fix in context. Here is how it helps with exception handling:
- Traceback analysis. RunCell parses the exception chain and pinpoints which variable or operation caused the failure, even in nested function calls.
- Fix suggestions. Instead of searching Stack Overflow, RunCell generates a corrected code cell that you can run immediately. It knows whether to add a
try/except, fix a type conversion, or handle a missing key. - Preventive checks. RunCell can scan your code and flag operations that are likely to raise exceptions -- like accessing dictionary keys without
.get(), or dividing without checking for zero -- before you run the cell.
Since RunCell operates inside your existing Jupyter environment, it has access to your actual data and variables. The suggestions it provides are specific to your situation, not generic advice.
FAQ
What is the difference between try/except and try/catch in Python?
Python uses try/except, not try/catch. The try/catch syntax belongs to languages like Java, JavaScript, and C++. In Python, the keyword is except. The functionality is the same: you attempt code that might fail and define a handler for the failure case.
Can I use multiple except blocks in Python?
Yes. You can chain as many except blocks as you need, each catching a different exception type. Python evaluates them in order and executes the first matching block. You can also catch multiple exceptions in one block using a tuple: except (ValueError, TypeError) as e:.
When should I use else with try/except?
Use the else block when you have code that should only run if the try block succeeded. The main benefit is that exceptions raised in the else block are not caught by the preceding except blocks, preventing you from accidentally silencing unrelated errors.
Does finally always execute in Python?
Yes. The finally block executes whether the try block completed normally, raised a handled exception, or raised an unhandled exception. It even runs if the try or except block contains a return statement. The only exceptions are if the Python process is killed externally or os._exit() is called.
How do I create custom exceptions in Python?
Create a new class that inherits from Exception. You can add custom attributes and override the __init__ method. For example: class MyError(Exception): pass. For more complex cases, add fields like error codes or context data. Always inherit from Exception, not BaseException.
Conclusion
Python's try/except is the standard mechanism for handling runtime errors. It lets you catch specific exceptions, run cleanup code, and keep your programs stable when things go wrong. The full pattern -- try/except/else/finally -- covers every scenario: attempt the operation, handle failures, run success logic, and clean up resources.
The key principles are straightforward. Catch specific exceptions, not broad ones. Keep your try blocks small. Always log or report errors instead of silencing them. Use finally or context managers for cleanup. Raise meaningful exceptions in your own code with clear error messages.
Whether you are reading files, making API calls, or processing user input, proper exception handling is the difference between a script that crashes at 2 AM and one that logs the error and keeps running. Start with the basics -- a simple try/except around code that might fail -- and build up to custom exception hierarchies as your projects grow.