Skip to main content

Lib/subprocess.py

cpython 3.14 @ ab2d84fe1023/Lib/subprocess.py

subprocess is a pure-Python module with a small C helper (_posixsubprocess) for the fork/exec path on POSIX. It provides two layers of API: the high-level convenience functions (run, call, check_call, check_output) that each create a Popen object, wait for it, and return or raise; and the low-level Popen class that gives full control over file descriptors, environment, working directory, and process group setup.

Sentinel constants PIPE, DEVNULL, and STDOUT are negative integers recognized by Popen.__init__ to create pipes, redirect to /dev/null, or merge stderr into stdout. On POSIX, _execute_child chooses between os.posix_spawn (when no incompatible options are set) and a fork+exec sequence. On Windows the entire path goes through _winapi.CreateProcess.

Map

LinesSymbolRolegopy
1-100Module header, sentinel constants, CompletedProcessPIPE=-1, STDOUT=-2, DEVNULL=-3; CompletedProcess is a simple result container with check_returncode().(stdlib pending)
100-300SubprocessError, CalledProcessError, TimeoutExpiredException hierarchy; CalledProcessError carries returncode, cmd, output, and stderr; TimeoutExpired carries timeout and captured I/O.(stdlib pending)
300-700Popen.__init__, _get_handles (POSIX), _execute_child (POSIX)Argument normalization, pipe creation, posix_spawn fast path, and full fork+exec fallback via _posixsubprocess.fork_exec.(stdlib pending)
700-1100Popen.communicate, _communicate, _readerthread, _waitDeadlock-safe bidirectional I/O using threads or select; _wait polls os.waitpid in a loop with WNOHANG.(stdlib pending)
1100-1600run, call, check_call, check_outputHigh-level wrappers; run adds input, capture_output, timeout, and check parameters; the others are compatibility shims delegating to run.(stdlib pending)
1600-2600Popen.__init__ (Windows), _execute_child (Windows), _winapi pathCreateProcess call, handle duplication via DuplicateHandle, _handle wrapper, and Windows-only parameters creationflags, startupinfo.(stdlib pending)

Reading

_execute_child fork/exec sequence (lines 300 to 700)

cpython 3.14 @ ab2d84fe1023/Lib/subprocess.py#L300-700

def _execute_child(self, args, executable, preexec_fn, close_fds,
pass_fds, cwd, env, ...):
# Fast path: use posix_spawn when possible
if (not close_fds and
not pass_fds and
not preexec_fn and
not cwd and
not env):
try:
os.posix_spawn(executable, args, env, ...)
return
except (AttributeError, OSError):
pass

# Slow path: fork + exec
errpipe_read, errpipe_write = os.pipe()
try:
self.pid = _posixsubprocess.fork_exec(
args, executable_list,
close_fds, sorted(pass_fds or []), cwd, env or os.environ,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite,
errpipe_read, errpipe_write,
restore_signals, start_new_session,
gid, extra_groups, uid, child_umask,
child_umask,
preexec_fn,
)
finally:
os.close(errpipe_write)
# Read any exec error from child via errpipe
exception_name, hex_errno, err_msg = (
errpipe_data.split(b':', 2))
if exception_name:
child_exec_never_called = ...
raise child_exception_type(...)

_execute_child first attempts os.posix_spawn when no incompatible options are requested. posix_spawn is significantly faster because it avoids copying the parent's entire address space; the kernel sets up file descriptors and executes the target atomically. When posix_spawn is unavailable or an incompatible option is set (such as preexec_fn, custom cwd, or close_fds=True with pass_fds), the code falls back to _posixsubprocess.fork_exec.

fork_exec is a C function that forks, sets up file descriptors in the child, runs preexec_fn if provided, and calls execve. Errors in the child process are communicated back to the parent through errpipe: the child writes the exception class name, errno, and message as ASCII bytes before exiting, and the parent reads and re-raises them. This two-pipe design (one for the child's stdout/stderr, one for exec errors) means exec failures are reported as Python exceptions rather than as mysterious non-zero exit codes.

communicate deadlock avoidance (lines 700 to 1100)

cpython 3.14 @ ab2d84fe1023/Lib/subprocess.py#L700-1100

def communicate(self, input=None, timeout=None):
if self._communication_started and input:
raise ValueError("Cannot send input after starting communication")

if timeout is not None:
endtime = _time() + timeout
else:
endtime = None

try:
stdout, stderr = self._communicate(input, endtime, timeout)
except:
...
finally:
self._communication_started = True

sts = self.wait(timeout=self._remaining_time(endtime))
return (stdout, stderr)
def _communicate(self, input, endtime, timeout):
# Use threads on all platforms; use select on POSIX when no input
if self.stdin and not self._communication_started:
self.stdin_thread = threading.Thread(
target=self._readerthread,
args=(self.stdin, input_view))
self.stdin_thread.daemon = True
self.stdin_thread.start()
...
if self.stdout:
self.stdout_thread = ...
if self.stderr:
self.stderr_thread = ...
# Wait with timeout
self.stdout_thread.join(self._remaining_time(endtime))
...

The classic deadlock scenario is: parent writes to child's stdin while the child's stdout pipe fills its kernel buffer, blocking the child from writing, which prevents it from reading from stdin, deadlocking both sides. communicate avoids this by reading stdout and stderr in background daemon threads while the main thread writes stdin (or waits for completion if there is no input). On POSIX without input, select-based polling is used instead of threads to reduce overhead. The _readerthread approach is always used on Windows because select does not work on pipes there.

run timeout and process cleanup (lines 1100 to 1600)

cpython 3.14 @ ab2d84fe1023/Lib/subprocess.py#L1100-1600

def run(*popenargs, input=None, capture_output=False,
timeout=None, check=False, **kwargs):
if capture_output:
if ('stdout' in kwargs or 'stderr' in kwargs):
raise ValueError(...)
kwargs['stdout'] = PIPE
kwargs['stderr'] = PIPE

with Popen(*popenargs, **kwargs) as process:
try:
stdout, stderr = process.communicate(input, timeout=timeout)
except TimeoutExpired as exc:
process.kill()
exc.stdout, exc.stderr = process.communicate()
raise
except:
process.kill()
raise
retcode = process.poll()
if check and retcode:
raise CalledProcessError(retcode, process.args,
stdout, stderr)
return CompletedProcess(process.args, retcode, stdout, stderr)

run wraps Popen in a context manager so that file descriptors are always closed on exit. On TimeoutExpired, it kills the process with SIGKILL (on POSIX) and then calls communicate() once more to drain any buffered output before re-raising. This second communicate call is essential: without it the pipe buffers may still hold data and the child process would remain a zombie until the buffers are consumed. The check flag turns a non-zero return code into a CalledProcessError, which carries the captured stdout and stderr for diagnostic use.

gopy mirror

subprocess depends on os.fork, os.execve, os.pipe, os.waitpid, os.posix_spawn, select.select, and threading.Thread, all of which map to Go's syscall and os/exec packages. The gopy port will expose Popen as a Go struct wrapping *exec.Cmd, implement communicate using goroutines in place of threads, and raise TimeoutExpired / CalledProcessError as gopy exception objects. The Windows _winapi path is out of scope for the initial port.