#!/usr/bin/env python3 """Example SystemExit edge case. Try:except blocks should always be as explicit as possible about the errors the catch; however, sometimes you really just need to catch anything. When this "anything" happens, always catch "Exception" -- never use a bare except. "But a bare except is the only way to catch everything", you say? Nope. You just need to be more explicit: SystemExit, KeyboardInterrupt, and GeneratorExit are a bit more slippery and will not be caught with "Exception", but can be caught explicitly. If another error occurs while you're handling the first, the second error will be placed on the top of the stack, and the first error will not reslove. This funcionality makes sense -- you've probably seen it via the message "During handling of the above exception, another exception occurred:" -- but it can lead to a fun edge case with the used with "finally": If a SystemExit is raised in "try", then another exception is raised during a "finally" block, the SystemExit will be silent. Example: # If "func" raises a SystemExit or similar, the logger call in the "finally" # block will raise an UnboundLocalError that will replace SystemExit on # the traceback handling stack. # If this block is also handled via "except Exception", the SystemExit # will be lost. try: try: val = func() # func raises SystemExit except Exception: val = None finally: logger.info('got %s', val) # Will raise UnboundLocalError except Exception: logger.info('this will be logged!') # And our handling is resolved! except SystemExit: logger.info('this will never be called') Example: $ ./sandbox_sys_exit.py INFO === Calling primary function: INFO -- Caught slippery err: SystemExit INFO -- Calling secondary function: INFO >>> Caught secondary ... """ import functools import logging import sys from typing import Callable, Optional FORMAT = '%(levelname)-8s %(message)s' logging.basicConfig(format=FORMAT, level=logging.DEBUG) LOGGER = logging.getLogger('sandbox-sys-exit') # Raise Functions # ====================================================================== def do_nothing(): ... def raise_for_primary(): raise ValueError('primary') def raise_for_secondary(): raise Exception('secondary') def raise_for_tertiary(): raise Exception('tertiary') def raise_generator_exit(): raise GeneratorExit('GeneratorExit') def raise_keyboard_interrupt(): raise KeyboardInterrupt('KeyboardInterrupt') def raise_system_exit(): raise SystemExit('SystemExit') # Example # ====================================================================== def example( primary: Callable, secondary: Optional[Callable] = None, tertiary: Optional[Callable] = None, logger: logging.Logger = LOGGER, ) -> None: """Demonstrate edge case for try:except:finally block. Arguments: primary: A callable that raises an exception -- preferably one of SystemExit, KeyboardInterrupt, or GeneratorExit. secondary: An optional callable for use in "except" blocks. Should raise any other exception. If not given, we won't do anything. tertiary: An optional callable for use in "finally" block. Should raise any other exception. If not given, we won't do anything. logger: An optional logger. Returns: None. Raises: Whatever you tell it to. """ secondary = secondary or do_nothing tertiary = tertiary or do_nothing try: logger.info('=== Calling primary function: %s', primary) primary() except Exception as err: logger.info(' -- Caught regular err: %s', err) secondary() raise except (SystemExit, KeyboardInterrupt, GeneratorExit) as err: logger.info(' -- Caught slippery err: %s', err) secondary() raise else: logger.info(' -- No error. This is boring.') finally: logger.info(' -- Calling secondary function: %s', secondary) tertiary() return # Main, etc. # ====================================================================== def main(logger: logging.Logger = LOGGER): """Entrypoint.""" primary = [ raise_for_primary, raise_system_exit, raise_keyboard_interrupt, raise_generator_exit, functools.partial(sys.exit, 0), functools.partial(sys.exit, 1), ] # An error during error handling # ---------------------------------- secondary = raise_for_secondary tertiary = None for func in primary: try: example(func, secondary, tertiary) except (Exception, SystemExit, KeyboardInterrupt, GeneratorExit) as err: logger.info('>>> Caught %s', err) else: logger.info('>>> Nothing raised') # Another error during "finally" # ---------------------------------- secondary = None tertiary = raise_for_tertiary for func in primary: try: example(func, secondary, tertiary) except (Exception, SystemExit, KeyboardInterrupt, GeneratorExit) as err: logger.info('>>> Caught %s', err) else: logger.info('>>> Nothing raised') if __name__ == '__main__': main() # __END__