Running Bash commands in Python
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Python can run shell commands, but the right tool is almost always subprocess, not os.system. The difference matters because subprocess lets you capture output, check exit codes, control environment variables, and avoid common shell-injection mistakes.
Use subprocess.run for most commands
For one-shot commands, subprocess.run is the default choice:
A few details here are important:
- Pass the command as a list so Python does the argument splitting safely.
- Use
capture_output=Trueif you needstdoutandstderrin Python. - Use
text=Trueto get strings instead of raw bytes. - Use
check=Trueso Python raises an exception when the command fails.
This is much safer than building one shell string and hoping quoting rules work out.
Read exit codes and handle errors
Sometimes you do not want an exception on failure. In that case, inspect the return code yourself:
This is a good pattern when the command's nonzero exit codes have domain-specific meanings rather than representing a hard failure every time.
Use shell=True only when you need real shell features
If you need pipes, glob expansion, or shell built-ins, you may be tempted to use shell=True:
This works, but it is also the risky mode. If any part of that command string contains user input, you may have a shell-injection vulnerability.
Whenever possible, replace shell pipelines with plain Python or separate subprocess calls. For example, a safer version of the previous command could parse the file in Python directly.
Stream output with Popen for long-running commands
When the command runs for a long time and you want to consume output as it arrives, use subprocess.Popen:
This is useful for build logs, deployment scripts, and any command where waiting for all output at the end would be too late.
Pass environment variables and working directories
Two other subprocess features are extremely useful in automation scripts:
cwd changes the working directory for the child process, and env lets you override or add environment variables without affecting the current Python process.
Common Pitfalls
The biggest mistake is using shell=True for commands that could be expressed as a simple argument list. That creates quoting bugs and security problems for no real benefit.
Another common issue is ignoring the exit code. A command may print something useful and still fail, so always check returncode or use check=True.
People also forget the difference between bytes and text. Without text=True, stdout and stderr come back as bytes objects.
Finally, do not use os.system for new code unless you genuinely need the simplest possible fire-and-forget call and do not care about output or structured error handling.
Summary
- Use
subprocess.runas the default way to run Bash or shell commands from Python. - Pass commands as argument lists instead of raw shell strings whenever possible.
- Check exit codes or use
check=Trueso failures do not go unnoticed. - Use
Popenwhen you need streaming output from long-running processes. - Avoid
shell=Trueunless you explicitly need shell syntax and trust the input.

