How does a Python program set the exit status its supervisor sees when it fails?
answer
- One small integer is all the supervisor gets
- sys.exit raises rather than terminates
- Which base class SystemExit inherits from
- Non-integer argument goes to stderr
- Only the low eight bits survive
basics
~20 ssys.exit(n) raises SystemExit, and the interpreter exits with that integer status. Falling off the end of the program exits 0; an uncaught exception prints a traceback and exits 1; sys.exit('message') prints the text to stderr and exits 1.
solid answer
~40 s`sys.exit` does not stop the process on the spot — it raises `SystemExit`, which unwinds the stack normally, so `finally` blocks, context-manager exits and `atexit` callbacks all still run before the interpreter reports the status. The argument decides the status: an integer becomes the status directly, `None` means 0, and any other object is printed to `sys.stderr` with the status set to 1. Because `SystemExit` inherits from `BaseException` rather than `Exception`, an `except Exception:` handler correctly lets it through, while a bare `except:` swallows it and keeps the process alive. The usual service shape is a `main()` that returns an `int` called as `sys.exit(main())`, with 0 for success and distinct non-zero codes for the failure classes the supervisor's restart policy should treat differently.
code
console · 2 lines$ python -c "import sys; sys.exit(3)"; echo "status=$?"
status=3go deeper
Know the three everyday outcomes: normal end means 0, sys.exit(n) means n, and an uncaught exception prints a traceback and means 1. Recognise sys.exit(main()) as the standard entry-point idiom.
Explain the mechanics: sys.exit raises SystemExit, which unwinds the stack so finally and atexit still run, and which inherits from BaseException so except Exception: deliberately lets it through. Know the argument rules and the 0-255 range.
Demonstrate deliberate code design — separating a permanent misconfiguration from a transient dependency failure so the restart policy and the alerting can tell them apart — and be able to describe debugging a service that would not die because a catch-all swallowed its exit.
Own the convention across services: an agreed meaning for the small set of non-zero codes, alerting and restart policy keyed to them, and a rule that library code never exits the process. Be ready to say what that standardisation buys and where it is not worth enforcing.
## Exit status is the only thing a supervisor can read without parsing When the process ends, the supervisor reaps it and gets one small integer. That number decides whether the run counts as a success, whether a restart happens, and — with policies that distinguish codes — whether the service should stay down. So producing the right number is **part of the service's contract**, not a detail. ## How CPython produces it There are three ordinary paths. 1. First, the program runs to the end of its top-level code and the interpreter exits with 0. 2. Second, an exception propagates out of the top level: CPython prints the traceback to `sys.stderr` and exits with 1 — always 1, regardless of what the exception was. 3. Third, the program raises `SystemExit`, normally via `sys.exit`, and the interpreter uses the value carried on the exception. That third path deserves care because it is routinely misdescribed. **`sys.exit` does not terminate anything; it raises.** The exception then unwinds the stack like any other, which is precisely what you want: `finally` blocks run, `with` blocks call their `__exit__`, and functions registered with `atexit.register` run at the end. The argument becomes the exception's `code` attribute: - an `int` is used as the status; - `None` (the default) means 0; - anything else is printed to `sys.stderr` and the status becomes 1. That last rule is why `sys.exit('config file missing')` is a legitimate one-liner in a small script — it writes the message where the supervisor's log capture will see it and exits non-zero — and also why nobody should expect a string to become a numeric code. ## Why the base class matters `SystemExit` derives from `BaseException`, not from `Exception`. This is deliberate: the defensive `except Exception:` you wrap a request handler in must not accidentally cancel a shutdown. A bare `except:` or `except BaseException:` does catch it, and that is the classic bug where a **service refuses to die** — the intended exit is swallowed by a catch-all in a loop and the process runs on, doing nothing useful, while the supervisor sees a perfectly healthy process. A second surprise lives in **threads**. `SystemExit` raised inside a non-main thread ends only that thread; the threading machinery treats it as a normal way for a thread to finish and the process carries on, eventually exiting 0. A worker that decides the whole service must stop has to communicate that to the main thread — an event, a queue sentinel, a flag the main loop checks — rather than calling `sys.exit` where it stands. ## The numeric range On Unix the wait status carries only the **low eight bits** of what the process passed, so codes live in 0–255. `sys.exit(256)` is reported as 0, which looks like success and is one of the nastier self-inflicted wounds available; negative numbers wrap, so -1 arrives as 255. Keep codes small and deliberate. A common shape is: - 0 for success, - 1 for a generic failure, - and a couple of specific codes for classes the operator cares about — for example one code meaning `this configuration cannot work, restarting will not help` and another meaning `a dependency was unavailable, restarting later probably will`. Whether the supervisor can act on the distinction depends on its policy, but the codes cost nothing and they document intent in the logs. ## The blunt instrument `os._exit` ends the process immediately with the status given, skipping stack unwinding, `finally`, `atexit` and any flushing of buffered output. That is the right tool in a forked child that must not run the parent's cleanup handlers, and the wrong tool everywhere else — a service that exits this way regularly loses its last, most interesting log lines. ## Putting it together - Write `main()` so that it returns an `int` and does not call `sys.exit` from deep inside helper functions, where an exit is invisible to the reader and untestable. - Call it once, at the bottom of the module, as `sys.exit(main())`. - Let genuinely unexpected exceptions propagate so the traceback reaches the standard error stream and the status is 1; catch the ones you can classify and turn them into a deliberate code. The result is a process whose ending is readable both by a human scanning the log and by whatever started it.
- A worker thread calls sys.exit(1). What status does the process exit with?Most likely 0. `SystemExit` raised in a non-main thread ends only that thread — the threading machinery treats it as a normal finish and does not propagate it — so the process keeps running and later exits however the main thread ends. To stop the whole service from a worker, signal the main thread through an event or a sentinel and let the main thread return a non-zero code.
- Why can sys.exit(256) look like success to whatever started the process?On Unix the wait status keeps only the low eight bits of the value, so 256 truncates to 0 and the supervisor reads a clean exit. Negative values wrap the same way, with -1 arriving as 255. Keep exit codes in the 1-255 range and pick them deliberately.
- Do cleanup handlers still run when a service exits with sys.exit?Yes. `sys.exit` raises `SystemExit`, so the stack unwinds normally: `finally` blocks execute, context managers run their `__exit__`, and functions registered with `atexit.register` run before the interpreter finishes. `os._exit` is the exception — it hands the status to the kernel immediately and skips all of it, which is why it belongs in a forked child and almost nowhere else.
saying these in an interview costs you the question
- Thinks sys.exit stops the process immediately like os._exit
- Catches SystemExit in a bare except and keeps running
- Believes an uncaught exception still exits with status 0
- Expects a string passed to sys.exit to become the status
- Uses codes above 255 to encode extra detail
- Calls sys.exit from a worker thread to stop the service