Skip to content

except Exception is not except:

Level: 201 · for Python programmers

One line: except Exception catches every error a program can recover from and lets KeyboardInterrupt and SystemExit through, because those two sit beside Exception under BaseException rather than below it — so a bare except: swallows Ctrl-C and cancels sys.exit(), the first matching clause wins even when a better one follows, and the name after as is gone when the block ends.

issubclass(ValueError, Exception)          # True
issubclass(KeyboardInterrupt, Exception)   # False
issubclass(SystemExit, Exception)          # False

Everything that can be raised in Python descends from BaseException, and directly under it are four classes. Exception is the root of every error a program might handle: ValueError, OSError, KeyError, your own. The other three are not errors. KeyboardInterrupt is the user asking the program to stop, SystemExit is the program asking to stop, and GeneratorExit is a generator being told to finish. They were put beside Exception rather than under it so that a handler written for errors would not intercept a request to stop, and that is exactly the difference between except Exception: and a bare except:, which means except BaseException: and intercepts all of it.

The rest of the page is what except does with the object once it has matched: the order clauses are tried in, what happens to the as name, and the links one exception keeps to another.

The run

Verified output of except_exception_is_not_except_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. THE TREE HAS A ROOT ABOVE Exception
     [c.__name__ for c in ValueError.__mro__]         ['ValueError', 'Exception', 'BaseException', 'object']
     [c.__name__ for c in KeyboardInterrupt.__mro__]  ['KeyboardInterrupt', 'BaseException', 'object'] no Exception on the way up
     issubclass(KeyboardInterrupt, Exception)         False
     issubclass(SystemExit, Exception)                False                            sys.exit() raises this one
     issubclass(GeneratorExit, Exception)             False                            and a closed generator gets this one

2. except Exception LETS Ctrl-C THROUGH; A BARE except DOES NOT
     try:                                             escaped: KeyboardInterrupt
         raise KeyboardInterrupt
     except Exception:
         outcome = 'caught'
     try:
         raise KeyboardInterrupt
     except:
         outcome = 'a bare except caught it'
     outcome                                          'a bare except caught it'
     try:
         raise KeyboardInterrupt
     except BaseException:
         outcome = 'so does BaseException, by name'
     outcome                                          'so does BaseException, by name'

3. sys.exit() IS AN EXCEPTION, AND A BARE except CANCELS THE EXIT
     try:
         sys.exit(3)
     except SystemExit as e:
         code = e.code
     code                                             3                                sys.exit() raised; nothing exited
     try:
         sys.exit(3)
     except:
         swallowed = True
     swallowed                                        True                             the program that meant to stop is still running

4. THE FIRST MATCHING CLAUSE WINS
     try:
         int('x')
     except Exception:
         handler = 'Exception'
     except ValueError:
         handler = 'ValueError'
     handler                                          'Exception'                      the specific clause below it can never run
     issubclass(ValueError, Exception)                True                             because this is true

5. THE as-NAME IS DELETED WHEN THE BLOCK ENDS
     try:
         1 / 0
     except ZeroDivisionError as err:
         kind = type(err).__name__
     kind                                             'ZeroDivisionError'
     err                                              NameError                        unbound: the block deleted it on the way out
     try:
         1 / 0
     except ZeroDivisionError as err:
         saved = err
     type(saved).__name__                             'ZeroDivisionError'              keep it by binding another name

6. AN EXCEPTION IS AN OBJECT, AND raise from IS A LINK
     try:
         try:
             {}['k']
         except KeyError as e:
             raise ValueError('bad config') from e
     except ValueError as v:
         chain = (type(v).__name__, type(v.__cause__).__name__)
     chain                                            ('ValueError', 'KeyError')       from e: the cause is on the new exception
     try:
         try:
             {}['k']
         except KeyError:
             int('x')
     except ValueError as v:
         ctx = (type(v.__context__).__name__, v.__cause__)
     ctx                                              ('KeyError', None)               raised while handling: linked as __context__, not __cause__
     try:
         try:
             {}['k']
         except KeyError:
             raise ValueError('bad config') from None
     except ValueError as v:
         hidden = (v.__context__ is not None, v.__suppress_context__)
     hidden                                           (True, True)                     from None keeps the context and hides it from the traceback

7. else RUNS WHEN NOTHING WAS RAISED; finally RUNS REGARDLESS
     def probe(text):
         log = []
         try:
             log.append(int(text))
         except ValueError:
             log.append('except')
         else:
             log.append('else')
         finally:
             log.append('finally')
         return log
     probe('1')                                       [1, 'else', 'finally']
     probe('x')                                       ['except', 'finally']

8. A GROUP IS CAUGHT IN PIECES, AND NOT BY A PLAIN except
     try:
         raise ExceptionGroup('two', [ValueError('a'), TypeError('b')])
     except* ValueError as eg:
         got_v = [type(e).__name__ for e in eg.exceptions]
     except* TypeError as eg:
         got_t = [type(e).__name__ for e in eg.exceptions]
     got_v, got_t                                     (['ValueError'], ['TypeError'])  each except* clause got its slice of the group
     try:                                             escaped: ExceptionGroup
         raise ExceptionGroup('one', [ValueError('a')])
     except ValueError:
         pass
     issubclass(ExceptionGroup, ValueError)           False                            a group is not any of its members

