Skip to content
Happy Programming Guide
Start learning
Python

Debugging Python Exit Errors: A Comprehensive Guide

What each Python exit code means, why SystemExit is not an error, and how to diagnose a script that dies silently, hangs, or is killed by the operating system.

Hands typing on a laptop keyboard

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#

Terminal
python script.py
echo $?              # macOS and Linux
POWERSHELL
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.

Output
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#

Python
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:

Python
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.

Terminal
dmesg | grep -i "killed process"       # Linux
docker inspect <container> | grep -i oom

Find out what is consuming the memory:

Python
import resource, sys

peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
# kilobytes on Linux, bytes on macOS
print("peak memory:", peak)
Terminal
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:

Python
# 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.

Terminal
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:

Terminal
python -m pip install --force-reinstall --no-cache-dir numpy scipy pandas

Exiting with no message at all#

Four common causes:

Python
# 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:

Python
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#

Python
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.

Terminal
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:

Python
requests.get(url, timeout=10)

Writing scripts that exit well#

Python
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, and which exceptions to catchRead next

Keep reading

Python

Introduction to Python

What Python is, what people build with it, how it compares with other languages, and how to run your first program in…

4 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *