A full statement has four parts. try holds the code that might fail.
except runs only if a matching exception was raised in try. else
runs only if try finished without raising — it's for the "success path"
code you don't want wrapped in the try. finally runs always, whether
there was an exception or not, and even if you return/break — it's for cleanup.
try:
conn = open_db() # might raise
except DBError as e:
log(e) # runs only on failure
else:
conn.commit() # runs only on success
finally:
conn.close() # always runs — cleanup
Putting the success-only code in else (instead of at the bottom of try) keeps
the try block narrow, so except only catches errors from the risky line.
Group related types in a tuple after except, or use separate except
clauses when each needs different handling. The as e binds the exception
instance, letting you inspect its message, args, or attributes.
try:
value = int(raw)
except (ValueError, TypeError) as e: # tuple -> one handler for both
print(f"bad input: {e}") # e is the exception object
except KeyError: # separate handler, different logic
print("missing key")
Clauses are checked top to bottom, and the first match wins — so order specific exceptions before their base classes. Use the tuple form when several types share handling; use separate clauses when they don't.
A bare except: (or except BaseException:) catches everything — including
KeyboardInterrupt and SystemExit, which are how the user Ctrl-Cs or the
program exits. It also swallows bugs like NameError or TypeError that you'd
rather see crash loudly, making problems invisible.
try:
do_work()
except: # too broad — even Ctrl-C is caught
pass # silent failure — hides real bugs
try:
do_work()
except Exception as e: # catches errors, not Ctrl-C / SystemExit
log.exception(e) # at least record what happened
Catch the narrowest exception you can actually handle. If you must catch
broadly, use except Exception (not bare), and always log rather than silently
pass.
finally always runs, even when try or except already has a return. If
finally itself returns (or raises), it overrides the pending return or
exception — the value from try/except is silently discarded. This is a classic
gotcha.
def f():
try:
return 1 # value queued...
finally:
return 2 # ...but finally's return wins
f() # 2, not 1
def g():
try:
raise ValueError
finally:
return "swallowed" # finally return suppresses the exception!
g() # "swallowed" — exception vanishes
Avoid return/break inside finally: it masks both return values and
exceptions. Keep finally for cleanup only.
A bare raise (no argument) inside an except block re-raises the current
exception, preserving its original traceback — useful for logging then letting it
propagate. To translate one error into another while keeping the cause, use
raise NewError from original, which sets __cause__ and shows both in the
traceback.
try:
parse(config)
except KeyError as e:
log.error("config invalid")
raise # re-raise same exception, original traceback
try:
value = data["port"]
except KeyError as e:
raise ConfigError("missing port") from e # explicit chaining
When an exception is raised inside an except block without from, Python
still links it implicitly via __context__ ("During handling of the above..."). Use
from e to make the cause explicit, or from None to suppress the chain.
The else block runs only if the try body raised nothing, and —
crucially — it is not protected by the except. This lets you keep the
risky line minimal in try and put the "on success" follow-up code in else,
so you don't accidentally catch exceptions from the wrong place.
try:
f = open(path)
except OSError:
handle()
else:
# runs only if open() succeeded; its errors aren't swallowed above
data = f.read()
f.close()
Rule of thumb: put only the operation that can fail in try, and the success
continuation in else.
To break a reference cycle: the exception holds a traceback that
references the frame, which references e. Python deletes e at the end of
the except block to let it be collected promptly. Accessing e afterward
raises NameError; copy what you need into another variable first.
try:
risky()
except ValueError as e:
err = str(e) # capture before the block ends
print(e) # NameError: name 'e' is not defined
print(err) # fine
Rule of thumb: don't use the as variable after its except block — save any
needed data into a separate name.
Yes. Clauses are tried top to bottom, and the first matching one
wins. A broad parent (Exception) before a specific child means the child
clause is unreachable. Always list specific exceptions first, broad ones
last.
try:
parse()
except ValueError: # specific first
...
except Exception: # general fallback last
...
Rule of thumb: order except clauses from most specific to most general.
Pass a tuple of types to a single except. They share one handler, and
as e still binds whichever occurred. Don't forget the parentheses — except A, B: is a syntax error in Python 3.
try:
load()
except (ValueError, KeyError, TypeError) as e:
log.warning("bad input: %s", e)
Rule of thumb: group exceptions you handle identically into one tuple-based except clause.
It sets the new exception's __cause__ to Y, producing a "The above
exception was the direct cause..." chained traceback. from None
suppresses the chain (__suppress_context__), useful when the original
error is noise. Without from, Python still shows the implicit context.
try:
int(x)
except ValueError as e:
raise ConfigError("invalid port") from e # explicit cause
raise ConfigError("invalid port") from None # hide the original
Rule of thumb: use from e when wrapping to preserve the real cause; from None to hide an irrelevant internal error.
Raising/handling an exception costs more than a normal return, but
setting up a try block is nearly free (especially with the zero-cost
try in 3.11+). So EAFP is cheap when failures are rare; it only loses to
LBYL when exceptions fire frequently in a hot loop.
# great when misses are rare:
try: return cache[k]
except KeyError: return slow(k)
# if misses are common, a membership check may be faster
Rule of thumb: don't fear try/except for the common-success path; reconsider only when the exception path is the frequent one.
The finally return wins — it overrides any return (or exception) from
the try/except. A return (or break) in finally even swallows a
pending exception, silently discarding it. This is a notorious bug source.
def f():
try:
return 1
finally:
return 2 # f() returns 2, the 1 is lost
def g():
try:
raise ValueError
finally:
return 0 # swallows the ValueError!
Rule of thumb: never return/break from finally — it hides results and
exceptions.
An exception propagates outward through the call stack until a matching
except catches it; unmatched, it keeps bubbling up. Inner finally clauses
run as the exception passes through them. If nothing catches it, it reaches
the top and prints a traceback.
def inner():
try:
raise ValueError
finally:
print("inner cleanup") # runs as it propagates
def outer():
try:
inner()
except ValueError:
print("caught in outer")
Rule of thumb: catch exceptions at the level that can actually handle them;
let finally handle cleanup at each layer in between.
Use assert only for internal invariants / debugging checks that
should never fail in correct code — they document assumptions. Don't use them
to validate external input or enforce logic, because assert is
stripped when Python runs with -O. Validate real conditions with an
explicit raise.
assert n >= 0, "internal: count went negative" # invariant
if not user_input: # external validation
raise ValueError("input required") # never an assert here
Rule of thumb: assert for "this can't happen" sanity checks; raise for
anything that depends on user input or runtime conditions.
Inside an except block use logging.exception(msg) (or
logger.error(msg, exc_info=True)), which records the message plus the full
traceback. Plain logging.error(str(e)) loses the traceback, making
debugging harder.
import logging
try:
process()
except Exception:
logging.exception("processing failed") # message + traceback
raise
Rule of thumb: use logging.exception inside except blocks so the traceback
is captured, not just the message.
More Errors & Exceptions interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.