I have a habit of adding return 0 to the end of my Python main() functions, even if they don't actually return anything. A friend pointed this weirdness out, and guessed, probably correctly, that this is a habit from C/C++ where returning 0 means 'everything went as expected'. But does that make any sense at all in Python?
Let's assume you're writing a Python script that you want to call from the command line. It might also be a library, but if called as a script you use the classic idiom if __name___ = "__main__" in your code, as follows, and save it in a file called exit-examples.py.
def main() -> None:
print("Hello World")
# None is returned implicitly if
# there is no return statement
return None
if __name__ == "__main__":
main()So let's run this from the command line, like in a shell script1, the special parameter $? expands to give the exit status of the most recent pipeline, so we can use that to show us what came back:
$ python exit-examples.py; echo Returned $?
Hello World
Returned 0Runs without issue, and is somehow returning 0 in the first step.
Let's try the usual way, something bad happens, and an exception is raised, if we replace main() above with the following:
def main() -> None:
print("Hello World")
raise AssertionError
return Noneand then run it again:
$ python exit-examples.py; echo Returned $?
Hello World
Traceback (most recent call last):
File "/home/<snip>/exit-examples.py", line 28, in <module>
main()
File "/home/<snip>/exit-examples.py", line 22, in main
raise AssertionError
AssertionError
Returned 1So far so good, the two most common scenarios work as expected, success returns 0, even if there's no value returned from the script, and a failure results in something being returned.
Let's make that first return into a none zero number, and see if some kind of return there would cause a failure.
def main() -> int:
print("Hello World")
return 1
if __name__ == "__main__":
main()This results in the same succesful commands as before:
$ python exit-examples.py; echo Returned $?
Hello World
Returned 0Trying that gives the same result, the 1 isn't passed outside of the Python world, and since we didn't hit an error, the script ended 'successfully' and still returned 0. Which means that my habit of adding return 0 is pointless, especially since if something is going wrong I normally raise an exception, or if it's minor, write it to a log message.
But, suppose we did want to control the return value from the function. That might be useful somewhere you want a shell script respond conditionally to the result of a Python program, but don't or can't do that within the Python program itself. Or suppose I just wanted to make my habit mean something?
Here sys.exit() comes into play, which allows you to control the return value of your program. Use it as follows:
import sys
def main() -> int:
print("Hello World")
return 1
if __name__ == "__main__":
sys.exit(main())This will return the value 1 from the program, and so be interpreted as unsuccessful, e.g.:
$ python exit-examples.py; echo Returned $?
Hello World
Returned 1You could of course return any value you like between 0 and 255, but other than 0 being success, there's not much agreement what the various exit codes mean. On Linux there are some reserved exit codes, and the FreeBSD sysexits(3) list some now deprecated exit codes, but there's no real standard you can stick to.
Also note that some programs, like grep(1) exit 0 only when something has been found, 1 when nothing has been found (but this doesn't mean an error) and >1 when there has been an error2.
If you choose to try and make the error codes to your program mean something (and document this!) then you can use it in more complex scripts like:
#!/bin/sh
eval python exit-examples.py
return_code=$?
if [ $return_code = 0 ]; then
echo "Success!"
elif [ $return_code = 1 ]; then
echo "Mild panic!"
elif [ $return_code = 42 ]; then
echo "Other fallback"
else
echo "Real Failure"
exit $return_code
fiWhich might be useful for kicking off other processes, or not, that it's not possible to manage from Python, and would be less error prone than trying to grep messages from stdout in your shell script.
You can make the return from Python something that's not an integer, like a string, but the shell will interpret that as a 1.
One final warning: out of range exit codes are interpreted as modulo 256, that mean after 255, you start going round again. If you write a program that has an exit code of 256, it'll come out as 0 and be considered successful!3.
Tested with Python 3.11 on FreeBSD 14.2-RELEASE, using the default sh shell, not Bash, but it should act the same for this. I think. ↩
The lack of a consistent error handling is one of the many reasons I really don't like shell scripts, and only use them for the simplest of simple things, or when there's no other choice. (I'll let others argue about if this is actually the shell language's fault, or the fact it's mostly used not to deal with it's own functions, but to string together disperate programs.) ↩