Working with Environment Variables in Python: os.environ, os.environb, and Cross-Platform Considerations
How to read, modify, and synchronize environment variables in Python using os.environ and os.environb, handle encoding differences across platforms, and pass environment to child processes.
On this page
The short answer
os.environ is a dict-like snapshot of the process environment captured at interpreter startup. Mutating it calls setenv or unsetenv automatically, but os.putenv does not update the mapping. On Unix, os.environb provides a bytes view synchronized with os.environ. Encoding of keys and values depends on the filesystem encoding or UTF-8 Mode. os.reload_environ (Python 3.14+) refreshes the cache after external changes but is not thread-safe. Child processes receive environment through the env parameter of spawn*/exec* functions, which must contain string keys and values.
Reading environment variables via os.environ and os.getenv
os.environ is a mapping object representing the process environment. Accessing a key that does not exist raises KeyError, while os.environ.get(key, default) returns the default instead. os.getenv(key, default=None) is a convenience wrapper that also returns None when the variable is absent. The critical distinction is between a variable that is not set (returns None) and one set to an empty string (returns ''). Using the truthiness of the result conflates these two states, which can silently mask configuration errors in production deployments.
The source documentation states that environ is a mapping where both keys and values are strings. On platforms that support bytes environment access, the bytes representation is available through os.environb and os.getenvb. For most application code, os.environ or os.getenv is sufficient because the interpreter handles decoding from the filesystem encoding or UTF-8 Mode automatically.
import os
# Distinguish absent from empty
value = os.environ.get("APP_MODE")
if value is None:
print("variable not set")
else:
print("value:", repr(value))Practical tip
When you need to detect whether a variable is truly absent versus set to an empty string, use os.environ.get(name) and check for None rather than relying on truthiness, because an empty string is falsy but semantically different from an unset variable.
Modifying and deleting variables via os.environ
Assigning to os.environ[key] calls the underlying setenv system function. Deleting a key calls unsetenv. The pop() and clear() methods also trigger unsetenv for each removed entry. This automatic synchronization is the reason the documentation advises modifying os.environ rather than calling os.putenv directly, because putenv writes to the C-level environ array without updating the Python mapping, leaving os.environ stale.
On some platforms, including FreeBSD and macOS, repeated calls to setenv or putenv may cause memory leaks in the C runtime. This is a platform-level concern rather than a Python bug, but it means that long-running processes that frequently modify environment variables should minimize the number of distinct keys they create.
import os
os.environ["APP_MODE"] = "production" # calls setenv
os.environ.pop("APP_MODE", None) # calls unsetenv if presentSynchronization of os.environ and os.environb
os.environb is a bytes-version mapping available only when os.supports_bytes_environ is True, which holds on Unix platforms. The two mappings are kept in sync: modifying environb updates environ and vice versa. This means you can write a bytes value to environb and read the decoded string from environ, or the reverse. On Windows, supports_bytes_environ is False and environb does not exist, so code targeting both platforms must guard access with that flag.
The synchronization uses the filesystem encoding and its error handler for conversion between bytes and str. If a bytes value contains sequences that cannot be decoded under the current encoding, surrogateescape produces lone surrogates in the str view. Round-tripping through both representations preserves the original bytes.
import os
if os.supports_bytes_environ:
os.environb[b"RAW_KEY"] = b"\xff\xfe"
print(os.environ["RAW_KEY"]) # decoded via fsencode/fsdecodeEncodings: UTF-8 Mode, fsencode/fsdecode, and getenvb
When Python UTF-8 Mode is active (enabled via -X utf8 or PYTHONUTF8=1, or automatically when the locale is C or POSIX), environment variables are decoded using UTF-8 regardless of the system locale. Outside UTF-8 Mode, the filesystem encoding governs decoding. os.fsencode and os.fsdecode expose these conversions explicitly and are the recommended way to handle path-like bytes that may appear in environment values.
os.getenvb returns the raw bytes of an environment variable without any decoding step. This is useful when you need to inspect or forward values that may contain bytes invalid under the current encoding, or when writing portable code that must avoid the implicit decode-then-re-encode round trip.
import os, sys
print("filesystem encoding:", sys.getfilesystemencoding())
print("utf8 mode:", sys.flags.utf8_mode)
# Raw bytes access (Unix only)
if os.supports_bytes_environ:
raw = os.getenvb(b"LANG")
print("LANG bytes:", raw)Environment cache and os.reload_environ
The documentation is explicit: os.environ and os.environb are a cache of environment variables at the time Python started. Changes made outside the interpreter, or through os.putenv and os.unsetenv, are not reflected in the mapping. os.reload_environ, added in Python 3.14, refreshes both mappings from the current process environment. Before 3.14, no standard mechanism existed to resynchronize the cache after external modification.
This matters in scenarios where a parent process or a signal handler modifies the environment through C-level APIs, or when a library calls putenv without going through os.environ. Without reload_environ, subsequent reads return stale values.
import os
# After external modification of the process environment:
os.reload_environ() # Python 3.14+
print(os.environ.get("EXTERNALLY_SET_VAR"))Errors and thread safety when working with the environment
All functions in the os module raise OSError or a subclass when arguments have correct types but are rejected by the operating system. For environment operations this is rare because setenv and unsetenv succeed for valid strings, but invalid surrogate characters in keys or values can trigger UnicodeEncodeError before the system call is reached.
os.reload_environ carries an explicit warning that it is not thread-safe. Reading from os.environ, os.environb, or calling os.getenv while a reload is in progress may return an empty result. In multi-threaded applications, serialize access to reload_environ or avoid it entirely by managing environment state through os.environ mutations only.
import os
try:
os.environ["BAD_KEY"] = "value\ud800" # lone surrogate
except UnicodeEncodeError as e:
print("encoding error:", e)Cross-platform portability: os.name, platform limitations, and PATH
os.name returns 'posix' on Unix-like systems and 'nt' on Windows. On WebAssembly, Android, and iOS, large parts of the os module are unavailable or behave differently: process APIs like fork, execve, and spawn are missing, and getuid and getpid may be stubs. Code that must run across these targets should check os.name and os.supports_bytes_environ before relying on platform-specific behavior.
os.get_exec_path returns the list of directories searched for executables, reading from the PATH variable in the provided env dictionary or in os.environ by default. This is the programmatic equivalent of the shell PATH lookup and is used internally by the p-variants of spawn and exec functions.
import os
print("os.name:", os.name)
print("supports_bytes_environ:", os.supports_bytes_environ)
print("exec paths:", os.get_exec_path())Passing environment to child processes via spawn and exec
The spawn*e and exec*e variants accept an env parameter that completely replaces the child process environment rather than inheriting from the parent. Keys and values in this mapping must be strings; invalid types cause the function to fail with a return value of 127. When env is provided, the PATH lookup for the executable uses the new environment, not the parent's os.environ.
This distinction between inheriting (spawnl, spawnv, execl, execv) and replacing (spawnle, spawnlpe, spawnve, spawnvpe, execle, execvpe) is essential for sandboxing child processes or injecting configuration without polluting the parent environment.
import os
child_env = dict(os.environ)
child_env["APP_MODE"] = "production"
# spawnvpe replaces the child environment entirely
status = os.spawnvpe(os.P_WAIT, "cp", ["cp", "index.html", "/dev/null"], child_env)
print("exit status:", status)Things to check
- os.environ.get returns None for absent variables and '' for variables set to empty string; these are distinguishable only by identity check against None.
- os.environ[key] = value calls setenv; del os.environ[key] calls unsetenv; os.putenv does not update os.environ.
- os.environb exists only when os.supports_bytes_environ is True (Unix); modifying one mapping updates the other.
- UTF-8 Mode forces UTF-8 decoding of environment variables regardless of locale; check sys.flags.utf8_mode.
- os.reload_environ is available from Python 3.14 and is not thread-safe; concurrent reads may return empty results.
- env parameter in spawn*e and exec*e must contain string keys and values; invalid entries cause return value 127.
- On WebAssembly, Android, and iOS, process-related os functions are unavailable or stubbed.
Where this applies
os.environb and os.getenvb are Unix-only (gated by supports_bytes_environ). os.reload_environ requires Python 3.14 or later and is explicitly not thread-safe. On FreeBSD and macOS, frequent setenv calls may leak memory at the C level. UTF-8 Mode can only be enabled at interpreter startup and cannot be toggled at runtime. The spawn/exec functions are unavailable on WebAssembly, Android, and iOS.