Skip to content

Investigating side-effect of using deepcopy to restore original os.environ state

import os, subprocess
class StrHelpers:
@classmethod
def popenWithoutEnv(cls):
print(f"""subprocess.Popen('echo $foo', shell=True): {subprocess.Popen('echo $foo', shell=True)}""")
@classmethod
def popenWithEnv(cls, env):
print(f"""subprocess.Popen('echo $foo', shell=True, env=env): {subprocess.Popen('echo $foo', shell=True, env=env)}""")

os.environ is loaded originally and becomes a stale copy of the original os env variables

Section titled “os.environ is loaded originally and becomes a stale copy of the original os env variables”
  • Taking a deepcopy of the os.environ variable creates a reference to the original os.environ
"""Recreating the 'bug'"""
import os, copy
# pre-experiment assertions
os.environ['foo'] = 'foo'
StrHelpers.popenWithoutEnv()
# do mods
myEnv = copy.deepcopy(os.environ)
myEnv['foo'] = 'bar'
# what the shell sees
StrHelpers.popenWithoutEnv()
subprocess.Popen('echo $foo', shell=True): <Popen: returncode: None args: 'echo $foo'>
foo
subprocess.Popen('echo $foo', shell=True): <Popen: returncode: None args: 'echo $foo'>
bar
import os, subprocess
class StrHelpers(StrHelpers):
@classmethod
def osEnvironFooLookup(cls):
print(f"""os.environ['foo']: {os.environ['foo']}""")
@classmethod
def osEnvironFooGetEnv(cls):
print(f"""os.getenv['foo']: {os.getenv('foo')}""")

Odder still, looking up the os.environ value returns what we would deem correct

Section titled “Odder still, looking up the os.environ value returns what we would deem correct”
import os, copy
# pre-experiment assertions
os.environ['foo'] = 'foo'
StrHelpers.popenWithoutEnv()
# do mods
myEnv = copy.deepcopy(os.environ)
myEnv['foo'] = 'bar'
# post-experiment examination
# what python thinks
StrHelpers.osEnvironFooLookup()
StrHelpers.osEnvironFooGetEnv()
# what the shell sees
StrHelpers.popenWithEnv(myEnv)
foo
subprocess.Popen('echo $foo', shell=True): <Popen: returncode: None args: 'echo $foo'>
os.environ['foo']: foo
os.getenv['foo']: foo
subprocess.Popen('echo $foo', shell=True, env=env): <Popen: returncode: None args: 'echo $foo'>
bar
## There are two ways around this behavior
# os.environ.copy()
# Context managed modification

1. Use os.environ.copy to prevent this from occurring

Section titled “1. Use os.environ.copy to prevent this from occurring”
import os, copy
# pre-experiment assertions
os.environ['foo'] = 'foo'
StrHelpers.popenWithoutEnv()
# do mods
myEnv = os.environ.copy()
myEnv['foo'] = 'bar'
# post-experiment examination
# what python thinks
StrHelpers.osEnvironFooLookup()
StrHelpers.osEnvironFooGetEnv()
# what the shell sees
StrHelpers.popenWithEnv(myEnv)
subprocess.Popen('echo $foo', shell=True): <Popen: returncode: None args: 'echo $foo'>
os.environ['foo']: foo
os.getenv['foo']: foo
subprocess.Popen('echo $foo', shell=True, env=env): <Popen: returncode: None args: 'echo $foo'>
foo
bar

2. Use a contextmanager to control the modifications

Section titled “2. Use a contextmanager to control the modifications”
import os
from contextlib import contextmanager
@contextmanager
def modifiedEnv(*remove, **update):
env = os.environ
update = update or {}
remove = remove or []
envVarsToModifyBeforeTest = (set(update.keys()) | set(remove)) & set(env.keys())
envVarsToRestoreOnExit = {var: env[var] for var in envVarsToModifyBeforeTest}
envVarsToRemoveOnExit = frozenset(var for var in update if var not in env)
env.update(update)
[env.pop(var, None) for var in remove]
yield
env.update(envVarsToRestoreOnExit)
[env.pop(var) for var in envVarsToRemoveOnExit]
"""Testing context managed solution"""
def printResults():
StrHelpers.osEnvironFooLookup()
StrHelpers.popenWithEnv(myEnv)
os.environ['foo'] = 'foo'
with modifiedEnv(foo='bar') as myEnv:
print("Inside context manager modified environment")
printResults()
print("Outside context managed modified environment")
printResults()
```python
# Inside context manager modified environment
os.environ['foo']: bar
subprocess.Popen('echo $foo', shell=True, env=env) #: <Popen: returncode: None args: 'echo $foo'>
# Outside context managed modified environment
os.environ['foo']: # foo
# bar
subprocess.Popen('echo $foo', shell=True, env=env) # : <Popen: returncode: None args: 'echo $foo'>
# foo