skip to content

Handler Syntax Forms

How you spell an except clause decides what it catches and what escapes, and a return smuggled into finally decides what the function gives back. Interviewers read these spellings as habits.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

7

What does a bare `except:` catch that `except Exception` does not?

level: juniorimportance: must knowfreq 78%

answer

  1. The tree has two roots
  2. Ctrl-C travels as an exception
  3. Bare except is not except Exception
  4. Interrupt, exit, generator teardown sit outside
  5. Bare except: equals except BaseException:

basics

~10 s

A bare except: catches every BaseException subclass, including KeyboardInterrupt, SystemExit and GeneratorExit. except Exception stops one level lower, so Ctrl-C and an exit request still propagate. Catch Exception, or a narrower type, instead.

solid answer

~50 s

Python's exception tree is rooted at `BaseException`, not at `Exception`. Every error you would normally want to handle -- `ValueError`, `OSError`, `KeyError` -- lives under `Exception`. The other direct children of `BaseException` are `KeyboardInterrupt`, `SystemExit`, `GeneratorExit` and `BaseExceptionGroup`, and they sit outside `Exception` precisely so that ordinary handlers do not swallow them: they signal Ctrl-C, an explicit interpreter-exit request, and generator teardown, rather than a program error. A bare `except:` is exactly `except BaseException:`, so it catches all of them. In practice that means Ctrl-C stops working, `sys.exit()` inside the guarded block silently does nothing, and closing a generator can misbehave. Write `except Exception:` when you genuinely need a catch-all at a boundary, and a specific type everywhere else. Reserve `except BaseException:` for code that must run cleanup and then re-raise with a bare `raise`.

code

python · 10 lines
python
def risky():
    raise KeyboardInterrupt

try:
    try:
        risky()
    except Exception as exc:
        print("Exception handler saw", type(exc).__name__)
except KeyboardInterrupt:
    print("KeyboardInterrupt escaped except Exception")

go deeper

for a junior

Recall the one-line fact: a bare except: also catches Ctrl-C and exit requests, except Exception does not. Be able to name KeyboardInterrupt and SystemExit as the classes that live outside Exception.

for a middle

Explain the mechanics: the tree is rooted at BaseException, a bare clause is except BaseException:, and control signals were put outside Exception on purpose. Show the narrower handler you would write instead.

for a senior

Demonstrate the operational judgment: where a catch-all boundary is justified, how small the guarded block must be, and the cleanup-then-bare-raise shape for the rare case that must see BaseException.

for a principal

Own the systemic angle -- make the linter rule an error in CI so the bare form cannot land, and set the team convention for where catch-all boundaries are allowed to exist at all rather than relitigating it per review.

