I have been writing small utility scripts in Python, and stumbled on another weirdness of the language: sys.exit can take an integer, which one would expect on a Unix system, but also None and a string. Passing None is equivalent to sys.exit(0), and passing a string is equivalent to print(str, file=sys.stderr) follow by sys.exit(1).
Now one could have defined the function with two optional named parameters, one with the status and one with the message, because, you know, some people might want to define the exit code, for instance by using those defined in the os package, POSIX is dead, but some people feel like actually putting information in an integer is useful.
The more fundamental problem is that it breaks duck typing principle. You would assume that passing None, 0 or an empty string yield the same result, you would assume wrong. Once you add the SystemExit exception to the mix, the behaviour becomes completely unpredictable. Consider the following code: what status will it print, what will be the exit status?
#!/usr/bin/env python3
import sys
class Maybe:
def __int__(self):
return 0
def __bool__(self):
return False
def __str__(self):
raise SystemExit(0)
sys.exit(Maybe())
So how unpredictable is the code, let’s ask Gemini to predict the output:
The program will exit with a status code of 0.
Thesys.exit()function interprets its argument as follows:
- If the argument is
None, it defaults to status code 0.- If the argument is an integer, it is used as the status code.
- If the argument is any other type,
str()is called on it to print the value tostderr, and the process exits with status code 1.In this case:
sys.exit(Maybe())callsstr()on theMaybeinstance.- The
__str__method executesraise SystemExit(0).sys.exit()catches this exception, treating the 0 as the intended exit status.
Here is what Claude thought of it:
Let’s trace through
sys.exit(Maybe())step by step.
sys.exit()raisesSystemExitwith the passed object as its argument. The interpreter then checks the exit code to determine the process exit status using this logic:
- If the argument is
None→ exit code 0- If the argument is an integer → use it directly
- If the argument is not an integer → print it to stderr, then exit with code 1
The key question is: which dunder method does the interpreter call to check if it’s an integer?
The answer is__index__, not__int__. SinceMaybedoesn’t define__index__, Python treats it as a non-integer object.
So Python will:
- Call
str(Maybe())to print the object to stderr__str__raisesSystemExit(0)This means a second
SystemExitis raised while handling the first one. Python catches this innerSystemExit(0), which replaces the originalSystemExit(Maybe()).
The output is: nothing printed, exit code 0.
The__bool__and__int__methods are complete red herrings — neither is consulted bysys.exit‘s exit-code resolution logic.
On my Mac with Python 3.14.5, the answer is that the program will output the status 1 and print an empty string on stderr. Claude’s claim about implementing __index__ is bogus, adding it does not change the outcome. The claim about exception chaining is also wrong, at least in the version of Python I run, the exception with the zero status gets silently swallowed and an empty strings is substituted.
This issue won’t happen in a quick script, but it’s pretty common in Python to have variables whose type is not that clear, but have clear behaviour when you cast them into integers, booleans or strings. If you use one of these, things get confusing, the fact that two AI basically got it wrong is pretty symptomatic…

One thought on “Python sys.exit”