Skip to content
Topics
Python
Python Try Except: How to Handle Exceptions the Right Way

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 / 0

Output:

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    result = 10 / 0
ZeroDivisionError: division by zero

Exceptions 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 zero

Catching 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 str

FileNotFoundError

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: 42

The 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 = None

The 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")
BlockWhen It RunsPurpose
tryAlways (first)Contains the code that might raise an exception
exceptOnly when a matching exception occursHandles the error
elseOnly when no exception occurs in tryRuns the success-path logic
finallyAlways (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.

ExceptionWhen It OccursExample
ExceptionBase class for most exceptionsParent of all non-system-exiting exceptions
ValueErrorWrong value for the correct typeint("abc")
TypeErrorWrong type for an operation"text" + 5
KeyErrorMissing dictionary keyd["missing"]
IndexErrorList index out of range[1,2,3][10]
FileNotFoundErrorFile does not existopen("no.txt")
ZeroDivisionErrorDivision or modulo by zero1 / 0
AttributeErrorObject lacks the attributeNone.append(1)
ImportErrorModule import failsimport nonexistent
OSErrorOS-level operation failsDisk full, permission denied
StopIterationIterator has no more itemsnext() on exhausted iterator
RuntimeErrorGeneric runtime errorCatch-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 str

You 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 ZeroDivisionError

Custom 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 @ symbol

Custom 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.

ApproachCatchesRisk Level
except:Everything including SystemExitVery High
except Exception:All standard exceptionsHigh
except ValueError:Only ValueErrorLow
except (ValueError, TypeError):Two specific typesLow

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 occurs

Real-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 results

Try/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"
Criteriaif/else (LBYL)try/except (EAFP)
Best whenThe check is cheap and the error is commonThe 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)SameSlower (exception creation has overhead)
Race conditionsPossible (state can change between check and use)None (atomic operation)
ReadabilityClear for simple conditionsBetter for operations that can fail in multiple ways
File operationsif os.path.exists(path) -- file could be deleted between check and opentry: open(path) -- handles the actual failure
Dictionary accessif key in dict -- simple and fasttry: 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.

📚