skip to content

questions

4

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

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

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

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