Skip to content

try / except / else / finally Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on try/except/else/finally, catching multiple exception types, why bare except is bad, finally with return, and exception chaining with raise from.

Read the in-depth guidePython try/except/else/finally Explained — Catching Exceptions the Right Way(opens in new tab)
15 of 15

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel