Supervisors, test harnesses and dev servers must stop children cleanly and report why they died.
p.terminate() asks a process to stop: SIGTERM on POSIX, which the program can catch to clean up. p.kill() forces it: SIGKILL, which can't be caught. On Windows both call TerminateProcess. p.send_signal(sig) sends any signal, for example signal.SIGINT to mimic Ctrl+C.
The graceful pattern:
p.terminate()
try:
p.wait(timeout=5)
except subprocess.TimeoutExpired:
p.kill()
p.wait()Always wait() after signalling, so the process is reaped; an exited child that hasn't been waited for stays a zombie in the process table.
On POSIX, a negative returncode -N means the process was killed by signal N: -15 is SIGTERM, -9 is SIGKILL. A shell reports the same event as 128 + N, so 137 from a shell or Docker means SIGKILL, often the out-of-memory killer. signal.Signals(15).name turns a number into 'SIGTERM'.
CalledProcessError's message also decodes this, saying "died with <Signals.SIGKILL: 9>".
Example
import signal, subprocess, sys
child = """
import signal, sys, time
signal.signal(signal.SIGTERM, lambda *a: (print("child: cleaning up", flush=True), sys.exit(0)))
time.sleep(30)
"""
p = subprocess.Popen([sys.executable, "-c", child])
try:
p.wait(timeout=0.5)
except subprocess.TimeoutExpired:
p.terminate() # child: cleaning up
print("graceful exit:", p.wait(timeout=5)) # graceful exit: 0
p = subprocess.Popen(["sleep", "30"])
p.kill()
code = p.wait()
print(code, signal.Signals(-code).name) # -9 SIGKILLRemember
- terminate() = SIGTERM (catchable), kill() = SIGKILL
- Always wait() after stopping a process to reap it
- returncode -N means killed by signal N; shells report 128+N
Watch out for
- Calling kill() first, so the child never gets to flush files or release locks.