subprocess
working directory
programming
Python
software development

How can I specify working directory for a subprocess

Master System Design with Codemia

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

Introduction

Pass the cwd parameter to subprocess.run(), subprocess.Popen(), or any of the other subprocess functions to set the child process's working directory. This changes where the child runs without altering the working directory of your main Python process. It is the correct approach in virtually every scenario where a subprocess needs to execute in a specific directory.

python
1import subprocess
2
3result = subprocess.run(
4    ["ls", "-la"],
5    cwd="/tmp",
6    capture_output=True,
7    text=True,
8    check=True,
9)
10print(result.stdout)

The command runs as if you had cd /tmp first, but your Python process stays in its original directory.

Why cwd Is Better Than os.chdir()

A common alternative is calling os.chdir() before launching the subprocess. This works, but it changes the working directory of the entire Python process, which affects all subsequent file operations, not just the subprocess you are about to launch.

python
1import os
2import subprocess
3
4# BAD: changes directory for the entire process
5os.chdir("/tmp")
6subprocess.run(["ls", "-la"])
7
8# Everything after this point now operates in /tmp
9# Opening "config.yml" would look for /tmp/config.yml

Compare with cwd, which is scoped to the child process only:

python
1import subprocess
2
3# GOOD: only the subprocess sees /tmp as its working directory
4subprocess.run(["ls", "-la"], cwd="/tmp")
5
6# The Python process is still in its original directory
7# Opening "config.yml" looks in the original location
ApproachAffects parent processAffects child processThread-safe
cwd parameterNoYesYes
os.chdir()Yes (global)Yes (inherited)No (process-wide state)

In multi-threaded applications, os.chdir() is particularly dangerous because changing the working directory affects every thread in the process simultaneously.

Using cwd with subprocess.run()

subprocess.run() is the recommended high-level function for most use cases. Here are practical examples across different operating systems.

Running a build command in a project directory

python
1import subprocess
2from pathlib import Path
3
4project_dir = Path.home() / "projects" / "myapp"
5
6result = subprocess.run(
7    ["make", "build"],
8    cwd=project_dir,
9    capture_output=True,
10    text=True,
11    check=True,
12)
13print(result.stdout)

Running a git command in a specific repository

python
1import subprocess
2
3result = subprocess.run(
4    ["git", "log", "--oneline", "-5"],
5    cwd="/home/dev/my-repo",
6    capture_output=True,
7    text=True,
8    check=True,
9)
10print(result.stdout)

Windows example

python
1import subprocess
2
3result = subprocess.run(
4    ["cmd", "/c", "dir"],
5    cwd=r"C:\Users\dev\project",
6    capture_output=True,
7    text=True,
8    check=True,
9)
10print(result.stdout)

Using cwd with subprocess.Popen()

For long-running processes or when you need streaming output, use Popen with the same cwd parameter:

python
1import subprocess
2
3process = subprocess.Popen(
4    ["python", "manage.py", "runserver"],
5    cwd="/home/dev/django-project",
6    stdout=subprocess.PIPE,
7    stderr=subprocess.PIPE,
8    text=True,
9)
10
11# Read output line by line
12for line in process.stdout:
13    print(line, end="")
14
15process.wait()

Streaming output with error handling

python
1import subprocess
2
3process = subprocess.Popen(
4    ["npm", "run", "build"],
5    cwd="/home/dev/frontend",
6    stdout=subprocess.PIPE,
7    stderr=subprocess.STDOUT,  # merge stderr into stdout
8    text=True,
9)
10
11for line in process.stdout:
12    print(f"[build] {line}", end="")
13
14return_code = process.wait()
15if return_code != 0:
16    print(f"Build failed with exit code {return_code}")

Combining cwd with env

You can set both the working directory and environment variables for a subprocess. The env parameter replaces the entire environment, so you typically start with os.environ.copy() and add your overrides:

python
1import os
2import subprocess
3
4env = os.environ.copy()
5env["NODE_ENV"] = "production"
6env["PORT"] = "3000"
7
8result = subprocess.run(
9    ["node", "server.js"],
10    cwd="/home/dev/api",
11    env=env,
12    capture_output=True,
13    text=True,
14    check=True,
15)

Using pathlib.Path for Cross-Platform Paths

Hard-coded string paths are brittle. pathlib.Path produces platform-appropriate paths and makes the code self-documenting:

python
1from pathlib import Path
2import subprocess
3
4# Construct path relative to the script's location
5script_dir = Path(__file__).resolve().parent
6data_dir = script_dir / "data" / "input"
7
8result = subprocess.run(
9    ["python", "process.py"],
10    cwd=data_dir,
11    capture_output=True,
12    text=True,
13    check=True,
14)

subprocess.run() accepts both str and Path objects for cwd, so no explicit conversion is needed.

Handling Missing Directories

If the directory passed to cwd does not exist, Python raises FileNotFoundError before the subprocess is launched. The error message is clear but can be confused with the command itself not being found.

