A script was hanging in production. Wouldn't respond to Ctrl+C. We had to kill -9 the process. Took us 20 minutes to figure out why.

The culprit:

try:
    do_something()
except:
    pass

That bare except: was catching KeyboardInterrupt. The script literally couldn't be stopped gracefully. Someone thought they were being defensive. They created a zombie.

Never Use Bare except:

This is the one rule I'd tattoo on every Python developer if I could. except: catches everything, including:

At minimum, catch Exception instead. It skips the system-level stuff:

# ❌ Catches everything including Ctrl+C
try:
    risky_operation()
except:
    print("Something went wrong")

# ✅ Catches application errors, lets system signals through
try:
    risky_operation()
except Exception as e:
    print(f"Error: {e}")

Even better: catch specific exceptions when you know what might go wrong.

try:
    with open(filepath) as f:
        data = f.read()
except FileNotFoundError:
    print(f"File not found: {filepath}")
except PermissionError:
    print(f"No permission to read: {filepath}")

The finally Block

finally runs no matter what—whether the code succeeds, throws an error, or even returns early. It's for cleanup.

def read_file(path):
    f = None
    try:
        f = open(path)
        return f.read()
    except FileNotFoundError:
        return ""
    finally:
        if f:
            f.close()  # This runs even after the return!

But honestly, for file handling, just use with. It handles the cleanup automatically:

def read_file(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        return ""

finally is more useful for things like database connections, locks, or temporary state that needs resetting.

Custom Exceptions

As your codebase grows, generic exceptions become confusing. Is this ValueError from invalid user input or a bug in my code?

Custom exceptions make intent clear:

class PaymentError(Exception):
    """Base class for payment-related errors"""
    pass

class InsufficientFundsError(PaymentError):
    pass

class CardDeclinedError(PaymentError):
    pass

def process_payment(amount, balance):
    if amount > balance:
        raise InsufficientFundsError(f"Need {amount}, only have {balance}")
    # process...

# Calling code
try:
    process_payment(100, 50)
except InsufficientFundsError:
    show_message("Please add funds to your account")
except CardDeclinedError:
    show_message("Card was declined, try another")
except PaymentError:
    show_message("Payment failed, please try again")

The hierarchy lets you catch specific errors or broad categories depending on what you need.

EAFP vs LBYL

Two philosophies for handling potential errors:

LBYL (Look Before You Leap): Check if something will work before trying it.

import os
if os.path.exists(filepath):
    with open(filepath) as f:
        data = f.read()

EAFP (Easier to Ask Forgiveness than Permission): Just try it and handle the error.

try:
    with open(filepath) as f:
        data = f.read()
except FileNotFoundError:
    data = None

Python culture prefers EAFP. Why? Because between checking and acting, things can change (race conditions). Also, in the success case, you avoid the overhead of the check.

But use judgment. If failure is expected most of the time, checking first might be cleaner than constantly catching exceptions.

Logging Exceptions Properly

When catching exceptions, log them properly:

import logging

try:
    do_something_risky()
except Exception as e:
    # ❌ Loses the stack trace
    logging.error(f"Error: {e}")
    
    # ✅ Includes full stack trace
    logging.exception("Something went wrong")

logging.exception() automatically includes the traceback. When you're debugging at 3am, you'll be grateful for those stack traces.

When to Let It Crash

Not every error needs catching. Sometimes crashing with a clear traceback is better than limping along in a broken state.

Catch errors when you can actually do something useful: retry, fall back to a default, show a user-friendly message. If you're just going to log and re-raise, maybe don't catch it at all.

# ❌ Pointless
try:
    do_thing()
except Exception as e:
    logging.error(e)
    raise  # Why bother catching?

# Just let it bubble up naturally

Exceptions exist to communicate problems. Don't silence them unless you have a good reason.

← Back to Python Articles

Back to Home