SystemExit handling ambiguity

I’m working on a robotics project where we need more control over fatal and near-fatal events, so would like to replace the scattered exit() calls with a consolidated handler.

One option (deferred ATM) is to use in-executable signals, although that makes it difficult to recover context.

Currently trying to make raise SystemExit do what it seems suited for, and finding there are weakly- or un- documented behaviours that create ambiguity - e.g. excepthook is avoided (thanks, issue 4679).

My feeble understanding is that:

  1. in nested functions, exceptions are handled by the innermost relevant except: clause, and
  2. where there’s a sequence of except: clauses following a try:, the first relevant match will handle it - not necessarily the most specific

So I am puzzled by the main + module code below, which handles SystemExit as an Exception, rather than precisely itself. Can anyone explain or point to specific docco?

Thanks!


$ python main.py # run demo
---------- Exception ----------


# main.py
import sys
import os

import module.igniter

def exit_handler(type, value, caught_trace):
    sys.stderr.write(f'\nexit_handler: exception {type} caught: {value}')
    os._exit(0)         # I'm gone

if __name__ == "__main__":
    reason = 0

    sys.excepthook = exit_handler

    try:
        Igniter.light()

    except SystemExit:
        sys.stderr.write(f'---------- SystemExit ----------')
        reason = 253

    except Exception as e:
        sys.stderr.write(f'---------- Exception ----------\n')
        reason = 254

    except:
        sys.stderr.write(f'---------- Fallback? ----------\n')
        reason = 255

    os._exit(reason)

and …

# module/igniter.py
import sys

class Igniter():
    name: None

    def __init__(self):
        pass

    def light(self):
        sys.stdout.write('Stand back ...')
        raise SystemExit

Both of those are correct. My recommendation: IIDPIO. If in doubt, print it out!

What exception are you actually getting? I tried it out, adding print(e) inside the Exception branch, and… the results may explain why you didn’t get reason code 253 :slight_smile:

My feeble understanding is that:

  1. in nested functions, exceptions are handled by the innermost relevant except: clause, and
  2. where there’s a sequence of except: clauses following a try:, the first relevant match will handle it - not necessarily the most specific

That is correct.

So I am puzzled by the main + module code below, which handles SystemExit as an Exception, rather than precisely itself. Can anyone explain or point to specific docco?
[…]

import module.igniter
[...]
   try:
       Igniter.light()
   except SystemExit:
       sys.stderr.write(f'---------- SystemExit ----------')
       reason = 253
   except Exception as e:
       sys.stderr.write(f'---------- Exception ----------\n')
       reason = 254

It looks odd to me too, so I followed Chris’ advice and printed it out:

 try:
     Igniter.light()
 except SystemExit as e:
     print(f'---------- SystemExit ----------\n', type(e), e)
     reason = 253
 except Exception as e:
     print(f'---------- Exception ----------\n', type(e), e)
     reason = 254

and look:

 [~/tmp/s1]fleet2*> py3 main.py
 ---------- Exception ----------
  <class 'NameError'> name 'Igniter' is not defined

You problem is that you’re assuming you’re getting a SystemExit, but
in fact your code never got far enough to raise one. The mistake here is
to not recite the exception. Always collect the exception in a
variable (as e, above) and include it in any logging or messages.

In this instance you wanted ignite.Igniter or to from module.igniter import Igniter
which would be my preference.

Cheers,
Cameron Simpson cs@cskk.id.au

Using the ‘f’ prefix when there is no interpolation field is a bit confusing. Otherwise, great answer.

That’s what I get for copying and adapting…

Many thanks to Chris, Cameron - your suggestion has taken me far, although the original bug is still lurking deep in our code base.

I’ve turned off all the signal handlers, in case they were getting some cross-talk, but it’s not them.

Symptoms are currently:

SystemExit exception is occurring, and being ignored by all handlers, i.e. it prints the argument (if a string) and exits with 1 or the argument (if integer).

This looks awfully like the default built-in behaviour for SystemExits, so I must simplify, simplify, … until the bug comes to the surface.

