Skip to content

Instantly share code, notes, and snippets.

@muppetjones
Created June 13, 2021 17:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save muppetjones/4b964e1a57447e31476f41487d641cb2 to your computer and use it in GitHub Desktop.
Save muppetjones/4b964e1a57447e31476f41487d641cb2 to your computer and use it in GitHub Desktop.
Edge Case for SystemExit
#!/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: <function raise_system_exit at 0x10a2534c0>
INFO -- Caught slippery err: SystemExit
INFO -- Calling secondary function: <function raise_for_secondary at 0x10a003160>
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__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment