Python
Shell Scripting
Programming
Automation
Script Integration

How can I call a shell script from Python code?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The standard way to call a shell script from Python is the subprocess module. It gives you control over arguments, exit codes, output capture, environment variables, and timeouts, which makes it safer and more maintainable than older helpers such as os.system.

Running a script directly with subprocess.run

If your script is executable and has a correct shebang, call it as a normal program:

python
1import subprocess
2
3result = subprocess.run(
4    ["./deploy.sh", "--env", "staging"],
5    check=True,
6    capture_output=True,
7    text=True,
8)
9
10print(result.stdout)

This is the preferred form because Python passes the command and arguments directly without invoking a shell. That means fewer quoting bugs and much lower risk of shell injection.

For this to work, the script should be executable:

bash
chmod +x deploy.sh

And it should declare an interpreter, for example:

bash
#!/usr/bin/env bash

Running the script through a shell

Sometimes you really do need shell features such as pipes, wildcards, or variable expansion. In that case, call the shell explicitly:

python
1import subprocess
2
3result = subprocess.run(
4    ["bash", "./deploy.sh", "staging"],
5    check=True,
6    text=True,
7    capture_output=True,
8)
9
10print(result.stdout)

This is still safer than shell=True because you are invoking bash directly with a fixed argument list.

Use shell=True only when you need shell syntax in a single command string:

python
1result = subprocess.run(
2    "echo $HOME && ./deploy.sh staging",
3    shell=True,
4    executable="/bin/bash",
5    text=True,
6    capture_output=True,
7)

That form is flexible, but it requires careful input handling.

Passing environment variables and working directories

Real scripts often depend on environment variables or a specific current directory. subprocess.run supports both cleanly:

python
1import os
2import subprocess
3
4env = os.environ.copy()
5env["APP_ENV"] = "staging"
6
7result = subprocess.run(
8    ["./deploy.sh"],
9    cwd="/Users/markqian/projects/example-app",
10    env=env,
11    text=True,
12    capture_output=True,
13    check=True,
14)

This is better than changing global process state with os.chdir or mutating os.environ in a way that leaks into the rest of your program.

Handling failures properly

When check=True is set, Python raises subprocess.CalledProcessError if the script exits with a non-zero status. That is usually what you want:

python
1import subprocess
2
3try:
4    subprocess.run(["./deploy.sh"], check=True)
5except subprocess.CalledProcessError as exc:
6    print(f"Script failed with exit code {exc.returncode}")

You can also add a timeout:

python
subprocess.run(["./deploy.sh"], check=True, timeout=30)

That prevents a hung shell script from blocking the Python process forever.

If you need to stream output while the script runs, use subprocess.Popen instead of run. That gives you access to the process object and lets you read stdout incrementally, which is useful for long-running deployment or build scripts.

Common Pitfalls

The biggest mistake is building a shell command by concatenating untrusted input into a string. If user-controlled data ends up inside a shell=True command, you have created a command injection risk.

Another common issue is forgetting execute permissions or the shebang line. In that case, the script exists, but the operating system does not know how to run it.

Relative paths also cause trouble. A script that works from your terminal may fail from Python because the current working directory is different. Use cwd= or absolute paths when the script depends on local files.

Finally, do not use os.system for new code. It gives you almost no control over stdout, stderr, structured arguments, or error handling compared with subprocess.

One more subtle issue is platform assumptions. A script that depends on Bash-specific syntax may fail on systems where /bin/sh points to a different shell, so be explicit about the interpreter when the script requires it.

Summary

  • Use subprocess.run for calling shell scripts from Python.
  • Prefer passing an argument list such as ["./script.sh", "arg"] over shell=True.
  • Call bash explicitly when you need a shell but want predictable argument handling.
  • Use cwd, env, capture_output, check, and timeout to make script execution reliable.
  • Avoid os.system and avoid building shell command strings from untrusted input.

Course illustration
Course illustration

All Rights Reserved.