skip to content

Process Lifetime

The process from before your first line to after your last: how the search path is assembled, what importing costs at startup, and what still runs on the way out.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

20

What does sys.exit() do, and what process exit code results?

level: juniorimportance: must knowfreq 60%

answer

  1. It raises rather than kills
  2. The status a shell records
  3. None, an int, and everything else
  4. A non-integer argument prints and exits 1
  5. Only the low eight bits survive

basics

~20 s

sys.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
pycon
>>> import sys
>>> try:
...     sys.exit(4)
... except SystemExit as exc:
...     print("code:", exc.code)
...
code: 4

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

What does sys.argv hold when you run python tools/report.py versus python -m tools.report?

level: juniorimportance: must knowfreq 58%

basics

~10 s

sys.argv[0] is the program name: the script path exactly as typed, or, under -m, the full path of the resolved module file. The program's own options always start at sys.argv[1]; interpreter switches never appear.

open as a page

Why does `except Exception` not catch SystemExit from sys.exit()?

level: middleimportance: must knowfreq 55%

basics

~20 s

SystemExit inherits directly from BaseException, not from Exception, so except Exception deliberately lets it pass. That keeps a broad error handler from swallowing a requested shutdown. A bare except: means except BaseException and does swallow it.

open as a page

Why move an `import` statement into a function body instead of the module top?

level: middleimportance: must knowfreq 48%

basics

~20 s

To keep its cost off the startup path. A module-level import runs while the module body runs, so every process pays it; an import inside a function runs only when that function is called, and never if it is not. It also breaks import cycles.

open as a page

How does python -m pkg.mod differ from running python pkg/mod.py?

level: middleimportance: must knowfreq 48%

basics

~20 s

Python's -m switch resolves a dotted module name through the import system, imports the parent package, and puts the working directory first on sys.path. A file path runs that file with its own directory first and no package context, so relative imports fail.

open as a page

Why can Python's __del__ method run late, never run, or silently hide an exception?

level: middleimportance: must knowfreq 55%

basics

~20 s

del fires when CPython actually destroys the object, not when your code stops using it, and the language promises nothing for objects still alive when the process ends. Any exception it raises has no caller to receive it, so it is reported and discarded.

open as a page

How does a virtual environment reshape sys.path when activation only edits PATH?

level: middleimportance: must knowfreq 62%

basics

~20 s

The interpreter does it, not the shell. Finding a pyvenv.cfg beside the executable, startup points sys.prefix at the environment root while sys.base_prefix keeps the base installation, so the site module builds site-packages under the environment instead.

open as a page

What does atexit.register guarantee about when and in what order your callback runs?

level: juniorimportance: should knowfreq 45%

basics

~20 s

atexit.register queues a callable to run during normal interpreter shutdown, and the queued callables run last-registered-first. Nothing runs after a hard exit: os._exit, an unhandled fatal signal or an interpreter crash skips every registered handler.

open as a page

How do distributions installed with pip end up importable without you editing sys.path?

level: juniorimportance: should knowfreq 32%

basics

~20 s

Before your first line runs, CPython imports the site module. It derives the interpreter's site-packages directories from sys.prefix and appends them to sys.path, so anything an installer wrote there is importable with no manual path work.

open as a page

What does CPython's `-X importtime` print, and how do its two time columns differ?

level: middleimportance: should knowfreq 32%

basics

~20 s

It writes one line per module imported during the run to stderr, showing that module's own execution time and the cumulative time including everything it triggered. Cumulative tells you which branch to cut; self tells you which module is doing the work.

open as a page

Why must a forked child call os._exit rather than sys.exit()?

level: seniorimportance: should knowfreq 30%

basics

~20 s

After os.fork() the child inherits a copy of the parent's stack and its buffered output. sys.exit() raises SystemExit, which unwinds through those inherited handlers and flushes the copied buffers, duplicating output and possibly resuming the parent's loop. os._exit() terminates immediately with none of that.

open as a page

How does a module-level `__getattr__` defer a package's heavy submodule imports?

level: seniorimportance: should knowfreq 22%

basics

~20 s

A __getattr__ function defined at module scope is called when an attribute lookup on that module misses. A package's __init__ can drop its eager submodule imports and import each one on first access instead, caching the result into the module globals.

open as a page

Why does a metrics scraper's __main__ module body re-run inside every spawned worker process?

level: seniorimportance: should knowfreq 34%

basics

~20 s

A child built from a fresh interpreter inherits none of the parent's memory, so multiprocessing rebuilds the entry module by executing its file again — under a substitute module name, so the module body runs but the name guard block does not.

open as a page

Why do a document-conversion worker's atexit handlers never run when its container is sent SIGTERM?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A default SIGTERM terminates the process at the OS level, so the interpreter never reaches finalization and no registered handler runs. Install a Python SIGTERM handler that raises SystemExit, and treat the shutdown flush as best effort regardless.

open as a page

How do you identify code that Python's site startup imports before your script's first line?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Enumerate what startup added: run python -m site to see the assembled path, the user site directory and site.ENABLE_USER_SITE, then read every .pth file in each site directory and check whether a sitecustomize or usercustomize module is importable there.

open as a page

How would you set and defend a cold-start budget for a Python CLI a small team owns?

level: principalimportance: should knowfreq 28%

basics

~20 s

Pick a number tied to how the tool is invoked, measure it on the deployment interpreter with a representative cold run, then enforce it automatically — a test asserting heavy modules are absent from sys.modules outlasts any review convention.

open as a page

What does runpy.run_path give you that a normal import does not?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

runpy.run_path executes the code at a filesystem location in a fresh throwaway namespace and returns that namespace as a dictionary. Nothing is cached in sys.modules, the file need not be importable, and run_name lets you execute it exactly as the interpreter would.

open as a page

What happens to each line of a .pth file found in a site-packages directory?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

The site module reads the file at startup. Blank and comment lines are skipped, a line starting with import is executed as code, and every other line is treated as a directory path, resolved against the file's own directory and appended to sys.path.

open as a page

What does exit code 137 from a Python CI step mean?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

137 is the shell's 128+N encoding for a process killed by signal 9, usually the kernel out-of-memory killer or a container memory limit. Python did not choose it: no sys.exit() call produced 137, the runner's shell reported the kill that way.

open as a page

How does weakref.finalize improve on __del__ for releasing an external resource?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

weakref.finalize attaches a callback to an object from the outside: it runs at most once when the object is collected, or at normal interpreter exit if the object is still alive, and it can be cancelled or invoked early without touching the class.

open as a page