subprocess env= Replaces, Doesn't Merge (tested)
Passing env={"MY_VAR": "x"} to subprocess.run() looks like it adds one variable on top of the normal environment. We measured what the child process actually receives: that one variable, and nothing else — no PATH, no HOME, nothing inherited.
1. env= gives the child only what you list, not your variable plus the rest
import subprocess, os
len(subprocess.run(["printenv"], capture_output=True, text=True).stdout.splitlines())
58
subprocess.run(["printenv"], env={"ONLY_THIS": "x"}, capture_output=True, text=True).stdout
'ONLY_THIS=x\n'
Measured: with no env= argument, the child inherits the full parent environment — 58 variables in our test. Pass env={"ONLY_THIS": "x"} instead, and the child gets exactly one variable. Nothing is merged; env= is a full replacement, not an addition.
2. That includes losing PATH, which breaks executable lookup
subprocess.run(["printenv", "PATH"], env={"MY_VAR": "hello"}, capture_output=True, text=True).returncode
1
subprocess.run(["printenv", "PATH"], env={"MY_VAR": "hello"}, capture_output=True, text=True).stdout
''
Measured: PATH is simply absent from the child's environment, so printenv PATH exits with status 1 (variable not found) and empty output — the same way it would if you'd never set PATH anywhere. Programs that read PATH, HOME, locale variables, or any secret your own process needs (API keys, virtualenv markers) silently don't get them, with no warning that anything is missing.
3. A fixed executable path can mask the problem at first
subprocess.run(["ls"], env={"MY_VAR": "hello"}, capture_output=True, text=True).returncode
0
Measured: this one still ran, which can be misleading — execvp() falls back to a built-in default search path (typically /bin:/usr/bin) when PATH is completely absent from the child's environment, not when it's merely different. So common commands in /bin or /usr/bin may keep working by accident even while PATH itself, and everything else you normally rely on being inherited, is gone.
4. What to actually do
If you want to add or override one or two variables while keeping everything else the parent process already has, build the dict explicitly: env={**os.environ, "MY_VAR": "hello"}. Only pass a hand-built minimal dict to env= when you specifically want to run the child in a stripped-down environment with nothing inherited — that's the one case where full replacement is actually what you want.
Frequently asked questions
Does subprocess.run(env={...}) add variables to the existing environment?
No — we measured it replacing the entire environment. The child process receives only the keys you pass in env=, not those plus whatever the parent process already had.
What happens to PATH if I pass env= with my own dict?
We measured PATH disappearing entirely from the child's environment unless you explicitly include it, which makes printenv PATH exit with status 1 as if PATH were never set at all.
Why did a command still work even though I passed a minimal env= dict?
Based on what we measured, programs in default system directories like /bin or /usr/bin can still be found because execvp() falls back to a built-in default search path when PATH is completely missing — that's different from PATH being merged or preserved.
How do I add an environment variable without losing the rest?
We'd merge explicitly: env={**os.environ, "MY_VAR": "hello"} — that keeps everything the parent process has and adds or overrides just the keys you specify.
Is this subprocess env= replacement behavior documented?
Yes — the Python docs for subprocess.Popen state that if env is not None, it defines the entire environment for the new process, replacing the default of inheriting the current process's environment. We confirmed this matches observed behavior in Python 3.13.5.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5 on Linux: ran subprocess.run(['printenv'], env={'ONLY_THIS': 'x'}) and compared its full child environment against the same call with no env= argument at all, counting how many variables the child actually received. PlainHeaders is our extension; Python's subprocess module is built in.