This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Cannot set exit code in atexit callback
Type: behavior Stage:
Components: Extension Modules, Interpreter Core Versions: Python 3.11, Python 3.10
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: Melebius, Mike Hommey, chrahunt, glyph, gwk, kakshay, pablogsal, r.david.murray, torsten
Priority: normal Keywords:

Created on 2016-05-16 07:57 by Melebius, last changed 2022-04-11 14:58 by admin.

Messages (19)
msg265678 - (view) Author: Miroslav Matějů (Melebius) Date: 2016-05-16 07:57
I want to set exit code of my script in a function registered in the atexit module. (See https://stackoverflow.com/q/37178636/711006.) Calling sys.exit() in that function results in the following error:

Error in atexit._run_exitfuncs:
Traceback (most recent call last):
  File "atexit_test.py", line 3, in myexit
    sys.exit(2)
SystemExit: 2

Despite the printed error, the exit code is set to 0. (This might be related with #1257.)

This problem seems to affect Python 3.x. I experienced it with Python 3.5.1 on Windows 7 x64 and I am able to reproduce it with Python 3.4.3 on Linux (x64). Python 2.7.6 on the same Linux machine works as expected: Exits without additional messages and the desired exit code is set.

A simple test case:

def myexit():
  import sys
  sys.exit(2)

import atexit
atexit.register(myexit)
msg265696 - (view) Author: R. David Murray (r.david.murray) * (Python committer) Date: 2016-05-16 14:28
Calling sys.exit in an atexit function strikes me as a really bad idea.  It feels to me like it breaks the contract of what atexit is designed for, which is to run multiple handlers at exit.  If you call sys.exit you are in some sense restarting the exit processing, which feels broken to me.  It would feel equally broken, however, to call _exit, since that would mean the rest of the handlers in the chain would not be run.

In any case, I personally would *expect* an exit called in an atexit handler to have no effect, since an exit is already in progress and we've just taken a detour to run the handlers.  I can see the argument for the reverse, however, so it isn't obvious to me what the correct answer is.

I do note that the OP in #1257 prefers to use the sys.exit return code if an atexit handler raises an error, which argues for not replacing it if the atexit handler raises SystemExit with a different RC.  It would be more consistent with the proposed handling of atexit errors.
msg271736 - (view) Author: George King (gwk) * Date: 2016-07-31 13:02
The documentation for atexit.register clearly states that a SystemExit raised inside of the registered function is a special case:

'''
If an exception is raised during execution of the exit handlers, a traceback is printed (unless SystemExit is raised) and the exception information is saved. After all exit handlers have had a chance to run the last exception to be raised is re-raised.
'''

Python 2.7.11 behaves as described; Python 3.5.2 does not.

I believe there is a clear argument for allowing atexit functions to set an exit status code: Ultimately, it is the responsibility of the application programmer to return an appropriate code for all execution paths, and it is up to the programmer to decide what is appropriate. I can easily imagine cases where the atexit function encounters a critical error and the appropriate behavior is to return an error status to the parent process.

In many large systems, returning the correct code is the most critical behavior of a process, and so if atexit prevents the programmer from doing so then its utility is greatly diminished.

I disagree that calling _exit is "equally broken". Calling _exit completely breaks the atexit unwinding contract, and if an error code is necessary, then this is exactly what I am forced to do!

In my mind the correct behavior would be that the process exit code is determined from the last exception that occurs in the exit process.
- If the program begins exiting with 0, and then an atexit handler raises SystemExit(1), then code 1 should be returned.
- If the program begins exiting with 1, and then an atexit handler raises SystemExit(0), then code 0 should be returned (this may seem strange, but handlers can do all manner of strange things!).
- If successive handlers raise multiple exceptions, the last one determines the code.
- If the program is exiting with any code, and an exception other than SystemExit is raised, then we should return the code that would result from raising that exception in normal execution (usually 1).

I expect this last case to be most contentious, because it changes behavior for both python2 and python3. However I think it is desirable because it gives the handlers precise capabilities (and responsibilities) regarding process status. The point of atexit is to allow modules to execute code in a deferred manner; the design already specifies a 'last exception wins' policy, and the problem is that we are unnecessarily suppressing the exit code that would result from that last exception.
msg271740 - (view) Author: R. David Murray (r.david.murray) * (Python committer) Date: 2016-07-31 14:12
Well, changing something like this in 2.7 is off the table in any case.  This would be a "feature release only" type of change if there is agreement that it is a good idea.
msg289145 - (view) Author: Glyph Lefkowitz (glyph) (Python triager) Date: 2017-03-06 23:34
I just bumped into this myself.  If this really is only fixable in a major release, there ought to at least be a minor release for the *documentation* to update it to be correct.
msg307052 - (view) Author: Torsten Landschoff (torsten) * Date: 2017-11-27 12:01
As this bug report clearly states this worked as documented in Python 2.7 and stopped working sometime in the Python 3 series.

I just ran into this while porting some code to Python 3 which uses an atexit handler to wind down some resources on process exit. This sometimes gets stuck and instead of hanging indefinitely the cleanup is aborted if it takes longer than a few seconds.

As this is actually an error, the registered exit function raises SystemExit to modify the exit code in this case. This used to work fine but does not anymore...

Observe:


## Python 2.7 works fine

$ sudo docker run python:2.7 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
124

## Python 3.2 (oldest Python 3 on docker hub) swallows the exit code (but prints it)

$ sudo docker run python:3.2 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0

## Same for 3.3 up to 3.6

$ sudo docker run python:3.3 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0
$ sudo docker run python:3.5 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0

$ sudo docker run python:3.6 python -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
Error in atexit._run_exitfuncs:
SystemExit: 124
0

## Python 3.7 swallows the exit code *and does not even print it*:

$ /opt/python-dev/bin/python3.7 -c "import atexit,sys;atexit.register(sys.exit,124)"; echo $?
0
msg334237 - (view) Author: Kumar Akshay (kakshay) * Date: 2019-01-22 20:36
Can I work on this?
I noticed the same behaviour as python3.7 in python3.8 from master branch.
msg334241 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2019-01-22 22:55
I think this is a documentation issue today. The docs say:

>If an exception is raised during execution of the exit handlers, a traceback
>is printed (unless SystemExit is raised) and the exception 
>information is saved. After all exit handlers have had a chance to run the
>last exception to be raised is re-raised.

Which is true except for two things:

- SystemExit is not covered by the paragraph (it just says that SystemExit will not print a traceback but what actually happens is that is ignored).
- The last exception is not re-raised (as it was on Python2.7).

I think we should update the docs to reflect that SystemExit is ignored and to remove that the last exception is re-raised.
msg334242 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2019-01-22 22:59
The behaviour regarding printing SytemExit was changed by Serhiy in 3fd54d4a7e604067e2bc0f8cfd58bdbdc09fa7f4 and in bpo-28994.
msg334243 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2019-01-22 23:00
@ Kumar Do you want to make a PR fixing the docs?
msg334263 - (view) Author: Kumar Akshay (kakshay) * Date: 2019-01-23 16:38
Sure, I would love to!
msg334264 - (view) Author: George King (gwk) * Date: 2019-01-23 16:56
I agree that regardless of the underlying issue, the docs should match the behavior. Additionally, I hope the docs will note the exact release at which the behavior changed.

@serhiy-storchaka do you have any opinion on this? I took a brief look at your commit cited above, and it appears that you were trying to suppress an unwanted stack trace display. Did you intend for the exit status behavior to also change?

I stand by my original assessment, as a somewhat dissatisfied user of the `atexit` feature. However I also acknowledge that at this point, CPython already has a subtle backwards-compatibility issue and further change might not be so welcome. I'm happy to discuss this further if people are interested, but I'm not going to lobby hard :)
msg334285 - (view) Author: Miroslav Matějů (Melebius) Date: 2019-01-24 08:56
I completely support @gwk’s opinion expressed in his comments. My original intention has been to set the exit code in an atexit callback. I tried a way and found that it was working in Python 2.7 but not in 3.x (without notice), so I filed this bug report. (See also the Stack Overflow link in my first post.)

I don’t find this just a documentation issue since it prevents the user from setting the exit code, although (as correctly stated by @gwk): “Ultimately, it is the responsibility of the application programmer to return an appropriate code for all execution paths” and “returning the correct code is the most critical behavior of a process”.

My use case is a testing library. The user calls assertions from my library and when their script finishes, my library is responsible for deciding whether the test has passed (and finishing the log). Before porting the library to Python (3.5 initially), I used to indicate successfulness by the exit code. With Python 3.x, I am forced to either:
1) print a result message and let the parent process parse it (implemented currently), or
2) force the user of my library to include something like
     sys.exit(testlib.result())
   as the last line of their scripts which I find annoying and error-prone.