What the run shows

Section 1: the tree has a root above Exception. ValueError's ancestry goes through Exception; KeyboardInterrupt's does not. SystemExit, which sys.exit() raises, and GeneratorExit, which a generator receives when it is closed, are the other two that skip it.

Section 2: except Exception lets Ctrl-C through, and a bare except does not. The first block raised KeyboardInterrupt past except Exception:, and the harness that runs this page reports it as escaped. The bare except: in the second block caught it, and except BaseException: in the third did the same by name, which is the honest spelling when catching everything is what you mean, to log and re-raise, say.

Section 3: sys.exit() is an exception, and a bare except cancels the exit. sys.exit(3) raised SystemExit with a code of 3, and nothing exited. The second block is the bug this page exists for: a bare except: caught the exit, and the program that meant to stop is still running. A worker loop written as while True: try: … except: continue cannot be stopped by Ctrl-C or by sys.exit(), only by kill -9.

Section 4: the first matching clause wins. int('x') raises ValueError, and except Exception: comes first and matches, so handler is 'Exception' and the except ValueError: below it can never run. Nothing warns. Specific clauses go first, and issubclass(ValueError, Exception) being True is the reason.

Section 5: the as-name is deleted when the block ends. Inside the block err is the exception, and kind was read from it. After the block err raises NameError: Python deletes the name on the way out, because the exception holds its traceback, the traceback holds the frame, and the frame holds err, which would be a reference cycle keeping every local alive. Bind another name inside the block, as saved = err does, to keep it.

Section 6: an exception is an object, and raise from is a link. raise ValueError(...) from e stores e on the new exception as __cause__, and the pair reads ('ValueError', 'KeyError'). An exception raised while handling another gets the older one as __context__ instead, with __cause__ left None. raise … from None keeps the context on the object and sets __suppress_context__, so the traceback stops showing it while a debugger can still find it. Since 3.11 e.add_note(text) attaches a line to the object that the traceback prints under it.

Section 7: else runs when nothing was raised; finally runs regardless. probe('1') logs the value, then 'else', then 'finally'; probe('x') logs 'except', then 'finally'. else is for the code that should run only on success and should not itself be inside the try. What a return inside finally does to the exception is finally can overwrite the return.

Section 8: a group is caught in pieces, and not by a plain except. An ExceptionGroup holds several exceptions at once, and except* gives each clause the slice that matches it: got_v is ['ValueError'] and got_t is ['TypeError']. A plain except ValueError: does not catch a group that contains a ValueError, because issubclass(ExceptionGroup, ValueError) is False, and the harness reports the group escaping. Groups come from code that runs several things at once, asyncio.TaskGroup above all, which the concurrency library ↗ owns.

If you are coming from Rust

Rust has no exceptions and so no filter to get wrong. A recoverable failure is a Result you must look at, and the compiler warns when you do not; an unrecoverable one is a panic!, and Ctrl-C is a signal that ends the process with no unwinding at all, as Ctrl-C is a signal measures from the Python side. The nearest thing to a bare except: is let _ = fallible();, and it has to be written on purpose. The Rust library's Keep going or stop ↗ is the decision this page's hierarchy encodes, and What a panic costs ↗ is the unrecoverable half.

If you are coming from C

C reports failure with a return value and errno, and there is no hierarchy to filter on: every check is written by hand, and a check you did not write is a failure you did not see. Ctrl-C is SIGINT, delivered to a handler or killing the process, which the Linux library's signals chapter ↗ owns. Python's KeyboardInterrupt is that signal turned into an exception by a default handler, and section 2 is what happens to it afterwards.

If you are coming from ABAP

ABAP's class-based exceptions are a tree too, rooted at CX_ROOT, with CX_STATIC_CHECK, CX_DYNAMIC_CHECK and CX_NO_CHECK beneath it, and CATCH cx_root catches all of them the way except Exception does here. What ABAP does not have is anything beside the root: there is no exception class for the user interrupting the program, so the split in section 2 has no counterpart, and a runtime error that is not an exception object cannot be caught at all. RAISE EXCEPTION … EXPORTING previous = … is section 6's raise from, and the object's PREVIOUS attribute is __cause__. CLEANUP is not finally: it runs only when the exception is going to be handled outside the TRY, not on the normal path, so it has no else either. (Not machine-checked — CI cannot run ABAP.)

Try it

  1. Search a codebase you maintain for a bare except:: grep -rn 'except:' --include='*.py'. For each hit, ask whether Ctrl-C should be able to stop the loop it sits in. Section 3 says it cannot.
  2. Find a worker loop with except: continue or except Exception: pass in it, run it, and press Ctrl-C. One of the two spellings stops; the other does not.
  3. Look for an except Exception: clause that has a more specific clause after it. Section 4 says the second one is dead code, and no tool will have told you.
  4. Take one place where you re-raise a different exception, add from e, and compare the traceback with and without; then try from None and see what disappears.
  5. Run a linter over your code and look for the name after as being used outside its block. Section 5 says that read raises NameError, and the linter reports it as an undefined name.

See also