Exiting a Python Script Cleanly

Scripts need to finish in a way that tells the caller (or the shell) whether they succeeded or failed. The way you exit matters for automation, error handling, and portability.

Common Exit Patterns

1. return from main() (preferred for scripts)

The cleanest exit is to return an integer status from main() and let the if __name__ == "__main__": block pass it to sys.exit().

import sys


def main() -> int:
    try:
        do_work()
    except FileNotFoundError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Why raise SystemExit(...) instead of sys.exit(...)? They are functionally the same in most cases, but raise SystemExit is explicit and slightly more readable to some people. Use whichever you prefer, but be consistent.

2. sys.exit()

sys.exit() raises a SystemExit exception, which propagates up to the interpreter and stops the script. It is clean and standard.

import sys

if len(sys.argv) != 2:
    print("Usage: script.py <file>")
    sys.exit(1)

In scripts, this is fine. In modules meant to be imported, it is usually better to raise a regular exception or return a status so the caller can decide what to do.

3. os._exit()

os._exit() terminates the process immediately without running cleanup handlers (like finally blocks, __exit__ methods from context managers, or atexit functions). This is usually only useful when you want a guaranteed, hard exit from a child process, e.g., after os.fork().

Avoid os._exit() in normal scripts. You risk leaving resources, temporary files, or sockets in a bad state.

import os

os._exit(1)  # Use only in special cases, not normal cleanup paths.

4. quit() and exit()

These are built-in helpers intended for the interactive REPL. They work in scripts too, but they are not recommended for production code because they may not be available in all environments (for example, some embedded Python interpreters may not define them). Always use sys.exit() or raise SystemExit instead.

Exit Codes

  • 0 = success
  • 1 = general error
  • 2 = misuse of command (e.g., bad arguments)

Other non-zero values are possible, but unless you are writing for a specific tool or pipeline that expects specific codes, stick to 0 and 1. Some teams use 2 for argument errors, but consistency matters more than the exact number.

Best Practices for Security Scripts

  • Return from main(), then exit once at the bottom of the script.
  • Print errors to sys.stderr, not sys.stdout, so they do not pollute output piped to other tools.
  • Avoid os._exit() unless you have a specific reason to bypass cleanup.
  • Avoid quit()/exit() in scripts.
  • Catch expected errors near where they can happen, but let unexpected errors propagate so they produce a traceback.

Example

#!/usr/bin/env python3
import sys


def process_file(path):
    with open(path, "r") as f:
        return f.read()


def main() -> int:
    if len(sys.argv) != 2:
        print("Usage: script.py <file>", file=sys.stderr)
        return 1

    try:
        data = process_file(sys.argv[1])
    except FileNotFoundError as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

    print(data)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())