Skip to content

Mastering Shell Commands in Python: A Practical Guide for Windows and Linux

Why This Guide?

If you're familiar with Python 3, you likely know that subprocess is a powerful tool for running shell commands. It enables Python to interact with system commands, offering a wide range of functionalities.

However, subprocess can be complex, with numerous options and nuanced behaviors. Dealing with pipes, input/output, error handling, and encoding issues can be confusing. Often, we rely on snippets of code that "just work" without fully understanding why they work or when a different approach is needed.

Today, I've revisited the official documentation and re-learned subprocess to gain a comprehensive understanding. My goal is to provide a clear and thorough guide, enabling you to confidently use subprocess in various scenarios and choose the right options for your specific needs!


What is subprocess, and Why Use It?

subprocess is a Python module designed to execute system commands. For example:

  • On Linux, running ls -l to list directory contents.
  • On Windows, running dir to view files.

Why not just use shell scripts? Python code offers better clarity, maintainability, and the ability to integrate logic directly within your code. subprocess.run (introduced in Python 3.5) is the most versatile function, and we'll focus on it in this guide.


subprocess.run Parameter Overview

subprocess.run has many parameters. Let's introduce them upfront before diving into specific use cases. This will give you a general understanding of each option's purpose.

ParameterDescriptionCommon ValuesNotes
argsThe command to runList (e.g., ["ls", "-l"]) or string (e.g., "ls -l")List for no shell, string for shell=True
shellWhether to execute the command via a shellTrue / False (default)True is less efficient; required for Windows built-in commands
capture_outputWhether to capture stdout and stderrTrue / False (default)Equivalent to stdout=PIPE, stderr=PIPE
stdoutDestination for standard outputPIPE / None / STDOUT / File objectPIPE captures; None displays in terminal
stderrDestination for standard errorPIPE / None / STDOUT / File objectSTDOUT merges with stdout
textWhether to return strings directlyTrue / False (default)True requires encoding; avoids manual decoding
encodingOutput encoding"utf-8" / "gbk" etc.Recommend "utf-8"; consider system encoding on Windows
errorsError handling during decoding"strict" / "ignore" / "replace"Only applicable when text=True
inputInput data for the commandString (e.g., "data")String with text=True; bytes otherwise
checkCheck return code; raise exception on failureTrue / False (default)Raises CalledProcessError

Return Object (cp or exception e):

  • cp.returncode: Return code (0 for success, non-0 for failure).
  • cp.stdout: Standard output.
  • cp.stderr: Standard error output (or logs, e.g., from FFmpeg).
  • e.stdout / e.stderr: Output and error during an exception.

Core Scenarios and Usage: From Simple to Complex

Let's use these parameters to explore common scenarios, starting with the basics.

Scenario 1: Running a Simple Command and Capturing Output

Linux: Running echo

python
import subprocess

cp = subprocess.run(
    args=["echo", "hello world"],
    capture_output=True,    # Capture stdout and stderr
    text=True,             # Return strings directly
    encoding="utf-8"       # UTF-8 encoding
)
print(cp.stdout)  # Output: hello world

Windows: Running dir

python
import subprocess

cp = subprocess.run(
    args="dir",
    shell=True,            # Windows built-in commands require a shell
    capture_output=True,
    text=True,
    encoding="utf-8"
)
print(cp.stdout)  # Output: Directory listing

Key Points:

  • capture_output=True simplifies capturing output.
  • Linux uses a list for args, Windows built-in commands need shell=True.

When to Use?

  • Running simple commands and needing the results.

Scenario 2: Running Complex Commands (with Pipes |)

Linux: Running ls -l | grep file

python
import subprocess

cp = subprocess.run(
    args="ls -l | grep file",
    shell=True,
    capture_output=True,
    text=True,
    encoding="utf-8"
)
print(cp.stdout)  # Output: Filtered list of files

Windows: Running dir | find "txt"

python
import subprocess

cp = subprocess.run(
    args='dir | find "txt"',
    shell=True,
    capture_output=True,
    text=True,
    encoding="utf-8"
)
print(cp.stdout)  # Output: Lines containing "txt"

Key Point:

  • shell=True enables the use of pipes.

When to Use?

  • Combining commands to filter output.

Scenario 3: Capturing Output and Errors Separately

Linux: Running ls on a Non-Existent File

python
import subprocess

cp = subprocess.run(
    args=["ls", "nope"],
    stdout=subprocess.PIPE,  # Capture standard output separately
    stderr=subprocess.PIPE,  # Capture standard error separately
    text=True,
    encoding="utf-8"
)
print(f"Output: {cp.stdout}")
print(f"Error: {cp.stderr}")  # Output: ls: cannot access 'nope'

Windows: Running dir on a Non-Existent File

python
import subprocess

cp = subprocess.run(
    args="dir nope",
    shell=True,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True,
    encoding="utf-8"
)
print(f"Output: {cp.stdout}")
print(f"Error: {cp.stderr}")  # Output: File not found

Key Point:

  • stdout=PIPE, stderr=PIPE capture output and error streams independently.

When to Use?

  • When you need to differentiate between standard output and error messages.

Scenario 4: Checking Results and Handling Exceptions

