Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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=True makes a nonzero exit status raise CalledProcessError. If you omit it, inspect result.returncode yourself.
  • capture_output=True collects standard output and standard error in memory.
  • text=True decodes 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common errors and fixes

  • Python cannot find bash. The executable is not available under that name on the process’s PATH. 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 cwd deliberately.
  • The script runs but fails to find its own files. It may assume a working directory. Set cwd to the directory it expects, or revise the script to resolve its own file paths.
  • CalledProcessError is raised. The script returned a nonzero status and check=True surfaced it. Inspect exc.returncode and captured exc.stderr to diagnose the script’s failure.
  • TimeoutExpired is 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=True when 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 in result.stdout and result.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):

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.