50.7 Environment Variables and Working Directory
When an external command is executed via the subprocess module, it inherits a runtime context from the Python process that spawned it. This context includes two critical components: the set of environment variables and the current working directory. Understanding how to control and modify this context is essential for ensuring that the child process behaves as expected, as many programs rely on these settings for configuration and file path resolution.
Inheriting and Modifying the Environment
By default, a child process launched with subprocess.run() or its counterparts inherits the environment variables of the parent Python process. This is often the desired behavior, as it provides the child with the same PATH for finding executables, the same language/localization settings (e.g., LANG), and any other variables your application might rely on.
However, there are frequent scenarios where you need to modify this environment. For instance, you might need to add a new directory to the PATH, set a configuration-specific variable like APP_ENV to “production”, or temporarily override a variable for the child’s execution. This is achieved using the env parameter. If you provide a dictionary to this parameter, it completely replaces the inherited environment for the child process. This is a crucial detail; it doesn’t just update the existing environment, it supersedes it.
import subprocess
import os
# Get a copy of the current environment to modify
my_env = os.environ.copy()
my_env["APP_SECRET"] = "super-secret-value"
my_env["PATH"] = "/my/custom/tools:" + my_env["PATH"]
# The child process will ONLY have the variables defined in `my_env`
result = subprocess.run(
["my_script.sh"],
env=my_env, # Completely replaces the inherited environment
capture_output=True,
text=True
)
print(result.stdout)
A common pitfall is providing a partial env dictionary, which results in a child process with a minimal environment, often missing critical variables like PATH, USER, HOME, or SYSTEMROOT. This will likely cause the child command to fail in unexpected ways. The best practice is to always start with a copy of os.environ and modify that copy, ensuring all the essential base variables are preserved.
# UNSAFE: This child process will have *only* the MY_VAR variable.
subprocess.run(["my_command"], env={"MY_VAR": "value"})
# SAFE: This child process has the complete inherited environment plus our change.
safe_env = os.environ.copy()
safe_env["MY_VAR"] = "value"
subprocess.run(["my_command"], env=safe_env)
Setting the Current Working Directory
The current working directory (CWD) is the directory from which all relative paths in the child command are resolved. By default, the child process inherits the working directory of the Python interpreter. You can change this using the cwd parameter. This is particularly useful when you need to run a command that expects to find specific files (e.g., configuration, data, or other scripts) in a relative location.
import subprocess
# This will run 'git status' from within the '/path/to/my/repo' directory.
# It's equivalent to doing `cd /path/to/my/repo && git status` in a shell.
result = subprocess.run(
["git", "status"],
cwd="/path/to/my/repo", # Sets the child's working directory
capture_output=True,
text=True
)
if result.returncode == 0:
print("Git status successful:")
print(result.stdout)
else:
print("Git status failed:")
print(result.stderr)
It is a best practice to always use absolute paths for the cwd argument to avoid ambiguity. Relying on a relative path depends on the Python process’s own working directory, which may change, making your code less predictable. If the provided directory does not exist, a FileNotFoundError exception will be raised.
Combining Environment and Directory Changes
The effects of env and cwd are independent but can be combined to create a highly specific execution context for the child process. This is a powerful technique for isolating the command’s runtime environment.
import subprocess
import os
# Create a custom environment
custom_env = os.environ.copy()
custom_env["CUSTOM_CONFIG_DIR"] = "/app/config"
# Run a command from a specific directory with our custom environment
result = subprocess.run(
["./scripts/database_migrate.sh"], # This is a relative path
cwd="/app/project", # So it will be resolved to /app/project/scripts/database_migrate.sh
env=custom_env,
capture_output=True,
text=True
)
In this example, the database_migrate.sh script will be executed from the /app/project directory (so it can use relative paths to find adjacent files), and it will have access to the CUSTOM_CONFIG_DIR environment variable, which it might use to locate its configuration files. This decouples the script’s logic from the environment of the parent Python process, making the overall system more robust and configurable.