## Two roots, not one Every raisable object in Python inherits from `BaseException`. Directly beneath it sit five classes on 3.14: `Exception`, `KeyboardInterrupt`, `SystemExit`, `GeneratorExit` and `BaseExceptionGroup`. That split is deliberate design, not an accident of history. `Exception` is the root of *program errors* -- things your code did wrong or the world did to it: a bad value, a missing key, a closed socket. The other four are *control signals*: the interpreter or the user telling the program to stop or unwind, expressed through the same raise/propagate machinery because that machinery already knows how to run every `finally` block on the way out. You can check the shape yourself: ```pycon >>> [c.__name__ for c in BaseException.__subclasses__()] ['BaseExceptionGroup', 'Exception', 'GeneratorExit', 'KeyboardInterrupt', 'SystemExit'] >>> issubclass(KeyboardInterrupt, Exception) False ``` ## What the two clauses compile to `except Exception:` catches `Exception` and everything under it. A bare `except:` has no type at all and catches whatever propagates -- it behaves exactly like `except BaseException:`. The two spellings are interchangeable in effect; the bare form is merely shorter and, for that reason, far more common in code written in a hurry. ## Why the difference bites Three concrete consequences follow, and interviewers ask about the first one. **Ctrl-C stops working.** When you press Ctrl-C, the interpreter raises `KeyboardInterrupt` in the main thread. If a loop body is wrapped in a bare `except:` that logs and continues, the interrupt is caught like any other error and the loop keeps going. The operator presses Ctrl-C harder, sees a log line each time, and eventually reaches for a kill signal. **An exit request is swallowed.** `sys.exit()` works by raising `SystemExit`. Called inside a bare-except block, it is caught and discarded, and execution simply carries on past the point where the author believed the program had stopped. **Generator teardown is corrupted.** When a suspended generator is closed, `GeneratorExit` is thrown in at the suspension point. Code that catches it and refuses to finish turns a clean shutdown into a `RuntimeError`. A fourth case matters for async code: `asyncio.CancelledError` has inherited from `BaseException` since 3.8, exactly so that a blanket `except Exception:` cannot eat a cancellation. That is only a protection if you do not then write a bare `except:` around the awaited call. ## The rule of thumb The narrower the type, the better the handler. Prefer the specific class you can actually recover from: ```python try: rate = rates[currency] except KeyError: rate = fetch_rate(currency) ``` Widen to `except Exception:` only where a boundary genuinely needs one: the body of a worker loop that must not die because one item is bad, a request handler, a plugin call. Even there, keep the guarded block small, and record the failure with `logging.exception` so the traceback survives. Never write a bare `except:` where you meant `except Exception:`. There is no case where the extra breadth is what you wanted by accident. ## When catching BaseException is correct There is a legitimate pattern, and it always ends in a re-raise: ```python try: do_work() except BaseException: flush_partial_results() raise ``` Here you are not *handling* the interrupt, you are running cleanup on the way past it. The bare `raise` re-raises the exception being handled, so Ctrl-C still terminates the program and the exit request still exits. If your handler does not end in `raise`, catching `BaseException` is a bug. `contextlib.suppress` is the safe way to say "ignore this specific failure": ```python from contextlib import suppress with suppress(FileNotFoundError): remove_stale_cache_file() ``` It names the types explicitly, so it cannot accidentally swallow an interrupt. ## Version notes The `BaseException`/`Exception` split is stable across all of Python 3. Two additions are worth knowing: `asyncio.CancelledError` moved to `BaseException` in 3.8, and 3.11 (PEP 654) added `BaseExceptionGroup` as a direct child of `BaseException`, with `ExceptionGroup` under `Exception`. So a group carrying only ordinary errors is catchable by `except Exception:`, while a group that may carry a `KeyboardInterrupt` deliberately is not. ## Spotting it Every mainstream Python linter flags the bare form -- the rule is usually named for "bare except" or "blind except" -- and turning it into a CI error is a two-minute fix that removes the whole class of bug from a codebase.

  • Where is `except Exception:` genuinely the right choice?
    At a boundary that must survive one bad item: the body of a worker loop, a request handler, a call into plugin code. Keep the guarded block as small as possible, record the failure with `logging.exception` so the traceback is preserved, and make sure the loop can still be stopped -- which it can, because `except Exception:` lets `KeyboardInterrupt` and `SystemExit` through.
  • Is there ever a reason to catch `BaseException` deliberately?
    Yes, when you must run cleanup on the way out -- flush a partial batch, release an external resource -- and the handler ends in a bare `raise`. You are passing the exception along, not handling it. A `BaseException` handler that ends in `pass`, `continue` or a `return` is a bug, because it converts Ctrl-C and an exit request into a silent no-op.
  • How does `contextlib.suppress(FileNotFoundError)` compare with wrapping the same block in a bare `except:`?
    `contextlib.suppress` names the exact types it will swallow, so anything else -- including `KeyboardInterrupt` -- still propagates, and the intent is readable at the call site. A bare `except:` swallows everything and hides that intent. Use `suppress` for the narrow 'this failure is genuinely fine' case, and never as a general silencer.

Exception is the building's fire alarm; BaseException also carries the evacuation order from the fire chief. A handler that silences everything silences the order to leave.

saying these in an interview costs you the question

  • Says bare except: and except Exception behave identically
  • Believes KeyboardInterrupt is a subclass of Exception
  • Wraps flaky code in bare except: followed by pass
  • Assumes Ctrl-C always reaches the interpreter regardless of handlers
  • Cannot name a BaseException subclass outside Exception
  • Thinks catching BaseException is fine as long as you log it

context

open as a page

In Python, what does a `return` inside a `finally` block do to the value the `try` block was returning?

level: juniorimportance: must knowfreq 52%

basics

~20 s

A return inside finally overrides everything already in flight: the try block's return value, a pending break or continue, and even an exception on its way out. The function returns the finally value, and the exception disappears silently.

open as a page

A CLI wrapped in `except BaseException` finishes normally despite calling sys.exit(1) -- what is the handler catching?

level: middleimportance: should knowfreq 44%

basics

~20 s

It is catching SystemExit. sys.exit(1) does not end the process directly -- it raises SystemExit, which derives from BaseException rather than Exception, so a bare except: or except BaseException intercepts the exit request and execution simply continues.

open as a page

Why does a `break` or `continue` inside a Python `finally` block silently swallow an exception?

level: middleimportance: should knowfreq 30%

basics

~20 s

A finally block runs while the exception is still in flight and re-raises it only when the block ends normally. A break or continue leaves the block early, so the held exception is dropped and the loop simply carries on.

open as a page

A reconciliation retry loop catching BaseException ignores Ctrl-C -- how do you diagnose and fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The loop's handler catches BaseException, so KeyboardInterrupt is treated as a retryable failure and each Ctrl-C only aborts the current attempt. Narrow the handler to Exception, or keep it broad, run cleanup, and end it with a bare raise.

open as a page

In a log-ingest worker, a `finally` block's flush raises while a parse error is already propagating — which error reaches the caller?

level: seniorimportance: should knowfreq 27%

basics

~20 s

The cleanup error wins. The exception raised inside finally propagates, and the parse error survives only as its context, printed under "During handling of the above exception". Upstream handlers keyed on the original type stop firing.

open as a page

What does PEP 758 change about `except` clauses in Python 3.14?

level: middleimportance: nice to knowfreq 12%

basics

~10 s

Python 3.14 allows several exception types in an except or except* clause without parentheses, as in except ValueError, TypeError:. Parentheses are still required when the clause also binds a name with as.

open as a page