python
1import subprocess
2
3try:
4    subprocess.run(["ls"], cwd="/nonexistent/path", check=True)
5except FileNotFoundError as e:
6    print(f"Directory not found: {e}")

To guard against this, create the directory first:

python
1from pathlib import Path
2import subprocess
3
4workdir = Path("/tmp/my-job-output")
5workdir.mkdir(parents=True, exist_ok=True)
6
7subprocess.run(["pwd"], cwd=workdir, check=True)

Distinguishing directory errors from command errors

When cwd is invalid, the error looks like:

text
FileNotFoundError: [Errno 2] No such file or directory: '/nonexistent/path'

When the command itself is not found, the error looks like:

text
FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent-command'

Both raise FileNotFoundError, but the message text differs. If you need programmatic distinction, check whether the path in the exception matches your cwd value:

python
1import subprocess
2from pathlib import Path
3
4workdir = Path("/tmp/my-job")
5cmd = ["my-tool", "--flag"]
6
7try:
8    subprocess.run(cmd, cwd=workdir, check=True)
9except FileNotFoundError as e:
10    if str(workdir) in str(e):
11        print(f"Working directory does not exist: {workdir}")
12    else:
13        print(f"Command not found: {cmd[0]}")

Relative Paths Inside the Subprocess

Once cwd is set, all relative paths used by the subprocess resolve against that directory, not the parent Python process's directory. This is the entire point of cwd, but it still surprises people during debugging.

python
1import subprocess
2
3# The subprocess writes to /tmp/output.txt, not ./output.txt
4subprocess.run(
5    ["bash", "-c", "echo hello > output.txt"],
6    cwd="/tmp",
7    check=True,
8)
9
10# Verify
11result = subprocess.run(
12    ["cat", "output.txt"],
13    cwd="/tmp",
14    capture_output=True,
15    text=True,
16)
17print(result.stdout)  # "hello"

If the subprocess reads a config file with a relative path like config/settings.yml, it will look for /tmp/config/settings.yml in this example. Make sure the file exists relative to the cwd you specified, not relative to your script.

A Real-World Pattern: Running Tests in a Subdirectory

A common use case is running a test suite in a specific project directory:

python
1import subprocess
2from pathlib import Path
3
4def run_tests(project_path: str, verbose: bool = False) -> bool:
5    """Run pytest in the given project directory."""
6    cmd = ["python", "-m", "pytest"]
7    if verbose:
8        cmd.append("-v")
9
10    result = subprocess.run(
11        cmd,
12        cwd=project_path,
13        capture_output=True,
14        text=True,
15    )
16
17    print(result.stdout)
18    if result.stderr:
19        print(result.stderr)
20
21    return result.returncode == 0
22
23# Run tests in three different projects
24projects = [
25    Path.home() / "projects" / "auth-service",
26    Path.home() / "projects" / "user-service",
27    Path.home() / "projects" / "api-gateway",
28]
29
30for project in projects:
31    success = run_tests(project, verbose=True)
32    status = "PASSED" if success else "FAILED"
33    print(f"{project.name}: {status}\n")

Each subprocess.run() call operates in its own directory without interfering with the others or with the parent process.

Common Pitfalls

Using os.chdir() when only a subprocess needs a different directory. This changes the working directory for the entire process, affecting all subsequent file operations and any other threads. Use cwd instead.

Assuming relative paths in the subprocess resolve against the script's location. They resolve against the cwd you specified. If you write output to results/data.csv and cwd is /tmp, the file ends up at /tmp/results/data.csv.

Confusing "directory not found" with "command not found." Both raise FileNotFoundError. Check the exception message to determine which path is missing.

Not using check=True. Without check=True, a subprocess that fails (non-zero exit code) returns normally, and your code continues as if nothing went wrong. Unless you have a specific reason to handle failure manually, use check=True to raise CalledProcessError on failure.

Passing a file path instead of a directory path to cwd. The cwd parameter expects a directory. Passing a file path raises NotADirectoryError.

Using shell=True with cwd on Windows. When shell=True is set on Windows, the command runs through cmd.exe. The cwd parameter still works, but the behavior of relative paths can differ because cmd.exe has its own path resolution rules.

Summary

  • Use the cwd parameter on subprocess.run() or subprocess.Popen() to set the child process's working directory.
  • cwd only affects the child process. The parent Python process stays in its original directory.
  • Prefer cwd over os.chdir() because os.chdir() is global, affects all threads, and makes the program harder to reason about.
  • pathlib.Path objects are accepted directly by cwd. No string conversion is needed.
  • If the directory does not exist, Python raises FileNotFoundError before the subprocess starts. Create it with mkdir(parents=True, exist_ok=True) if needed.
  • All relative paths used by the subprocess resolve against the cwd directory, not the parent script's location.
  • Always use check=True unless you have a specific reason to handle subprocess failure manually.

Course illustration
Course illustration

All Rights Reserved.