What does sys.exit() do, and what process exit code results?
answer
- It raises rather than kills
- The status a shell records
- None, an int, and everything else
- A non-integer argument prints and exits 1
- Only the low eight bits survive
basics
~20 ssys.exit() raises SystemExit rather than killing the process outright; if nothing catches it, the interpreter shuts down. No argument or None gives exit status 0, an integer gives that value, and any other object is printed to stderr with status 1.
solid answer
~40 s`sys.exit()` is not a system call — it raises `SystemExit`, an exception that unwinds the stack, runs `finally` blocks and context-manager cleanup, and only ends the process once it reaches the top of the main thread uncaught. The argument becomes the status: omitted or `None` means 0 (success), an `int` is used directly, and anything else is printed to `sys.stderr` and the process exits 1. The raised instance keeps the value on its `code` attribute, so a caller can catch `SystemExit` and inspect it. On POSIX only the low eight bits of the status survive the wait call, so `sys.exit(300)` reaches the shell as 44 and negative values wrap. An uncaught ordinary exception — a `ValueError` escaping the script — also produces status 1, after printing a traceback.
code
pycon · 7 lines>>> import sys
>>> try:
... sys.exit(4)
... except SystemExit as exc:
... print("code:", exc.code)
...
code: 4go deeper
Be ready to say that sys.exit() raises SystemExit and to state the three argument cases: nothing or None means 0, an integer means that code, anything else prints to stderr and means 1. Know that 0 is success.
Explain the mechanics: the exception unwinds the stack so finally blocks and exit methods still run, the status lives on the code attribute, and the OS keeps only the low eight bits. Know that a worker thread's sys.exit() ends the thread, not the process.
Show judgment about where exits belong. Demonstrate the sys.exit(main()) entry-point pattern, argue against library code that exits the process out from under its caller, and explain why a crash and a deliberate sys.exit(1) look identical to a CI step.
Own the exit-status vocabulary as an interface. Decide which non-zero codes a fleet of tools reserves for retryable versus fatal conditions, keep them under 126 to avoid shell and signal collisions, and document them so supervisors and pipelines can act on them without parsing logs.
## The contract a process has with whatever started it Every process hands one small integer back to its parent when it finishes. A shell puts it in `$?`, a CI runner turns it into a green or red step, a supervisor decides from it whether to restart the service. Zero conventionally means success and any non-zero value means failure; the specific non-zero number is the application's own vocabulary. `sys.exit()` is how Python code chooses that number deliberately. ## sys.exit raises; it does not terminate The single most important thing about `sys.exit()` is that it is not a wrapper around the C `exit()` call. Its entire implementation is `raise SystemExit(arg)`. That has consequences a candidate is expected to state: - The stack unwinds normally. `finally` blocks run, `with` statements call their `__exit__` methods, and any `except` clause broad enough to match will catch it — so `sys.exit()` inside a `try` is not guaranteed to exit anything. - Only after `SystemExit` reaches the top of the **main thread** uncaught does the interpreter begin shutdown and pass the status to the operating system. - Called on a non-main thread, `SystemExit` unwinds just that thread and is deliberately ignored by the threading machinery; the process keeps running with its status unaffected. Code that tries to "exit the program" from a worker thread simply ends that worker. ```python import sys try: sys.exit(4) finally: print("finally still runs") ``` ## What the argument means The rules are short and are the usual point of the question: | argument | resulting status | |---|---| | omitted, or `None` | 0 | | an `int` | that integer, truncated to 8 bits by the OS | | any other object | printed to `sys.stderr`, status 1 | That third row is where beginners go wrong. `sys.exit("config file missing")` is a perfectly idiomatic way to abort a script with a message — the message goes to standard error and the shell sees 1 — but it does **not** mean the string became the status, and it is not a substitute for logging. The value is preserved on the exception instance as its `code` attribute, which is what makes exit status testable: ```python import sys try: sys.exit("config file missing") except SystemExit as exc: print(repr(exc.code)) # 'config file missing' ``` ## Truncation, and why exit codes are small On POSIX the parent retrieves the status through a wait call, and only the low eight bits of the value passed to `exit()` survive. `sys.exit(300)` therefore surfaces as `300 & 0xFF` = 44, and `sys.exit(-1)` becomes 255. Because of this, portable programs use small codes, conventionally in the range 0–125; higher numbers collide with shell conventions (126 and 127 for "not executable" and "not found") and with the way killed processes are reported. ## The other ways a Python process picks a status - **Falling off the end of the script** — status 0. Returning a value from a function, including a `main()` function, sets nothing by itself; the common idiom `sys.exit(main())` exists precisely because a bare `main()` call throws its return value away. - **An uncaught ordinary exception** — the interpreter prints a traceback to standard error and exits 1. This is why a crashed script and an explicit `sys.exit(1)` are indistinguishable to CI from the status alone. - **`os._exit(status)`** — terminates immediately with no unwinding, no cleanup and no flushing of buffered output. It is the right tool in a forked child and the wrong tool almost everywhere else. - **The `exit` and `quit` names available in the REPL** — these are injected by the `site` module as a convenience for interactive use. They are not guaranteed to exist in a script (a `-S` interpreter has no `site`), so scripts import `sys` and call `sys.exit()`. ## Writing code that uses this well A command-line entry point normally computes a status and exits once, at the outermost level: ```python import sys def main(argv: list[str]) -> int: if len(argv) < 2: print("usage: tool INPUT", file=sys.stderr) return 2 return 0 if __name__ == "__main__": sys.exit(main(sys.argv)) ``` This keeps `main` a plain, testable function that returns an integer, and confines the process-ending side effect to one line. Scattering `sys.exit()` calls deep inside library code is the anti-pattern: a library that exits the process removes the caller's ability to handle the error, and because `SystemExit` bypasses `except Exception`, the caller usually does not even notice it happening until the process is gone.
- What exit status does a script produce when an unhandled ValueError propagates out of it?1. The interpreter prints the traceback to standard error and exits with status 1, exactly the same status as an explicit `sys.exit(1)`. That is why a supervisor or CI step cannot distinguish a deliberate failure from a crash by status alone — if you need that distinction, pick a distinct non-zero code for the deliberate path and reserve 1 for unexpected crashes.
- What happens when sys.exit() is called from a worker thread instead of the main thread?It raises `SystemExit` on that thread only. The exception unwinds that thread's stack and the threading machinery deliberately ignores it, so the thread ends silently and the process keeps running with its exit status untouched. To stop the whole process from a worker you have to signal the main thread — set an event or push a sentinel — and let the main thread call `sys.exit()`.
- Why do many CLI programs write sys.exit(main()) rather than just calling main()?Because a function's return value does nothing to the process status on its own. Wrapping the call makes `main` a plain function that returns an integer — trivially unit-testable, with no process-ending side effect — while the single `sys.exit()` at the entry point converts that integer into the status the shell reads.
sys.exit() is less like pulling the plug and more like sending a resignation letter up the chain: it travels through every layer of handling on the way out, and any layer can quietly file it in a drawer.
saying these in an interview costs you the question
- Says sys.exit() immediately terminates the process
- Thinks sys.exit('boom') makes the string the exit status
- Believes returning 1 from main() sets the exit status
- Assumes sys.exit() in a worker thread stops the process
- Confuses sys.exit() with os._exit(), which skips all cleanup
- Thinks any integer survives as-is, ignoring 8-bit truncation