I cannot decide whether calling sys.exit() in an atexit callback means breaking the contract as @r.david.murray states. However, the user shall be able to set the exit code of their application when it finishes and atexit should support some way to do that.
msg334286 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2019-01-24 09:06
You can always set the exit code calling sys.exit from outside the atexit handlers. I concur with R. David Murray in that calling sys.exit and expect anything sounds like a bad idea and an abuse if the atexit system.

A normal application would call sys.exit, some cleanup code will be called in the atexit handlers and finally the program will exit with the code originally set in the first call.

How and when your application will be calling sys.exit is an application architecture problem, and IMHO it should not be a CPython provided functionality to guarantee that you can do this in the atexit handlers.

It also violates the contract that right now is in the documentation: SystemExit exceptions are not printed or reraiaed in atexit handlers. Changing this (specially the second part) will be backwards incompatible, although that is a second order argument.
msg367957 - (view) Author: Glyph Lefkowitz (glyph) (Python triager) Date: 2020-05-03 08:25
This bug has been filed several times:

issue1257
issue11654

and it's tempting to simply close this as a dup, but this ticket mentions the documentation, which is slightly confusing: 

https://docs.python.org/3.8/library/atexit.html#atexit.register

It's not *wrong* exactly; 3.8's behavior matches the letter of the documentation, but "the last exception to be raised is re-raised" *implies* a change in exit code, since that is what would normally happen if an exception were re-raised at the top level.

