2025, Dec 02 01:00
Run Multiple Programs with Python subprocess.Popen and Exit Cleanly When Every Child Closes
Launch multiple apps with Python subprocess.Popen, wait for each to exit, and let your launcher quit cleanly—without fragile string escaping or shell parsing.
When you use Python as a small launcher to start multiple desktop tools or CLI apps, you usually want the parent script to quit automatically as soon as all those child processes are closed. A common pitfall is to overprocess command strings before launching them, which can keep the script around longer than necessary or introduce fragile behavior. Here’s a focused walkthrough of how to run several programs and have the script exit as soon as every one of them has been closed.
Problem setup
The goal is simple: start four programs from Python and stop the script once they’re all closed. The initial approach below tries to insert backslashes before spaces in paths, builds a list of commands, and waits for each process:
import os
import sys
import subprocess
from subprocess import Popen
def inject_backslashes(pathname):
# inserts backslashes into paths that have spaces in them
chars = list(pathname)
j = 0
while j < len(chars):
if chars == " ":
chars.insert(j, "\\")
j += 2
else:
j += 1
return "".join(chars)
# set full Path to Emulator Executable:
emu_bin = inject_backslashes("/usr/bin/snes9x-gtk")
# set full Path to your Tracker:
tracker_bin = inject_backslashes("/usr/share/OpenTracker/OpenTracker")
# set full Path to Qusb or SNI:
bridge_bin = inject_backslashes("/bin/QUsb2Snes")
# set full Path to your Timer:
timer_bin = inject_backslashes("/home/user/LibreSplit/libresplit")
# Runs the Emulator, socket, Timer and Tracker
cmds = [bridge_bin, timer_bin, tracker_bin, emu_bin]
handles = [Popen(x) for x in cmds]
for h in handles:
h.wait()What’s really going on
The lifecycle control itself is correct. Each Popen(...) launches a program and returns immediately, and wait() blocks until the corresponding process exits. After the last child finishes, the script also finishes. The sticking point was how the commands were passed in. Manually manipulating command strings to “escape” spaces is unnecessary here and easy to get wrong. Switching to the actual executable paths (without extra quoting gymnastics) made the waits behave as intended: close all apps and the launcher terminates.
There’s also a performance angle. The initial character-by-character handling was replaced with a simpler transformation, which makes the helper faster while keeping the behavior consistent with the intended space handling.
Solution
Feeding the real paths and simplifying the space handling made the script exit cleanly once all programs were closed:
import os
import sys
import subprocess
from subprocess import Popen
def escape_space_chars(path_text):
# inserts a backslash before spaces
token = path_text.replace(" ", "\\ ")
return token
# Runs the Emulator, socket, Timer and Tracker
jobs = [
escape_space_chars("/usr/bin/snes9x-gtk"),
escape_space_chars("/usr/share/OpenTracker/OpenTracker"),
escape_space_chars("/usr/bin/QUsb2Snes"),
escape_space_chars("/home/user/LibreSplit/libresplit")
]
children = [Popen(item) for item in jobs]
for child in children:
child.wait()Why you should care
Process management is deterministic with subprocess.Popen and wait: start a process, wait for it, and the parent exits when children are done. Issues often creep in when commands are prepared as strings with custom escaping. This adds accidental complexity and can mask what the operating system actually receives as the program name and arguments.
There’s also a safety consideration. Constructing command lines with ad hoc escapes is error-prone and can be hazardous. A safer habit is to pass an explicit list to Popen so that no shell parsing is needed, and to do further processing directly in Python rather than relying on shell constructs. This approach avoids the class of problems associated with shell-style injection.
Takeaways
Use Popen to start each program and wait() to block until it closes; that’s enough for the parent script to quit once everything is done. Prefer feeding the actual executable paths rather than hand-escaped strings. Keep string handling minimal and straightforward, and consider passing a pre-split list of arguments to Popen to avoid escaping pitfalls altogether.