Any instance of a computer program that is being executed is referred to as a process. Thus, when you open your favorite application, open the command line interface or execute a Python script, you are literally initiating a process.

Processes can themselves initiate other processes. When this happens, the newly started process is referred to as a subprocess or a child process. The original process that initiated the creation of the child process is known as the "parent process." . This relationship leads to  a hierarchical structure where the child process can also start other processes and so on.

In this article, we will explore the process of initiating external programs or applications from within a Python program. 

The subprocess module

The subprocess module in the standard library provides the necessary tools for initiating external processes from within a Python program. It makes it possible to run commands programmatically like you would from the command line interface.

To use the functions defined in the subprocess module, we first need to import the module in our program, as in:

import subprocess

The most basic tool defined in the module is the subprocess.run() function. Before we look deeper on how the function works, let us first look at a simple example:

See the version of Python you are running

#import the subprocess module
import subprocess

#check the version of Python
result = subprocess.run(["python", "--version"], capture_output=True, text = True)

#get the output from the subprocess
print(result.stdout)

#get the status code
print("Return code:", result.returncode)

Running the above program is just a programmatic way of running the python --version command from the commandline/shell.

The subprocess.run() function has the following syntax:

subprocess.run(*popenargs, input=None, capture_output=False, timeout=None, check=False, **kwargs)
popenargs An iterable such as a list containing the commands to be executed as you would type them in the commandline. 
input Data to be sent to the process's standard input.
capture_output If True, the standard output and standard error of the executed process will be captured and returned.
check If True, a CalledProcessError exception will be raised if the process exits with a non-zero return code
**kwargs Additional keyword arguments that can be passed to modify the behavior of the function.

The subprocess after being executed returns three important values:

  1. returncode:

    • An integer representing the exit code of the process. A value of 0 usually indicates successful execution, while non-zero values typically signify an error or an abnormal termination.
  2. stdout:

    • This attribute contains the standard output of the process as a byte sequence. If you use capture_output=True in the subprocess.run() call, the captured standard output will be available in this attribute. You can set text = True to interpret the captured standard output as a standard string rather than a byte sequence
  3. stderr:

    • This attribute contains the standard error message raised by the subprocess, if any. Similar to stdout, it is populated only when capture_output=True

Check pip's version

import subprocess

result = subprocess.run(['pip', '--version'], capture_output = True, text = True)

print("stdout: ", result.stdout)
print("returncode: ", result.returncode)
print("stderr: ", result.stderr)

stdout:  pip 23.3.2 from C:\Users\John\AppData\Local\Programs\Python\Python311\Lib\site-packages\pip (python 3.11)


returncode:  0
stderr:

Run shell commands as a string

The shell argument in the subprocess.run() function determines whether the command should be run in a shell. When set to True, the command is run through the system shell (e.g., /bin/sh on Unix-like systems, or cmd.exe on Windows).

The shell argument allows commands to be run  as a string, similar to how you would run them directly on the shell.

import subprocess

# Using shell=True
result_shell = subprocess.run('echo Hello, World!', capture_output = True, shell=True, text=True)

print(result_shell.stdout)

Check where some programs are installed in your system 

import subprocess

#check where python is installed
python = subprocess.run('where python', capture_output = True, text = True, shell = True ) 
# check where pip is installed 
pip = subprocess.run('where pip', capture_output = True, text = True, shell = True) 

#print the outputs
print('python: ', python.stdout)
print('pip:', pip.stdout)

python:  C:\Users\John\AppData\Local\Programs\Python\Python311\python.exe

pip: C:\Users\John\AppData\Local\Programs\Python\Python311\Scripts\pip.exe

In the above example, we called the subprocess.run() function with the where command which checks the absolute path to the executable of a program given as the argument.

Raising Errors

By default, errors raised by a subprocess are not propagated to the calling program. However we can set the check parameter to True  in the subprocess.run() function to make it raise a CalledProcessError if the subprocess returns a non-zero exit code, indicating an error.

import subprocess

try:
    #callpython with a badly formated code
    result = subprocess.run(["python", "-c", 'print(Hello, World'], capture_output = True, text = True, check = True)
except subprocess.CalledProcessError as e:
    print(e)

Sending input values when calling a subprocess

Input values can be passed to the called subprocess as additional parameters to the popenargs argument. For example, consider  if we want to call Python as a subprocess with a script which expects arguments. We can pass the arguments, then access them from the called subprocess using the sys.argv list.

A Python file called foo.py 

#foo.py

import sys 
import subprocess

#get the arguments
print(sys.argv)
input1, input2 = int(sys.argv[1]), int(sys.argv[2])

def add(a, b):
    result = a + b
    return f"{a} + {b} = {result}"

print(add(input1, input2))

run foo.py as a subprocess from main.py

#main.py

#import the subprocess module
import subprocess

#call python with foo.py
result = subprocess.run(['python', 'foo.py', '10', '20'], capture_output = True, text = True)

#print the returned value
print(result.stdout)

['foo.py', '10', '20']
10 + 20 = 30

More advanced examples

Create a virtual environment

Create a virtual environment from within a Python script

#import the subprocess module
import subprocess

#Create a virtual environment from a python program
subprocess.run( ["python", "-m", "venv", "virtualenv"])

CompletedProcess(args='python -m venv venv', returncode=0)

If you execute the above snippet,  a new virtual environment with the name virtualenv will be created in the working directory.

Run pip programmatically

We can run pip as a subprocess to manage modules and packages programmatically. This is especially useful for automating installations and other package management tasks.

Install a package with pip 

#import the subprocess module
import subprocess

#install flask with pip
subprocess.run('pip install flask', shell = True)

Collecting flask
  Obtaining dependency information for flask from https://files.pythonhosted.org/packages/bd/0e/63738e88e981ae57c23bad6c499898314a1110a4141f77d7bd929b552fb4/flask-3.0.1-py3-none-any.whl.metadata.......

.......

Downloading MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl (17 kB)
Installing collected packages: MarkupSafe, itsdangerous, colorama, blinker, Werkzeug, Jinja2, click, flask
Successfully installed Jinja2-3.1.3 MarkupSafe-2.1.4 Werkzeug-3.0.1 blinker-1.7.0 click-8.1.7 colorama-0.4.6 flask-3.0.1 itsdangerous-2.1.2

In the above example, we have run pip as a subprocess to install the flask package programmatically. This is the equivalent of running the pip install flask command from the commandline.

List all installed packages

import subprocess

#check the installed packages
subprocess.run("pip list", shell = True)

package      Version
------------ -------
blinker      1.7.0
click        8.1.7
colorama     0.4.6
Flask        3.0.1
itsdangerous 2.1.2
Jinja2       3.1.3
MarkupSafe   2.1.4
pip          23.2.1
setuptools   65.5.0
Werkzeug     3.0.1