So would it be a good idea to change the documentation?
msg367992 - (view) Author: George King (gwk) * Date: 2020-05-03 20:26
I think we should change the documentation to expand the parenthetical " (unless SystemExit is raised)" to a complete explanation of that special case.
msg367993 - (view) Author: Glyph Lefkowitz (glyph) (Python triager) Date: 2020-05-03 20:45
gwk, I absolutely agree; at this point that's the main thing I'm looking for.

But it would be great if whoever adds that documentation could include the rationale for this behavior too.  It still seems "obvious" to me that it should change the exit code, and every time I bump into this behavior I am freshly confused and have to re-read these bugs.  If I had a better intuition for what the error-handling *model* of atexit was, I think it might stick to memory a bit better.
msg399896 - (view) Author: Mike Hommey (Mike Hommey) Date: 2021-08-19 09:07
> I think we should change the documentation to expand the parenthetical " (unless SystemExit is raised)" to a complete explanation of that special case.

That would not be enough, since the case for other exceptions would still be ambiguous, as the described behavior ("After all exit handlers have had a chance to run the last exception to be raised is re-raised.") would imply the exit code would be altered, like when exceptions are raised in normal context. In 2.7 the only exception that _did_ change the exit code was SystemExit.
msg399897 - (view) Author: Mike Hommey (Mike Hommey) Date: 2021-08-19 09:10
> In 2.7 the only exception that _did_ change the exit code was SystemExit.

(and only if it was the last thrown exception)
History
Date User Action Args
2022-04-11 14:58:31adminsetgithub: 71222
2021-08-19 09:10:43Mike Hommeysetmessages: + msg399897
2021-08-19 09:07:24Mike Hommeysetnosy: + Mike Hommey
messages: + msg399896
2021-07-26 12:13:34wyz23x2setversions: + Python 3.10, Python 3.11, - Python 3.6, Python 3.7
2020-05-03 20:45:40glyphsetmessages: + msg367993
2020-05-03 20:26:16gwksetmessages: + msg367992
2020-05-03 08:25:19glyphsetmessages: + msg367957
2019-01-24 09:06:13pablogsalsetmessages: + msg334286
2019-01-24 08:56:05Melebiussetmessages: + msg334285
2019-01-23 16:56:51gwksetmessages: + msg334264
2019-01-23 16:38:28kakshaysetmessages: + msg334263
2019-01-22 23:00:43pablogsalsetmessages: + msg334243
2019-01-22 22:59:32pablogsalsetmessages: + msg334242
2019-01-22 22:55:33pablogsalsetnosy: + pablogsal
messages: + msg334241
2019-01-22 20:36:13kakshaysetnosy: + kakshay
messages: + msg334237
2019-01-21 17:52:31chrahuntsetnosy: + chrahunt

versions: + Python 3.7
2017-11-27 12:01:55torstensettype: enhancement -> behavior

messages: + msg307052
nosy: + torsten
2017-03-06 23:34:36glyphsetnosy: + glyph
messages: + msg289145
2016-07-31 14:13:10r.david.murraysettype: behavior -> enhancement
2016-07-31 14:12:47r.david.murraysetmessages: + msg271740
components: + Interpreter Core
versions: + Python 3.6, - Python 3.5
2016-07-31 13:02:46gwksetnosy: + gwk
messages: + msg271736
2016-05-16 14:28:23r.david.murraysetnosy: + r.david.murray
messages: + msg265696
2016-05-16 07:57:08Melebiuscreate