os.waitpid

Reap a finished child and find out how it ended. Only waitpid and waitstatus_to_exitcode exist on Windows too; wait, wait3, wait4, waitid, the W* status helpers and flags, the CLD_* codes and the P_* idtypes are Unix only, and pidfd_open needs Linux 5.3+.

os functionPython 3 (all versions); waitid 3.3+, waitstatus_to_exitcode and pidfd_open 3.9+
Common call
pid, status = os.waitpid(pid, 0)
Returns
(pid, wait status) - decode with waitstatus_to_exitcode
Replaces
subprocess.Popen.wait() / .returncode
Watch out
the status is not the exit code: exit 3 is status 768
os.waitpid(pidpid — waitpid/wait4 on Unix: > 0 that child, 0 any child in my process group, -1 any child, < -1 any child in process group -pid. On Windows: a process handle (as returned by spawn* with P_NOWAIT); <= 0 raises.type: int · required, optionsoptions — 0 for a normal blocking wait, or flags OR-ed together: WNOHANG, WUNTRACED, WCONTINUED (waitid: WEXITED, WSTOPPED, WCONTINUED, WNOHANG, WNOWAIT). Ignored on Windows.type: int · required, /) / wait() / wait3(options) / wait4(pid, options) / waitid(idtype, id, optionsoptions — 0 for a normal blocking wait, or flags OR-ed together: WNOHANG, WUNTRACED, WCONTINUED (waitid: WEXITED, WSTOPPED, WCONTINUED, WNOHANG, WNOWAIT). Ignored on Windows.type: int · required, /) / os.waitstatus_to_exitcode(status) / os.WIFEXITED(status) ... / os.pidfd_open(pid, flags=0)
→ tuple[int, int] | int | bool | waitid_result | None

Parameters

NameTypeRequiredDescription
pidintyeswaitpid/wait4 on Unix: > 0 that child, 0 any child in my process group, -1 any child, < -1 any child in process group -pid. On Windows: a process handle (as returned by spawn* with P_NOWAIT); <= 0 raises.
optionsintyes0 for a normal blocking wait, or flags OR-ed together: WNOHANG, WUNTRACED, WCONTINUED (waitid: WEXITED, WSTOPPED, WCONTINUED, WNOHANG, WNOWAIT). Ignored on Windows.
statusintyeswaitstatus_to_exitcode and the W* helpers: a wait status as returned by wait(), waitpid() or os.system() on Unix.
idtype, idintyeswaitid: P_PID (id is a pid), P_PGID (a process group), P_ALL (id ignored), P_PIDFD (id is a pidfd from pidfd_open, Linux 5.4+).

Return value

tuple[int, int] | int | bool | waitid_result | None — waitpid/wait: (pid, status). wait3/wait4: (pid, status, rusage). waitid: a waitid_result (or None with WNOHANG). waitstatus_to_exitcode: the exit code, negative for a signal. W* helpers: bool or int. pidfd_open: a file descriptor.

Common patterns

Wait for one child and get its exit code
Works on Unix and Windows (on Windows pid is the handle from spawn* P_NOWAIT).
import os
_, status = os.waitpid(pid, 0)
exit_code = os.waitstatus_to_exitcode(status)
Reap finished children without blocking (Unix)
WNOHANG returns (0, 0) when nothing has exited yet; ChildProcessError means there are no children left.
import os
while True:
    try:
        pid, status = os.waitpid(-1, os.WNOHANG)
    except ChildProcessError:
        break
    if pid == 0:
        break
    print(pid, 'ended with', os.waitstatus_to_exitcode(status))
Decode a status with the W* helpers (Unix)
Check WIFSTOPPED first: waitstatus_to_exitcode must not be called for a stopped child.
import os
if os.WIFEXITED(status):
    print('exit code', os.WEXITSTATUS(status))
elif os.WIFSIGNALED(status):
    print('killed by signal', os.WTERMSIG(status), 'core dumped' if os.WCOREDUMP(status) else '')
elif os.WIFSTOPPED(status):
    print('stopped by signal', os.WSTOPSIG(status))
waitid and pidfd (Linux)
A pidfd refers to one process and cannot be confused with a reused pid.
import os
fd = os.pidfd_open(pid)
info = os.waitid(os.P_PIDFD, fd, os.WEXITED)
os.close(fd)
if info.si_code == os.CLD_EXITED:
    print('exit code', info.si_status)

Examples

