There are many scenarios where you want to write a small programs that are designed to automate simple tasks. For instance, you want to resize thousands of images in your computer to a smaller sizes, or send email to multiple user whenever a row of data added into a csv file.

This is what people usually called scripting. And Python is a powerful programming language for this kind of job. Because, there are a bunch of built-in modules and third-party libraries that helps you to do some cool automation stuff with Python.

One of the problems you might encounter when you want to automate stuff is running other software that written in other programming languages. It could be the system software that built-in to your operating system. Or, it could be a third-party software that you have installed from the internet.

In effect, Python offers a built-in module called subprocess. It's a Python Standard Library that used to spawn new processes (programs), read the input/output/error, and obtain their return codes.

Getting Started

One of the very basic usage of subprocess module is by using the run method. This method allows you to run external program by passing the command as a string inside the first argument.

In this example, we're going to execute the ls command, which is a command that available in Unix operating systems (MacOS, Linux, etc.) to print all files and folders inside current directory.

demo.py
import subprocess

subprocess.run('ls')

The output will be:

$ python demo.py
demo.py		filename.txt

Running shell command

If you are on windows, and you are trying to run commands like dir, you will get an error. It's because that command is built-in to the shell. So, to fix it, you can add the shell=True argument to the run method like below:

subprocess.run('ls', shell=True)

Adding the shell=True argument also allows you to run your entire command as a string. Because, if you dont enable the shell argument, you actually can't pass the entire command as a string like we did. Instead, we need to pass our command as a list of arguments.

subprocess.run(['ls', '-la'])

Capturing output

If you pay attention to the console, you will see that the command result is automatically printed out to the console. Even if we don't use print function by ourself. That's because the subprocess.run method doesn't capture our command output by default.

To prove this, we can try to capture our command by putting it into a variable.

result = subprocess.run('ls')
print(result)

You will see something like this:

demo.py		filename.txt
CompletedProcess(args='ls', returncode=0)

First of all, the interpreter execute the subprocess.run method, run the ls command, and print the output to the terminal. Then, we try to print the value inside the result variable and get this weird CompletedProcess object. What's going on here?

As I said, the subprocess.run method doesn't return the process ouput. It just print the result to the console. One way to get the process output is by adding the capture_output=True named argument, and access the stdout property of the result.

result = subprocess.run('ls', capture_output=True)
print(result.stdout)

Output:

demo.py		filename.txt

Now, instead of printing out the output directly to the terminal, the run method return the output to us, so we can grab it and print it out the the terminal by ourself.

Decode Bytes to String

There is still a problem though. If you check the type of our result.stdout, you can see that the type is not string, it's bytes. So what's the difference?

result = subprocess.run('ls', capture_output=True)
print(type(result.stdout))

Output:

<class 'bytes'>

Well, the big difference of bytes and string in Python is that bytes is immutable / cannot be modified. Bytes object are machine readable, in other hand, strings are human readable.

To convert bytes object into strings, we can use the decode method from the bytes object.

result = subprocess.run('ls', capture_output=True)
print(result.stdout.decode())

Or, we can add text=True parameter to our run method. This makes it automatically decode our output into a string.

result = subprocess.run('ls', capture_output=True, text=True)
print(result.stdout)

Checking Errors

To make sure that your command runs properly without any error, you can check the return code of your process. Usually, if the return code is 0, your code run successfully. Otherwise, if the return code is 1, something went wrong.

For example, if we want pass the a wrong argument to the program we want to run, it will throw an error like below.

# notexist folder is not exist!
result = subprocess.run(['ls', 'notexist'])

The output:

ls: notexist: No such file or directory

To make sure if the program doesn't throws an error, we can check the return code like below.

if result.returncode == 0:
    # Do something
else:
    print("Something went wrong!")

Also, the result comes with the stderr property. Use it if you want to see the error message.

if result.returncode == 0:
    # Do something
else:
    print(result.stderr)