Skip to main content

Python try/except Patterns

Error Handling

try/except/else/finally

Structure your exception handling using all four clauses for clarity.

The four clauses

A try statement has up to four parts: try runs first, except runs only if a matching exception was raised, else runs only if nothing was raised, and finally runs no matter what — even past a return. One function, both paths:

def divide(a, b):
try:
result = a / b
except ZeroDivisionError as e:
print(f"caught: {e}")
return None
else:
print("success path")
return result
finally:
print("cleanup runs either way")

print(divide(10, 2))
# success path
# cleanup runs either way
# 5.0

print(divide(10, 0))
# caught: division by zero
# cleanup runs either way
# None

Note that finally printed before each return value: Python finishes the finally block before the function actually returns. See ZeroDivisionError for the error itself.

In production code the same shape usually looks like this — log, re-raise, and clean up:

try:
payload = parse(raw)
except json.JSONDecodeError as exc:
logger.error("Invalid JSON: %s", exc)
raise
else:
process(payload)
finally:
clean_up()

Catch specific exceptions

A bare except: catches BaseException — including KeyboardInterrupt and SystemExit. This loop cannot be stopped with Ctrl-C:

import time

while True:
try:
time.sleep(1)
except: # also swallows Ctrl-C!
print("retrying...")

Catch the narrowest exception you can handle. If you genuinely need a catch-all, use except Exception: — it leaves Ctrl-C and interpreter shutdown alone.

Multiple except blocks

Handle different failures differently by stacking except clauses, most specific first. int() raises TypeError for wrong types and ValueError for unparseable strings:

def parse_port(value):
try:
return int(value)
except TypeError:
print(f"expected a string, got {type(value).__name__}")
except ValueError:
print(f"not a number: {value!r}")

parse_port(None) # expected a string, got NoneType
parse_port("abc") # not a number: 'abc'
parse_port("8080") # returns 8080

# same handler for both? group them in a tuple:
try:
port = int(value)
except (TypeError, ValueError) as e:
print(f"bad port: {e}")

The exception object

as e binds the exception instance so you can inspect it:

try:
int("abc")
except ValueError as e:
print(e) # invalid literal for int() with base 10: 'abc'
print(e.args) # ("invalid literal for int() with base 10: 'abc'",)
print(type(e).__name__) # ValueError

e only exists inside the except block — Python deletes the name when the block ends, so copy anything you need first.

What else is really for

else holds code that should run only on success but should not be protected by the handler. Keep the try block minimal — if follow-up code sits inside try, its own bugs get caught by accident:

try:
f = open("config.toml")
except FileNotFoundError:
config = {} # missing file is fine — use defaults
else:
with f:
config = parse(f.read()) # a bug in parse() is NOT silently eaten

Here only the open() call is guarded against FileNotFoundError; a parse() failure still surfaces loudly.

finally vs with

finally guarantees cleanup, but for resources that support it, a with statement says the same thing in less code:

f = open("data.txt")
try:
process(f)
finally:
f.close() # runs even if process() raises

# the same guarantee, shorter:
with open("data.txt") as f:
process(f)

Reach for finally when there is no context manager — releasing a lock you acquired conditionally, resetting global state, closing a connection from a library without with support. See file handling for more on with.

Logging and retries

logger.exception logs your message plus the full traceback — call it only inside an except block. Combined with a retry loop for transient failures:

import logging
import time

logger = logging.getLogger(__name__)

def fetch_with_retry(url, attempts=3):
for attempt in range(1, attempts + 1):
try:
return fetch(url)
except ConnectionError:
logger.exception("attempt %d/%d failed for %s", attempt, attempts, url)
if attempt == attempts:
raise # out of retries — let it propagate
time.sleep(2 ** attempt)

The final bare raise re-raises the original exception with its traceback intact, so callers still see a real failure instead of a silent None.

Frequently Asked Questions

What is the difference between "except Exception" and a bare "except:"?

A bare except catches BaseException, which includes KeyboardInterrupt (Ctrl-C) and SystemExit — so it can make your program unkillable and swallow normal shutdown. except Exception catches every ordinary error but lets those two propagate. If you need a catch-all, always write except Exception.

Does the order of except blocks matter?

Yes. Python checks them top to bottom and runs the first match. Because except Exception also matches ValueError, putting the general handler first makes the specific one unreachable — always order handlers from most specific to most general.

try:
int("abc")
except Exception:
print("generic handler wins")
except ValueError:
print("never runs")

Should I use try/except or check with if first?

Python idiom favors EAFP ("easier to ask forgiveness than permission"): just try the operation and handle the exception. It avoids race conditions (a file can vanish between the check and the open) and double lookups. Use if-checks (LBYL) when the failing case is common and cheap to test.

# LBYL
if "email" in row:
send(row["email"])

# EAFP (idiomatic)
try:
send(row["email"])
except KeyError:
pass

Next up in your learning path