Why os.system is the “Achilles’ heel” of automation scripts
When I first started learning Python, I often chose os.system() because it was convenient. A single line of code could run ls or ping. However, trouble started when my automation scripts grew from 200 to over 2,000 lines. The system began freezing for unknown reasons. Outputs from different processes got mixed up. Worse yet, I almost leaked server information due to a shell injection vulnerability while handling user input.
In reality, os.system is like throwing a grenade into the terminal and hoping for the best. It doesn’t tell you what happened inside. To write professional tools, you need subprocess. This library provides absolute control over data streams (stdin, stdout, stderr) and manages processes scientifically.
Choosing the Right Weapon: .run() or .Popen()?
Choosing the right function from the start can save you 50% of your refactoring time later. Here are the three most common options:
1. os.system – An Outdated Legacy
- Limitations: Only returns the exit code (0 for success). You are completely “blind” to the text content printed by the command.
- Risks: Cannot block malicious characters from users, making it vulnerable to shell injection attacks.
2. subprocess.run() – The Gold Standard
- Use Case: For 95% of daily tasks. It waits for the command to finish and returns an object containing everything you need.
- Characteristics: This is blocking. The program will pause until the command execution is complete.
3. subprocess.Popen – For OS Power Users
- Use Case: Suitable for running tasks in parallel or reading real-time logs from a running server.
- Characteristics: Non-blocking. You can run the command while executing other Python logic simultaneously.
Implementing subprocess.run: The Best Practice
Forget old functions like os.popen. Since Python 3.5, subprocess.run has been the cleanest and safest API.
Running Commands and Capturing Results
For example, if you need to check a file list and process that content in Python:
import subprocess
# Run command and capture output
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
if result.returncode == 0:
print("Success!")
print(result.stdout) # Clean text content
else:
print(f"Failed with error: {result.stderr}")
Important Note: Always pass the command as a list (["ls", "-l"]). This way, Python automatically handles spaces and special characters, protecting your script from dangerous injection attacks.
Automating Error Handling
Instead of writing continuous if/else statements, use the check=True parameter. Python will automatically raise an exception if the command fails, making your code 30% cleaner.
try:
# If 'git pull' fails, the script stops and jumps to the except block immediately
subprocess.run(["git", "pull"], check=True)
except subprocess.CalledProcessError as e:
print(f"Git Error: {e}")
Piping Techniques and System Security
Many people have a habit of using shell=True to write complex commands like cat file.txt | grep keyword. Don’t do that! shell=True is a massive security loophole. If the input variable contains the string ; rm -rf /, your server could vanish in an instant.
The safest way to connect commands (pipes) is through Python:
import subprocess
# Command 1: Read file
p1 = subprocess.Popen(["cat", "data.log"], stdout=subprocess.PIPE)
# Command 2: Filter data from Command 1's output
p2 = subprocess.Popen(["grep", "ERROR"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close() # Allow p1 to receive a termination signal
output, _ = p2.communicate()
print(f"Error lines: \n{output}")
3 “Survival” Rules When Working with Subprocess
After years of operating CI/CD systems, here is the hard-earned experience I’ve gathered:
- Always Set a Timeout: Never run a command without a time limit. A command hanging due to network congestion can freeze your entire system. Use
timeout=30to automatically kill overdue processes. - Prioritize text=True: By default, subprocess returns
bytes. Instead of manually using.decode('utf-8'), enabletext=Trueto work with strings immediately. - Separate Stderr: Do not merge info logs and error logs. Keeping stderr separate helps you debug twice as fast when issues occur.
Conclusion
Using subprocess correctly not only makes your code more professional but also protects your system from unnecessary security risks. Start replacing os.system with subprocess.run today to experience the difference in process management.