Without seeing your code I can’t say for sure, but when you say “handler”, do you mean something that says except Exception: ? Because that won’t catch SystemExit.

Does this mean you have a stack trace? Does it include the function
containing the try/except which should catch SystemExit?

Cheers,
Cameron Simpson cs@cskk.id.au

The program is exiting to the shell, and I’m only getting the result code - integer or string depending on what I include in the raise SystemExit(code) statement.

So I know exactly where it’s coming from, and where it ends up, but not how it gets there while evading my containing try/except clause, excepthook and signal handlers!!

The program code has the same features as the demo above, plus a set of:

signal.signal(signal.SIGHUP, 
signal.signal(signal.SIGINT, 
signal.signal(signal.SIGQUIT,
signal.signal(signal.SIGTERM,

… using a common handler. All of them write stderr and then flush for certainty, and yet none of them see the SystemExit - not even the except SystemExit clause.

The minimal demo works (thanks to the correction), so it must be possible.

Signal handlers don’t “see” (catch) exceptions raised outside of the signal handler. The signal handler won’t run unless a signal is sent to the running process. SystemExit does not trigger a signal handler, so the whole issue of signal handlers is a red herring.

SystemExit will be caught as normal by except SystemExit, but not by except Exception as it does not inherit from Exception. You need BaseException instead.

Have you checked for unexpected calls to os._exit() which exit without raising a catchable SystemExit?

Hmmm, apparently SystemExit is not caught by the global excepthook.

[steve ~]$ python3.10
Python 3.10.0 (default, Oct 28 2021, 20:43:43) [GCC 8.3.1 20190223 (Red Hat 8.3.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def handler(*args):
...     print(args)
... 
>>> import sys
>>> sys.excepthook = handler
>>> raise BaseException("error")
(<class 'BaseException'>, BaseException('error'), <traceback object at 0x7f3cccb5d6c0>)
>>> raise SystemExit(3)
[steve ~]$ echo $?
3

This does not seem to be documented, but I have confirmed that it behaves this way back to at least Python 2.4.

Nevertheless, it is caught by an explicit except handler, so if you make the entry point to your application look something like this:

if __name__ == '__main__':  # Idiomatic entry point.
    try:
        main()
    except SystemExit as e:
        print(type(e), e)
        raise

you should see the SystemExit caught, unless it has already been caught earlier and os._exit() called.

We don’t want the builtin sys.excepthook to print a traceback for SystemExit, but I wonder why that behavior isn’t implemented in sys_excepthook_impl() in “Python/sysmodule.c”. Instead it’s implemented at a lower level in PyErr_PrintEx() in “Python/pythonrun.c”, which calls handle_system_exit() before it calls the hook. PyErr_PrintEx() is already implemented to call handle_system_exit() if the hook raises an exception, so it could let the hook decide how to handle a normal exit instead of hard coding the behavior.

That’s in contrast to the way sys.displayhook is handled. For the builtin display hook, we want nothing to be printed to sys.stdout if the value is None. In this case, instead of handling that choice at a lower level in the implementation of the PRINT_EXPR opcode, it’s implemented in the builtin sys_displayhook() function in “Python/sysmodule.c”. Thus a custom sys.displayhook function is called if the value is None and can make its own choice about how to handle this case.

Y’all …

My issue stems from excessive cleverness on my part: one of my modules has some procedural code outside of a class definition, and that contains a few uses of sys.exit().

This module gets imported three-down in the initial code loading (and the procedural code executed during the import), however the outer try/except block in my main did not wrap the module imports, but followed on from them. I wrongly assumed all the sys.exit()s were safe inside the class definitions and were inside the try block! :smirk:

Hence nothing I did to trap SystemExit exceptions would have any effect and the interpreter would handle the sys.exit()s - as you’d expect.

The offending code was an early implementation of hardware detection, which needed to happen before loading hardware-appropriate modules. This is being refactored into an explicit hardware module that’s employed early in the try/except wrapper. Lesson learnt!

Case closed, for me.

P.S. I note (on the excepthook omission of SystemExit) that there’s an issue open:

https://bugs.python.org/issue46759