Skip to content
C Codeloom
Python

Python Error Handling with try and except

A practical guide to error handling in Python — catching specific exceptions, the else and finally clauses, raising and re-raising, custom exception types, and habits that lead to robust code.

·5 min read · By Codeloom
Beginner 10 min read

What you'll learn

  • The shape of try / except / else / finally
  • Catching specific exceptions instead of bare except
  • Raising and re-raising exceptions
  • Defining your own exception types
  • When to handle and when to let it crash

Prerequisites

  • Basic Python familiarity

Errors are not exotic events in Python — they are a normal part of program flow. Files don’t exist, networks drop, users type “five” into a number field. The try and except statements give you a precise vocabulary for saying which errors you expect, where you can recover, and which ones should bring the program down. Used well, they make code robust. Used poorly, they hide bugs forever.

The Basic Shape

try:
    value = int(user_input)
except ValueError:
    print("That wasn't a number, please try again.")

If int() raises ValueError, the except block runs. If it succeeds, the except block is skipped. Either way, execution continues after the try statement.

Catch Specific Exceptions

The single most important rule of error handling: catch the narrowest exception you can. A bare except: catches everything, including KeyboardInterrupt and SystemExit, which means Ctrl-C may stop working and bugs vanish without a trace.

# Don't do this
try:
    do_something()
except:
    pass

# Better
try:
    do_something()
except ValueError as e:
    log.warning("Skipping bad input: %s", e)

except Exception is almost as bad — it swallows every standard error type. Catch the specific exceptions your code might actually produce.

Catching Multiple Types

You can list several exception types in a tuple.

try:
    parse(payload)
except (ValueError, KeyError) as e:
    log.warning("Bad payload: %s", e)

Or you can use multiple except clauses for different recovery strategies.

try:
    response = fetch(url)
except TimeoutError:
    schedule_retry(url)
except ConnectionError:
    mark_offline(url)

else: When the try Succeeded

The else clause runs only if the try block did not raise. It is the right place for code that should run after a successful operation but that you don’t want to wrap in the try itself.

try:
    data = json.loads(payload)
except json.JSONDecodeError:
    log.warning("Invalid JSON")
else:
    process(data)  # only runs if loads() succeeded

Why bother? If process(data) accidentally raises json.JSONDecodeError too, the except clause would catch it incorrectly. Keeping the try body small protects against this.

finally: Always Run

The finally clause runs whether or not an exception happened — even if you return from inside the try. It is the standard place for cleanup.

f = open("data.txt")
try:
    process(f)
finally:
    f.close()

In practice, with statements (context managers) have replaced most uses of finally for resource cleanup, but finally is still useful when you can’t or don’t want to write a context manager.

Raising Exceptions

To signal an error, raise an exception with raise.

def withdraw(account, amount):
    if amount > account.balance:
        raise ValueError("Insufficient funds")
    account.balance -= amount

Choose a built-in exception type that matches the error: ValueError for bad arguments, TypeError for wrong types, KeyError for missing dict keys, FileNotFoundError for missing files, and so on.

Re-raising and Wrapping

Sometimes you want to catch an exception, do something useful, then let it propagate.

try:
    risky()
except ValueError:
    log.exception("risky() failed")
    raise  # re-raise the same exception

A bare raise inside an except block re-raises the current exception with its original traceback intact. This is almost always what you want.

If you need to convert an exception to a different type — for example, hiding a low-level driver error behind a friendly application error — chain them with raise ... from ....

try:
    row = db.query(sql)
except DriverError as e:
    raise DataAccessError("Lookup failed") from e

The from clause preserves the original cause in the traceback, so you don’t lose information.

Custom Exception Types

Defining your own exception types is cheap and dramatically improves error handling further up the stack.

class PaymentError(Exception):
    pass

class InsufficientFunds(PaymentError):
    pass

class CardDeclined(PaymentError):
    pass

Callers can now write except PaymentError to catch any payment problem, or except InsufficientFunds to handle just the one they know how to recover from. The hierarchy is the point.

Don’t Use Exceptions for Control Flow

Exceptions are slow on the raising path and confusing for readers. If a condition is part of normal flow — for example, checking whether a key exists — prefer if over try.

# Awkward
try:
    name = user["name"]
except KeyError:
    name = "guest"

# Clearer
name = user.get("name", "guest")

The general rule: use exceptions for exceptional cases. Use conditionals for things you expect to happen routinely.

Logging in except Blocks

Inside an except block, prefer log.exception("...") to log.error("..."). The former automatically attaches the traceback, which makes debugging dramatically easier.

try:
    work()
except Exception:
    log.exception("work() failed")
    raise

When to Let It Crash

The hardest part of error handling is knowing when not to. If you can’t actually recover from an error — say, your database connection is permanently broken — catching the exception just to log it and continue produces a silently broken program. Better to let it crash, get a clear traceback, and fix the cause.

The “catch and ignore” pattern is one of the biggest sources of mysterious bugs in real-world Python. Every except block should answer the question: what does this code do to make things better?

Wrapping Up

Good error handling is mostly about discipline: catch specific exceptions, keep try blocks short, use else and finally for clarity, raise meaningful types, and let the unfixable crash. With those habits, your code degrades gracefully where it can and fails loudly where it must.