subprocess shell=True kill() Leaves Child Running (tested)
Calling .kill() on a subprocess.Popen(..., shell=True) object looks like it stops the command you launched. We measured what it actually stops: only the intermediate shell process. The real program the shell started keeps running, orphaned, with nothing left tracking it.
1. shell=True creates two processes, not one
import subprocess, time
proc = subprocess.Popen("sleep 8", shell=True)
print("shell wrapper pid:", proc.pid)
children = subprocess.run(["pgrep", "-P", str(proc.pid)], capture_output=True, text=True).stdout.split()
print("children of shell pid:", children)
shell wrapper pid: 2940024
children of shell pid: ['2940025']
Measured: proc.pid is the pid of /bin/sh -c "sleep 8", not of sleep itself. The shell parses the command and forks sleep as its own child process with a different pid, exactly as the subprocess docs describe for the shell=True equivalence to Popen(["/bin/sh", "-c", cmd]).
2. proc.kill() only kills the shell; the real command survives
proc.kill()
proc.wait()
print("shell wrapper returncode:", proc.returncode)
time.sleep(0.3)
import os
print("child still alive:", os.path.exists(f"/proc/{children[0]}"))
shell wrapper returncode: -9
child still alive: True
Measured: proc.kill() reports success — the returncode is -9 (killed by SIGKILL), with no error or warning. But that only terminated the shell wrapper process. The sleep 8 process it had already forked is now an orphan (reparented to init), completely untouched, and keeps running for its full 8 seconds with nothing left in your program referencing it.
3. Why: kill() signals one pid, not a process tree
Per the subprocess docs, Popen.kill()/.terminate() send a signal to exactly the pid stored in proc.pid — they have no concept of child processes. This also means subprocess.run(..., shell=True, timeout=N)'s automatic timeout-kill has the identical gap: it kills the shell on timeout, not whatever the shell was running, so a command like "long_running_tool | grep x" can keep its real work going in the background indefinitely after Python has already moved on, thinking the timeout cleaned it up.
4. Fix: put the shell in its own process group and kill the group
import os, signal, subprocess
proc = subprocess.Popen("sleep 8", shell=True, start_new_session=True)
children = subprocess.run(["pgrep", "-P", str(proc.pid)], capture_output=True, text=True).stdout.split()
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
proc.wait()
print("child still alive:", os.path.exists(f"/proc/{children[0]}"))
False
Measured: start_new_session=True makes the shell (and anything it forks) its own process group leader. Sending the kill signal to the whole group with os.killpg(os.getpgid(proc.pid), signal.SIGKILL) instead of proc.kill() terminates both the shell and the real command together — confirmed the child pid was gone immediately after. Do this for any shell=True process you might need to kill or time out, especially pipelines (cmd1 | cmd2) where the shell forks multiple children at once.
Frequently asked questions
Does subprocess.Popen(shell=True).kill() stop the command I actually ran?
Not necessarily — we measured proc.kill() terminating only the /bin/sh wrapper process. The real command the shell forked (e.g. sleep 8) kept running afterward as an orphan, untouched.
Why does shell=True create two processes for one command?
Because Popen(cmd, shell=True) runs /bin/sh -c cmd — the shell itself is proc.pid, and it then forks the actual program as its own child with a separate pid, which we confirmed by finding it via pgrep -P.
Does subprocess.run(..., shell=True, timeout=N) kill the real command on timeout?
We didn't measure this case directly, but it shares the exact same mechanism as .kill() — the timeout kills the pid subprocess tracks (the shell), not any children the shell forked, so the same orphaning risk applies, especially for shell pipelines.
How do I make sure killing a shell=True subprocess kills everything it started?
We confirmed that passing start_new_session=True to Popen, then calling os.killpg(os.getpgid(proc.pid), signal.SIGKILL) instead of proc.kill(), terminates the shell and all its child processes together.
Does this orphan problem happen without shell=True?
No — without shell=True, Popen's pid is the actual program you launched (no intermediate shell process), so .kill() directly terminates it. The gap only exists because shell=True inserts a shell process in between.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in subprocess module on Linux: launched Popen('sleep 8', shell=True), found the real child pid with pgrep -P, called proc.kill(), then checked /proc/<pid> for both the shell wrapper and its child. PlainHeaders is our extension; Python's subprocess module is built in.