When a Python script stops unexpectedly, the exit code tells you a great deal before you read a single line of code. Zero means success. One usually means an unhandled exception. A number above 128 means the operating system killed the process, and the difference matters — 137 is almost always the memory limit, and no amount of reading your code will reveal that.
Read the exit code first#
python script.py
echo $? # macOS and Linux
python script.py
$LASTEXITCODE # PowerShell
| Code | Means |
|---|---|
| 0 | finished normally |
| 1 | unhandled exception, or an explicit exit |
| 2 | command-line usage error, from argparse |
| 130 | interrupted with Ctrl+C |
| 137 | killed — nearly always out of memory |
| 139 | segmentation fault, usually in a C extension |
| 143 | terminated politely, by a shutdown or a timeout |
Codes above 128 are 128 plus a signal number. 137 is 128 plus 9, meaning the process was killed outright; 143 is 128 plus 15, a request to terminate that Python could have handled.
Exit code 1: an unhandled exception#
The ordinary case, and the one with a traceback. Read it from the bottom up: the last line names the exception, and the frame above it is where it happened.
Traceback (most recent call last):
File "script.py", line 12, in <module>
process(rows)
File "script.py", line 7, in process
total += float(row["amount"])
ValueError: could not convert string to float: 'n/a'
The useful frame is usually the deepest one in your code, not the deepest overall.
SystemExit is special#
import sys
try:
sys.exit(1)
except Exception as e:
print("caught:", e) # this does NOT run
sys.exit() raises SystemExit, which inherits from BaseException rather than Exception — deliberately, so ordinary error handling does not swallow a deliberate exit. The same applies to KeyboardInterrupt.
That design has a consequence worth knowing:
try:
sys.exit(1)
except: # bare except catches everything
print("this DOES run") # and stops the exit
Exit code 137: out of memory#
No traceback, no error message, the process simply stops. On Linux the kernel’s out-of-memory killer has terminated it; in Docker or Kubernetes it usually means the container’s memory limit was reached.
dmesg | grep -i "killed process" # Linux
docker inspect <container> | grep -i oom
Find out what is consuming the memory:
import resource, sys
peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
# kilobytes on Linux, bytes on macOS
print("peak memory:", peak)
python -m pip install memory_profiler
python -m memory_profiler script.py
The usual causes are reading a whole file into memory, accumulating results in a list inside a long loop, or loading an entire dataset when you only needed to stream it:
# Loads everything
rows = list(csv.DictReader(open("huge.csv")))
# Processes one row at a time, flat memory
with open("huge.csv", newline="") as f:
for row in csv.DictReader(f):
process(row)
Exit code 139: segmentation fault#
Pure Python cannot segfault. This comes from a C extension — NumPy, a database driver, an image library — and usually means a version mismatch between compiled packages.
python -X faulthandler script.py
That prints a Python traceback at the moment of the crash, which tells you which library was involved. Then reinstall the compiled packages together so their binaries match:
python -m pip install --force-reinstall --no-cache-dir numpy scipy pandas
Exiting with no message at all#
Four common causes:
# 1. An explicit exit somewhere
sys.exit() # or exit(), or quit(), or os._exit()
# 2. A swallowed exception
try:
risky()
except Exception:
pass # the failure is now invisible
# 3. The script simply reached the end
def main():
if not valid:
return # nothing happens, no message
# 4. A daemon thread was doing the work
os._exit() is worth knowing about because it is the harshest: it terminates immediately without running cleanup handlers, flushing buffers or closing files. If output is missing as well as the exit being abrupt, that is a candidate.
To find where the exit happened:
import atexit, traceback, sys
def on_exit():
print("Exiting. Stack at this point:")
traceback.print_stack()
atexit.register(on_exit)
A script that hangs instead of exiting#
import faulthandler, signal
faulthandler.register(signal.SIGUSR1) # Linux and macOS
Then, from another terminal, send that signal to the process and Python prints the stack of every thread — showing exactly where it is stuck.
kill -USR1 <pid>
The usual causes are a non-daemon thread still running, a network call with no timeout, or a multiprocessing pool that was never closed. Always set a timeout on network calls:
requests.get(url, timeout=10)
Writing scripts that exit well#
import sys
import logging
log = logging.getLogger(__name__)
def main() -> int:
try:
run()
except FileNotFoundError as error:
log.error("Missing file: %s", error.filename)
return 2
except KeyboardInterrupt:
log.warning("Interrupted")
return 130
except Exception:
log.exception("Unexpected failure")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
Returning a code from main() and passing it to sys.exit() keeps the exit logic in one place. Distinct codes let a shell script or a scheduler react differently to different failures, which is the whole point of exit codes.
Questions people ask#
What is the difference between exit() and sys.exit()?
exit() and quit() are added by the site module for interactive use and may not exist in an embedded interpreter. Use sys.exit() in scripts.
Why does my exception handler not catch KeyboardInterrupt?
It inherits from BaseException, not Exception, so except Exception misses it deliberately. Catch it by name when you want to handle Ctrl+C gracefully.
How do I make cleanup run on exit?
Use a finally block, a context manager, or atexit.register(). None of them runs after os._exit() or a signal 9 kill.
Can I catch a segmentation fault?
No — the process is gone before Python regains control. Enable faulthandler to get a traceback at the moment it happens, then fix the underlying library mismatch.
Where to go next#
- Python try/except explained — catching the right exceptions.
- Why is my Python code not working? — reading tracebacks properly.
- Python best practices — logging and error handling habits.