Unix Runtime Behavior
Signals delivered between bytecodes, children that hang or go zombie, fork hazards, resource ceilings and output stuck in a buffer. It separates someone who scripts Python from someone who runs it.
part ofPythonoverview, primer and where to startread it →on this pageshowhide
explore
- Signals and Interrupts20 questions
- Handler Registration and Delivery4 questions
- SIGTERM and Graceful Shutdown4 questions
- Syscall Retry and EINTR4 questions
- Alarms and Interval Timers4 questions
- Running as PID 14 questions
- Spawning and Reaping Children15 questions
- Pipe Buffer Deadlocks4 questions
- Group Termination and Orphans4 questions
- Exit Status and Zombies3 questions
- Inherited Environment and cwd4 questions
- Duplicating an Address Space13 questions
- Forking Alongside Threads3 questions
- At-Fork Reset Hooks3 questions
- Copy-on-Write and Refcounts4 questions
- Exiting a Forked Child3 questions
- Kernel Quota Exhaustion16 questions
- File Descriptor Leaks4 questions
- Soft and Hard Rlimits4 questions
- Container OOM Kills3 questions
- Usable Processor Count5 questions
- Long-Lived Service Patterns20 questions
- WSGI and ASGI Interfaces4 questions
- Worker Process Models4 questions
- Preloading and Graceful Restart4 questions
- Running Under a Supervisor4 questions
- Single-Instance Lockfiles4 questions
- Standard Streams11 questions
- Line and Block Buffering4 questions
- Broken Pipes and SIGPIPE4 questions
- Descriptor-Level Redirection3 questions
questions
95 · 6 sectionsHow do you bound a blocking call with signal.alarm when it takes no timeout?
basics
~10 sInstall a SIGALRM handler with signal.signal that raises an exception, call signal.alarm(seconds) just before the blocking call, and call signal.alarm(0) in a finally block. The handler's exception unwinds the blocked call.
How do signal.SIGINT and signal.SIGTERM differ in a Python process by default?
basics
~20 sCPython installs its own default handler for signal.SIGINT that raises KeyboardInterrupt in the main thread, so finally blocks and atexit hooks still run. signal.SIGTERM keeps the operating system default, which terminates the process immediately with no Python cleanup.
What did PEP 475 change in Python 3.5 about system calls interrupted by a signal?
basics
~20 sSince Python 3.5, the interpreter reissues a system call that a signal interrupted instead of raising InterruptedError, after running the Python-level handler and recomputing any remaining timeout. Hand-written EINTR retry loops around stdlib calls became dead code.
Why can a handler installed with signal.signal() run well after the signal actually arrives?
basics
~10 sCPython installs its own C handler, which only records that the signal is pending. Your Python function runs later, when the main thread of the main interpreter reaches its next bytecode boundary check.
Why does a Python process running as PID 1 ignore signal.SIGTERM?
basics
~10 sThe kernel gives PID 1 no default action: a signal left at its default disposition is discarded, not acted on. CPython installs a handler for SIGINT but not SIGTERM, so call signal.signal(signal.SIGTERM, handler) yourself.
How do you make subprocess.run raise on a failing command, and which exception carries the exit code?
basics
~20 sPass check=True to subprocess.run, or call check_returncode() on the CompletedProcess it returns. Both raise subprocess.CalledProcessError, whose returncode attribute holds the child's exit status. Without check, a non-zero status comes back silently and is easy to ignore.
How do subprocess.Popen.terminate() and Popen.kill() differ on Unix?
basics
~20 sPopen.terminate() sends SIGTERM, which the child may catch and use to shut down cleanly. Popen.kill() sends SIGKILL, which no process can catch, block or ignore, so the child dies immediately with no cleanup. Both signal only the direct child.
Why can Popen.wait() deadlock when the child's stdout is subprocess.PIPE?
basics
~20 sA kernel pipe holds a fixed amount, roughly 64 KiB on Linux. Once the child fills it, the child blocks in write() while the parent blocks in Popen.wait, so neither moves. Popen.communicate drains both pipes and waits in one call.
Does subprocess.run's env argument merge with the parent environment or replace it?
basics
~20 sIt replaces it. The mapping you pass becomes the child's entire environment, so anything you leave out - PATH, HOME, LANG - is simply not there. Merge on purpose with a dict such as {**os.environ, 'KEY': 'value'}.
Why does Popen.terminate() leave a shell=True command's real program running?
basics
~20 sWith shell=True the direct child is /bin/sh, so Popen.pid is the shell's pid and terminate() signals the shell, not the program it started. Start the child with start_new_session=True and signal the whole group with os.killpg(os.getpgid(p.pid), signal.SIGTERM).
Why do Python pre-fork workers lose the copy-on-write sharing they start with?
basics
~20 sBecause CPython writes an object's reference count whenever a reference to it is taken or dropped. Merely reading a shared object dirties the whole page holding it, so a worker's private memory grows until little is really shared.
Why should a child created by os.fork() exit with os._exit()?
basics
~10 ssys.exit() only raises SystemExit, so the child unwinds, runs the parent's inherited atexit callbacks and flushes inherited buffers -- cleanup that happens twice. os._exit() ends the process immediately, skipping all of it.
Why does CPython raise a DeprecationWarning for os.fork() in a multi-threaded process?
basics
~20 sBecause the child gets the parent's whole memory but only the forking thread, so any lock the vanished threads held stays locked forever and the child deadlocks. Python 3.12 added the warning; Python 3.14 moved multiprocessing's Unix default to forkserver.
After os.fork(), does a change the child makes to a Python list reach the parent?
basics
~20 sNo. os.fork() gives the child a copy-on-write copy of the parent's address space, so the child sees the parent's objects exactly as they were at fork time, but every write it makes stays private to it.
Why does output written before os.fork() get printed twice?
basics
~10 sos.fork() copies the whole process, including text still sitting in stdout's userspace buffer. Both copies flush that same pending text later, so it prints twice. Call sys.stdout.flush() immediately before forking.
Why can open() without a with block leak a file descriptor in Python?
basics
~20 sA file object holds a kernel descriptor until something closes it. CPython usually closes it when the last reference disappears, but that is a refcounting implementation detail. A with block closes deterministically, even when the body raises.
What does os.cpu_count() report, and why is it the wrong number inside a container?
basics
~10 sos.cpu_count() reports the logical processors the host kernel exposes. A container's CPU limit is enforced as scheduling bandwidth rather than by hiding processors, so the count still says 64 on a two-CPU budget.
When does CPython raise a catchable MemoryError instead of the container being OOM-killed?
basics
~20 sMemoryError is raised only when an allocation request fails immediately, so the interpreter regains control. A cgroup limit is charged when pages are touched, not when they are requested, so the kernel kills the process instead -- there is nothing to catch.
Using resource.setrlimit, which limit changes are reversible and which are not?
basics
~20 sA process may move its soft limit freely between zero and its hard limit, so those changes are reversible. Lowering the hard limit is one-way: raising it again needs privilege an ordinary process lacks, and the drop is inherited by children.
Why does a containerized Python process exit with status 137 and print no traceback?
basics
~20 sStatus 137 is 128 + 9: the kernel delivered SIGKILL, most often when the process crossed its container memory limit. SIGKILL cannot be caught, so no except, finally or atexit code runs and nothing is printed.
Why do pre-fork Python application servers run several worker processes instead of one?
basics
~20 sBecause one CPython process executes Python bytecode on one core at a time under the global interpreter lock. Forking several workers, each with its own interpreter and its own lock, puts real work on every core and isolates a crash.
Why does preloading a Python app before forking workers save less memory than copy-on-write suggests?
basics
~20 sCopy-on-write shares pages only until something writes to one. CPython stores a reference count inside every object header, so merely touching a preloaded object dirties its page and the sharing quietly decays over the worker's lifetime.
How do you use fcntl.flock to guarantee only one copy of a Python job runs?
basics
~20 sOpen a fixed lockfile with os.open, then call fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB). A BlockingIOError means another copy holds it, so exit. Keep the descriptor open for the whole run; the kernel releases the lock when the process dies.
Why should a long-lived Python service run in the foreground instead of double-fork daemonising itself?
basics
~20 sA supervisor manages a process by being its parent. Staying in the foreground keeps the supervisor as the parent, so it knows the pid, reads the exit status, and captures whatever the process writes to sys.stdout and sys.stderr.
How do Python's pre-fork, threaded and async worker models differ in concurrency held and memory cost?
basics
~20 sA pre-fork worker holds one request and costs a whole interpreter. A thread holds one request and costs a stack. An async worker holds thousands of waiting connections in one thread — but only while nothing blocks.
Why does Python's print output appear instantly in a terminal but stall when the process is piped?
basics
~10 sCPython picks buffering per stream at startup: a terminal gets line buffering, so every newline flushes; a pipe or file gets a block buffer, 128 KiB on Python 3.14, that waits until it fills.
Why does CPython raise BrokenPipeError instead of dying on SIGPIPE like a C filter?
basics
~20 sCPython sets SIGPIPE to signal.SIG_IGN while the interpreter starts up. With the signal ignored, the failing write returns errno.EPIPE to the caller instead of terminating the process, and Python raises BrokenPipeError so the code can react.
What do Python's -u flag and PYTHONUNBUFFERED change about stdout and stderr?
basics
~20 sBoth force stdout and stderr to be unbuffered: the binary layer under each stream is created without a buffer and the text layer is write-through, so every write reaches the descriptor at once. Neither affects stdin.
Why does contextlib.redirect_stdout miss output from a compiled extension?
basics
~20 scontextlib.redirect_stdout only rebinds the Python object sys.stdout. Compiled code writes straight to file descriptor 1 and never consults sys.stdout, so its output bypasses the capture; redirecting the descriptor itself with os.dup2 is what catches it.
What does BrokenPipeError mean when a Python script writes to a pipe whose reader has exited?
basics
~20 sBrokenPipeError says a write failed because nobody is reading the other end any more: the downstream process exited or closed its read descriptor. The kernel fails the write with errno.EPIPE (32) and Python turns that into this OSError subclass.