1. Status to exit code
import os os.waitstatus_to_exitcode(3 << 8)
Returns
3
2. Decode a normal exit by hand
status = 768 ((status >> 8) & 0xff, status & 0x7f)
Returns
(3, 0)
3. Killed by a signal
status = 9 (status & 0x7f, (status & 0x7f) != 0)
Returns
(9, True)
4. The core-dump bit
status = 134 (status & 0x7f, bool(status & 0x80))
Returns
(6, True)
5. A stopped child
status = 0x137f (status & 0xff == 0x7f, status >> 8)
Returns
(True, 19)
6. waitpid on a spawned child (Unix and Windows)
import os, sys pid = os.spawnv(os.P_NOWAIT, sys.executable, [sys.executable, '-c', 'raise(SystemExit(7))']) _, status = os.waitpid(pid, 0) (status, os.waitstatus_to_exitcode(status))
Returns
(1792, 7)
7. subprocess decodes it for you
import subprocess, sys subprocess.run([sys.executable, '-c', 'raise SystemExit(7)']).returncode
Returns
7
8. No children: ChildProcessError
import subprocess, sys code = 'import os\ntry:\n os.waitpid(-1, 0)\nexcept ChildProcessError as e:\n print(type(e).__name__)' subprocess.run([sys.executable, '-c', code], capture_output=True, text=True).stdout
Returns
'ChildProcessError\n'

Pitfalls

1. Treating the status as the exit code
waitpid returns a wait status, with the exit code shifted left by 8 bits (on Windows too). Convert it before comparing.
raw status
import os, sys
pid = os.spawnv(os.P_NOWAIT, sys.executable, [sys.executable, '-c', 'raise(SystemExit(7))'])
_, status = os.waitpid(pid, 0)
status == 7
False
waitstatus_to_exitcode
import os, sys
pid = os.spawnv(os.P_NOWAIT, sys.executable, [sys.executable, '-c', 'raise(SystemExit(7))'])
_, status = os.waitpid(pid, 0)
os.waitstatus_to_exitcode(status) == 7
True
2. Reading only the high byte
A child killed by a signal has a high byte of 0, which looks like a clean exit. The low 7 bits hold the signal number - check them first (WIFSIGNALED / WTERMSIG do this).
status >> 8
status = 9  # killed by signal 9
status >> 8
0
check the signal bits
status = 9
sig = status & 0x7f
('killed by signal', sig) if sig else ('exit code', status >> 8)
('killed by signal', 9)

When to use

Use it
  • After os.fork or spawn* with P_NOWAIT - every child must be waited for, or it stays a zombie on Unix
  • Decoding the status of os.system on Unix (waitstatus_to_exitcode)
  • waitid / pidfd_open: race-free waiting on Linux
Reach for something else
  • Processes started with subprocess → Popen.wait() / .returncode already decode the status
  • waitpid(-1, ...) inside a library → it can reap children that belong to other code

Notes

CPython impl
wait, wait3, wait4, waitid and waitpid wrap the C calls in Modules/posixmodule.c; on Windows waitpid uses _cwait. waitstatus_to_exitcode (3.9+) returns WEXITSTATUS for a normal exit, -WTERMSIG for a signal, raises ValueError otherwise; on Windows it returns status >> 8.
Availability
waitpid and waitstatus_to_exitcode: Unix, Windows. wait, wait3, wait4, waitid, all W* functions and flags, CLD_*, P_ALL/P_PID/P_PGID/P_PIDFD: Unix only (waitid on macOS since 3.13). P_PIDFD needs Linux 5.4+, pidfd_open Linux 5.3+. None of them on WASI, Android or iOS.
Status layout
Low 7 bits: the signal that killed the child (0 = it exited). Bit 0x80: a core file was produced. High byte: the exit code. A low byte of 0x7f means stopped, with the stop signal in the high byte.
Windows
waitpid takes a process handle (any process, not only children), ignores options and returns the exit code shifted left by 8 bits so the Unix decoding still works.
waitid_result
waitid returns a waitid_result with si_pid, si_uid, si_signo (always SIGCHLD), si_status and si_code (one of the CLD_* values; CLD_KILLED and CLD_STOPPED were added in 3.9).
Linux values
On Linux: WNOHANG 1, WUNTRACED 2, WSTOPPED 2, WEXITED 4, WCONTINUED 8; CLD_EXITED 1 ... CLD_CONTINUED 6; P_ALL 0, P_PID 1, P_PGID 2, P_PIDFD 3. Other Unix systems may use other numbers - always use the names.

FAQ

Waiting needs real child processes, which cannot run inside the page. Only os.waitpid and os.waitstatus_to_exitcode exist on Windows; wait, wait3, wait4, waitid, the W* helpers and constants, CLD_* and P_* are Unix only, and pidfd_open needs Linux 5.3+. The examples decode statuses by hand and wait on a spawned Python child, which gives the same numbers on both systems.