Use Python’s subprocess.run() to start a Bash script. For a normal script, pass the interpreter, script path, and each script argument as separate items in a list; leave shell=False (the default). Add check=True to raise an exception when the script exits unsuccessfully, and use capture_output=True and text=True when you need its output as strings.
Run a Bash script with Python
This example runs a script through Bash, captures both output streams, and raises subprocess.CalledProcessError if the script returns a nonzero exit status:
import subprocess
result = subprocess.run(
["bash", "script.sh"],
check=True,
capture_output=True,
text=True,
)
print(result.stdout)
Replace script.sh with the path to your file. On a POSIX system, you can make the interpreter choice explicit with an absolute path such as /bin/bash. That is useful when you need a particular Bash executable rather than whichever bash appears first on PATH.
The Python documentation recommends subprocess.run() for subprocess use cases it can handle. Passing arguments as a sequence is generally preferred because Python preserves argument boundaries and handles the required quoting, including paths with spaces. See the Python subprocess documentation.
#1 Best Overall
What the options do
["bash", "script.sh"]starts Bash and tells it which script to run. It does not require the script to be executable.check=Truemakes a nonzero exit status raiseCalledProcessError. If you omit it, inspectresult.returncodeyourself.capture_output=Truecollects standard output and standard error in memory.text=Truedecodes captured output into strings rather than returning bytes.
When output might be large or continuous, consider whether collecting it all in memory is appropriate. For simpler, bounded output, captured strings are convenient; otherwise, redirect output to a file or handle a stream using a subprocess API suited to that need.
Pass arguments safely
Add every script argument as its own list item after the script path. Do not join values into a command string.
import subprocess
filename = "quarterly report.csv"
result = subprocess.run(
["bash", "scripts/analyze.sh", filename, "--format", "json"],
check=True,
capture_output=True,
text=True,
)
print(result.stdout)
The script receives the filename as one argument even though it contains a space. In Bash, positional parameters are available as $1, $2, and so on; use quoted expansions such as "$1" inside the script to preserve each argument as a single value.
Keeping shell=False avoids asking a shell to reinterpret data as syntax. This is the right pattern for filenames and values that might contain spaces or shell metacharacters.
Rank #2
Choose how to handle output and failures
Raise immediately on a failed script
For a script that must succeed before Python continues, use check=True. A nonzero exit code raises an exception, so catch it if your application needs to log the error or recover:
import subprocess
try:
result = subprocess.run(
["bash", "script.sh"],
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as exc:
print("Exit code:", exc.returncode)
print("Standard error:", exc.stderr)
raise
else:
print(result.stdout)
A process can start successfully yet the script can still fail. CalledProcessError represents the latter: the child process returned a nonzero status. With output capture enabled, the exception includes captured output attributes such as stdout and stderr.
Inspect the return code without an exception
If a nonzero exit is an expected branch in your program, omit check=True and test the status explicitly:
import subprocess
result = subprocess.run(
["bash", "script.sh"],
capture_output=True,
text=True,
)
if result.returncode != 0:
message = result.stderr.strip() or "Bash script failed"
raise RuntimeError(message)
print(result.stdout)
stdout and stderr are separate streams. Capturing them keeps diagnostic messages from being mixed into normal output, but it does not automatically display either stream. Decide whether to print, log, return, or otherwise process each one.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Set the working directory and environment
Relative paths in a script are resolved from its working directory, which may not be the directory containing the Python file. Set cwd when the script expects to run from a particular project directory. To add or override environment variables without discarding the rest of the parent process environment, copy os.environ and update the copy:
import os
import subprocess
child_env = os.environ.copy()
child_env["MODE"] = "production"
result = subprocess.run(
["bash", "scripts/deploy.sh"],
cwd="/srv/my-app",
env=child_env,
timeout=30,
check=True,
capture_output=True,
text=True,
)
print(result.stdout)
cwd sets the child process’s working directory. env supplies its environment; if you pass a new mapping without copying the existing environment, variables the script depends on may be absent. Do not put secrets into logs or error messages when passing sensitive values through the environment.
Bound execution time
timeout=30 gives the child process 30 seconds to finish. If that deadline is exceeded, subprocess.run() raises subprocess.TimeoutExpired. Catch the exception if your application needs to report a timeout, choose a retry policy, or perform other recovery. A timeout should be selected for the job’s expected duration rather than treated as proof that the script itself is defective.
Run an executable script directly
If the script has a valid shebang and its executable permission is set, you can run it directly instead of naming Bash as the executable:
import subprocess
result = subprocess.run(
["/path/to/script.sh", "first-arg"],
check=True,
capture_output=True,
text=True,
)
print(result.stdout)
This approach relies on the operating system being able to execute the file and use its shebang interpreter. If you specifically want Bash to interpret the file, use ["/bin/bash", "/path/to/script.sh"] instead. Explicitly selecting Bash also avoids relying on the script’s executable bit.
When to use shell=True
A normal script path does not need shell=True. Enable a shell only when the command itself needs shell syntax such as a pipeline, glob expansion, or shell-specific operators. For example, this POSIX command uses a pipe and glob:
import subprocess
result = subprocess.run(
"printf '%s\n' *.log | sort",
shell=True,
executable="/bin/bash",
check=True,
capture_output=True,
text=True,
)
print(result.stdout)
With shell=True, the command is interpreted by a shell. Python’s documentation places responsibility for quoting whitespace and metacharacters on the application when the shell is invoked explicitly. If untrusted or user-controlled values are interpolated into a shell command, they can change what the shell executes. Prefer an argument list with the default shell=False whenever possible.
If POSIX shell parsing is unavoidable, validate dynamic values against the allowed inputs and use shlex.quote() for individual values placed in a command string. That quoting is for POSIX shell syntax, not a universal solution for Windows shells. Python’s PEP 787 explains that quoting depends on the shell’s own string-quoting rules: a mechanism for one shell cannot be trusted for a shell that does not follow POSIX rules. See PEP 787.
Best Value
Common errors and fixes
- Python cannot find
bash. The executable is not available under that name on the process’sPATH. Use the correct absolute Bash path for the target system, or ensure the environment that launches Python includes Bash. - The script path is reported as missing. A relative path is resolved from the child’s working directory, not necessarily the script’s directory. Use an absolute path or set
cwddeliberately. - The script runs but fails to find its own files. It may assume a working directory. Set
cwdto the directory it expects, or revise the script to resolve its own file paths. CalledProcessErroris raised. The script returned a nonzero status andcheck=Truesurfaced it. Inspectexc.returncodeand capturedexc.stderrto diagnose the script’s failure.TimeoutExpiredis raised. The child did not finish within the configured timeout. Set a suitable deadline, or handle this condition as a timeout at the application layer.- An argument with spaces is split or interpreted unexpectedly. Pass it as a separate list element; do not concatenate arguments into a shell command. In the Bash script, quote parameter expansions such as
"$1". - Output is bytes rather than readable text. Set
text=Truewhen using captured output and you want decoded strings. If you need binary output, keep bytes and process them accordingly. - Output seems to disappear. With
capture_output=True, output is stored inresult.stdoutandresult.stderr; it is not automatically printed. Print or log the stream you need. - A command works in a terminal but not with
shell=False. It may depend on shell parsing such as a pipe or glob. Use a shell only for that syntax, or invoke the underlying programs directly with a list of arguments.
Make the invocation reproducible
For a script used by a scheduled job, service, or application, make the assumptions explicit: select the intended interpreter, use a stable script path, set its working directory, provide required environment variables, and apply a timeout. Keep arguments as separate list items. Capture output when you need to inspect it, and use check=True when a failed exit status should stop the Python flow.
Python and Bash are available in different environments and operating systems, so a command that depends on /bin/bash is specifically a POSIX-style setup. Confirm that Bash and the script’s external dependencies exist in the environment where Python will run; Python cannot make an unavailable interpreter or command available by itself.
Or skip the browser setup
If the job you actually need is capturing a website rather than running a local Bash script, ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. Its API accepts cookie/consent banners and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. AI agents can use its MCP server tools to take screenshots, get page info, and capture PDFs.
Example cURL request (see the ScreenshotNeo API documentation for parameters and setup):
Recommended Free Tools
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The free plan includes 1,000 screenshots per month with no card required; paid plans start at $5 for 3,000 shots. Sign up for free and get 1,000 screenshots a month with no card.
Frequently Asked Questions
Can Python run a Bash script on Windows?
Only if Bash is installed and available to the process, for example through an environment that provides Bash. A Bash command or POSIX path such as /bin/bash is not portable to a Windows shell by itself.
Does the .sh file need to be executable?
No, not when you invoke Bash explicitly, such as ["bash", "script.sh"]. Direct execution of the script does rely on executable permission and a valid shebang.
Should I use subprocess.Popen() instead?
Use subprocess.run() for a completed command when its built-in waiting, timeout, and result handling fit. Choose a lower-level process API when you need more interactive or streaming control.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