Linux: Checking Return Code vs. Raising Exception

python
import subprocess

# Method 1: Check the return code
cp = subprocess.run(
    args=["ls", "nope"],
    capture_output=True,
    text=True,
    encoding="utf-8"
)
if cp.returncode != 0:
    print(f"Failure! Return code: {cp.returncode}")
    print(f"Error: {cp.stderr}")

# Method 2: Use check=True
try:
    cp = subprocess.run(
        args=["ls", "nope"],
        capture_output=True,
        text=True,
        encoding="utf-8",
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Failure! Return code: {e.returncode}")
    print(f"Output: {e.stdout}")
    print(f"Error: {e.stderr}")

Windows: Similar Approach

python
import subprocess

try:
    cp = subprocess.run(
        args="dir nope",
        shell=True,
        capture_output=True,
        text=True,
        encoding="utf-8",
        check=True
    )
except subprocess.CalledProcessError as e:
    print(f"Failure! Return code: {e.returncode}")
    print(f"Output: {e.stdout}")
    print(f"Error: {e.stderr}")

Key Points:

  • capture_output=True only captures the output.
  • check=True raises an exception if the command fails.

When to Use?

  • When you need to ensure that a command executes successfully.

Scenario 5: Calling External Programs (e.g., FFmpeg)

Special Note: FFmpeg's normal logging goes to stderr, not stdout.

Transcoding a Video File

Using FFmpeg to convert input.mp4 to output.mp3:

python
import subprocess

cp = subprocess.run(
    args=["ffmpeg", "-i", "input.mp4", "output.mp3", "-y"],
    capture_output=True,
    text=True,
    encoding="utf-8"
)
if cp.returncode == 0:
    print("Transcoding successful!")
    print(f"Log: {cp.stderr}")  # FFmpeg's normal logging is in stderr
else:
    print(f"Transcoding failed! Return code: {cp.returncode}")
    print(f"Output: {cp.stdout}")
    print(f"Error details: {cp.stderr}")

# Or use check=True
try:
    cp = subprocess.run(
        args=["ffmpeg", "-i", "input.mp4", "output.mp3", "-y"],
        capture_output=True,
        text=True,
        encoding="utf-8",
        check=True
    )
    print("Transcoding successful!")
    print(f"Log: {cp.stderr}")  # FFmpeg's normal logging is in stderr
except subprocess.CalledProcessError as e:
    print(f"Transcoding failed! Return code: {e.returncode}")
    print(f"Output: {e.stdout}")
    print(f"Error details: {e.stderr}")

Key Points:

  • Use a list for calling external programs.
  • FFmpeg's normal logs are in stderr; failure information is also in stderr; stdout is usually empty.
  • When check=True, successful logs are in cp.stderr, and failure information is in e.stderr.

When to Use?

  • When processing audio/video or performing file conversions.

Scenario 6: Providing Input Data to a Command

python
import subprocess

data = "line1\nline2 py\nline3"
cp = subprocess.run(
    args=["grep", "py"],
    input=data,
    capture_output=True,
    text=True,
    encoding="utf-8"
)
print(cp.stdout)  # Output: line2 py

Key Point:

  • input provides data to the command's standard input.

When to Use?

  • When processing data generated by your program.

In-Depth Option Combinations

1. shell=True vs. False?

  • False: Efficient and secure; use a list for args.
  • True: Supports complex commands and pipes; use a string for args.

2. capture_output=True vs. check=True

  • capture_output=True: Captures output and errors, regardless of success.
  • check=True: Checks the return code and raises an exception on failure.
  • Combination:
    python
    cp = subprocess.run(["ls", "nope"], capture_output=True, check=True, text=True, encoding="utf-8")

3. stdout and stderr

  • PIPE: Captures the output in your program.
  • None: Displays the output in the terminal.
  • STDOUT (only for stderr): Merges stderr into stdout.
  • File: Writes the output to a file.

4. text=True

  • Purpose: Returns strings directly; requires specifying encoding.
  • Without: Returns bytes; requires manual decoding.

5. encoding and errors

  • encoding="utf-8": Recommended encoding.
  • errors="replace": Prevents encoding errors and replaces problematic characters.

Common Issues

  1. Why is shell=True often needed on Windows?

    • Built-in commands rely on cmd.exe.
  2. What if FFmpeg output goes to stderr?

    • Use capture_output=True. Both normal logs and errors will be in cp.stderr or e.stderr.
  3. How to handle encoding errors?

    • text=True, encoding="utf-8", errors="replace".

Which Options for Which Scenario?

ScenarioCommand FormatRecommended Options
Simple Command["cmd", "arg"]capture_output=True, text=True
Complex Command`"cmd1cmd2"`
Separate Capture["cmd", "arg"]stdout=PIPE, stderr=PIPE
Check SuccessAdd check=Truecapture_output=True
External Tool["ffmpeg", "-i", "in", "out"]capture_output=True, check=True
Provide InputUse inputtext=True, encoding="utf-8"

Key Takeaways:

  • Use capture_output=True to simplify capturing output.
  • text=True makes handling text easier; check=True ensures strict error handling.
  • Pay attention to stderr when working with tools like FFmpeg.