Back to Blog

Mastering External Command Execution in Python: A Step-by-Step Guide to Running Shell Commands Like a Pro

Lineserve TeamLineserve Team
December 12, 2025
5 min read

Ever found yourself needing to run a shell command from within your Python script, like listing files in a directory or executing a system utility? If you’re an intermediate Python developer looking to master external command execution, you’re in the right place. In this tutorial, we’ll dive into the powerful subprocess module, which is the modern, secure way to call external commands in Python. Forget the old days of os.system()—we’ll guide you through best practices, practical examples, and pitfalls to avoid, ensuring you can integrate shell commands seamlessly into your applications.

Why Use Subprocess for External Commands?

Python’s subprocess module provides a robust interface to spawn new processes, connect to their input/output/error pipes, and obtain their return codes. Unlike older methods like os.system(), which can be insecure and less flexible, subprocess offers fine-grained control and is the recommended approach for executing external commands.

Key Differences Between Subprocess Methods

Before we jump into code, let’s clarify the main methods in subprocess:

  • subprocess.run(): The modern, high-level function for running commands. It replaces older functions like call() and check_output(), offering simplicity and safety.
  • subprocess.call(): Runs a command and returns the return code. It’s basic and doesn’t capture output easily.
  • subprocess.check_output(): Runs a command and returns its output as bytes, raising an exception on non-zero exit codes.
  • subprocess.Popen(): Low-level class for more complex interactions, like streaming output or running in parallel.

For most use cases, subprocess.run() is your go-to. Let’s see it in action.

Getting Started with subprocess.run()

The subprocess.run() function takes at least one argument: the command as a list of strings (to avoid shell injection vulnerabilities). Here’s a basic example:

import subprocess

# Run a simple command: list files in the current directory
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
print(result.stdout)

In this code, ['ls', '-l'] is the command and its arguments. capture_output=True captures stdout and stderr, and text=True returns strings instead of bytes. Always pass commands as lists to prevent shell parsing issues.

Handling Errors and Return Codes

Commands can fail, so proper error handling is crucial. The result object from run() has attributes like returncode, stdout, and stderr. Let’s add error checking:

import subprocess

# Run a command and check for errors
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
if result.returncode == 0:
    print("Success:")
    print(result.stdout)
else:
    print("Error:")
    print(result.stderr)

If the command fails (non-zero return code), you can inspect result.stderr for details. For automatic exception raising on failure, use check=True:

try:
    result = subprocess.run(['ls', '/nonexistent'], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e:
    print(f"Command failed with code {e.returncode}: {e.stderr}")

Practical Examples and Use Cases

Let’s apply this to real-world scenarios. Suppose you’re building a script to automate file backups.

Example 1: Listing Files with Conditional Output

import subprocess

# List only Python files
result = subprocess.run(['ls', '*.py'], capture_output=True, text=True, shell=True)
print(result.stdout or "No Python files found.")

Note: Using shell=True here allows shell globbing (like *.py), but it increases security risks. Only use it when necessary and validate inputs.

Example 2: Executing a Script

import subprocess

# Run a bash script
result = subprocess.run(['bash', 'myscript.sh'], capture_output=True, text=True)
print(f"Script output: {result.stdout}")

This is great for integrating Python with shell scripts in deployment pipelines.

Example 3: Piping Commands

For more complex commands, you might need to pipe output. Use subprocess creatively:

import subprocess

# Simulate piping: list files and count them
p1 = subprocess.Popen(['ls'], stdout=subprocess.PIPE)
p2 = subprocess.Popen(['wc', '-l'], stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
output, _ = p2.communicate()
print(f"Number of files: {output.decode().strip()}")

This uses Popen for piping, a common pattern in shell scripting.

Best Practices and Tips

  • Always use lists for commands: Prevents shell injection. Never pass user input directly into shell=True.
  • Capture output wisely: Use capture_output=True for simplicity, but for large outputs, consider streaming with Popen.
  • Security first: Avoid shell=True unless essential. Sanitize all inputs.
  • Timeout for long-running commands: Add timeout=10 to prevent hangs.
  • Cross-platform compatibility: Commands like ls are Unix-specific; for Windows, use dir or check platform.system().

Common Pitfalls to Avoid

  1. Forgetting to handle return codes: Always check returncode to avoid silent failures.
  2. Mixing bytes and strings: Use text=True or decode manually to avoid encoding issues.
  3. Ignoring stderr: Capture both stdout and stderr for complete error diagnosis.
  4. Overusing shell=True: It can lead to security vulnerabilities; prefer list-based commands.
  5. Not testing on different systems: Commands behave differently on Windows vs. Unix—test thoroughly.

Summary and Next Steps

In this tutorial, we’ve explored how to execute external commands in Python using the subprocess module, focusing on subprocess.run() for its simplicity and security. You learned to handle outputs, errors, and real-world use cases like file listing and script execution, while adhering to best practices to keep your code robust and secure. Remember, subprocess gives you the power of the shell without the risks when used correctly.

Next, practice with your own scripts—try integrating a command into an automation tool. For deeper dives, check Python’s official docs or explore libraries like sh for even easier command wrapping. Happy coding!

Share this article

Lineserve Team

Lineserve Team

Lineserve Team is a contributor at the Hostraha blog, sharing insights on web hosting, cloud infrastructure, and web development.

Enjoy this article?

Subscribe to our newsletter for more hosting tips, tutorials, and special offers.