os.environ

os.environ is a dict-like snapshot of the environment taken when os is imported. Changing it also changes the real process environment, so subprocesses see the update. Values are always str: convert numbers and flags yourself.

os mappingPython 3.0+ (environb, getenvb, supports_bytes_environ 3.2+)Live demo
Common call
os.environ.get('PORT', '8000')
Returns
the value as str, or the default
Replaces
Hard-coded secrets and settings in source code
Watch out
Missing keys raise KeyError with os.environ[...]; values are always str ('0' is truthy)
os.environ / os.getenv(keykey — The variable name. On Linux and macOS names are case-sensitive; on Windows os.environ upper-cases them, so lookups ignore case.type: str · required, defaultdefault — getenv only: returned when the variable is not set. Not converted to str.type: any · default: None=None) / os.putenv(key, value) / os.unsetenv(key)
→ str | None

Demo

Live evaluation
A fixed two-variable environment. Look up a name with [ ], getenv and getenv with a default.
Try:
Inputs
namestrvariable name
Code
import os
from unittest import mock
with mock.patch.dict(os.environ, {'HOME': '/home/ada', 'LANG': 'C.UTF-8'}, clear=True):
    try:
        item = os.environ['HOME']
    except KeyError as e:
        item = f'KeyError: {e}'
    result = (item, os.getenv('HOME'), os.getenv('HOME', 'default'))
result
Result
('/home/ada', '/home/ada', '/home/ada')

Each tab works on a temporary environment (mock.patch.dict(..., clear=True) restores the real one afterwards), so the result does not depend on your machine. A missing name: os.environ[name] raises KeyError, getenv returns None or your default. In the second tab text is stored as-is, while 10 and 1.5 arrive as an int and a float, and os.environ refuses them with TypeError: str expected, not int (or float). On Windows os.environ upper-cases names, so os.environ['home'] finds HOME there but raises KeyError on Linux and macOS.

Parameters

NameTypeRequiredDescription
keystryesThe variable name. On Linux and macOS names are case-sensitive; on Windows os.environ upper-cases them, so lookups ignore case.
defaultanyno (None)getenv only: returned when the variable is not set. Not converted to str.
valuestryesputenv / os.environ[key] = value: must be a str — anything else raises TypeError.

Return value

str | None — os.environ[key] returns the str value (KeyError when unset); getenv returns the value, or default (None) when unset.

Common patterns

Typed settings with defaults
Read once, convert explicitly, fail early on bad values.
import os
PORT = int(os.environ.get('PORT', '8000'))
DEBUG = os.environ.get('DEBUG', '0') in ('1', 'true', 'yes')
DATABASE_URL = os.environ['DATABASE_URL']  # required: KeyError if missing
Run a child with an extra variable
Pass a modified copy instead of changing your own environment.
import os, subprocess
subprocess.run(['make', 'test'], env={**os.environ, 'CI': '1'}, check=True)
Temporarily set variables in a test
patch.dict restores os.environ (and the real environment) on exit.
import os
from unittest import mock
with mock.patch.dict(os.environ, {'APP_ENV': 'test'}):
    run_app()
Bytes environment (POSIX)
environb / getenvb give raw bytes where values are not valid in the file system encoding.
import os
if os.supports_bytes_environ:
    raw = os.environb.get(b'LANG')

Examples

1. A mutable mapping
import os from collections.abc import MutableMapping isinstance(os.environ, MutableMapping)
Returns
True
2. Default for a missing variable
import os from unittest import mock with mock.patch.dict(os.environ, clear=True): token = os.getenv('API_TOKEN', '') token
Returns
''
3. Child processes inherit the environment you pass
import os, subprocess, sys child = "import os; print(os.environ['GREETING'])" subprocess.run([sys.executable, '-c', child], env={**os.environ, 'GREETING': 'hi'}, capture_output=True, text=True).stdout
Returns
'hi\n'
4. Append to PATH portably
import os from unittest import mock with mock.patch.dict(os.environ, {'PATH': '/usr/bin'}, clear=True): os.environ['PATH'] += os.pathsep + '/opt/tool/bin' parts = os.environ['PATH'].split(os.pathsep) parts
Returns
['/usr/bin', '/opt/tool/bin']
5. putenv does not update os.environ
import os from unittest import mock with mock.patch.dict(os.environ): os.putenv('ONLY_PUTENV_X1', '1') seen = 'ONLY_PUTENV_X1' in os.environ os.unsetenv('ONLY_PUTENV_X1') seen
Returns
False
6. copy() gives a plain dict
import os type(os.environ.copy()).__name__
Returns
'dict'
7. Bytes environment: POSIX only
import os (os.supports_bytes_environ == (os.name != 'nt'), hasattr(os, 'environb') == os.supports_bytes_environ)
Returns
(True, True)

Pitfalls

1. Indexing a variable that may be unset
os.environ[name] raises KeyError. Use getenv / environ.get with a default unless the variable is truly required.
os.environ['API_TOKEN']
import os
from unittest import mock
with mock.patch.dict(os.environ, clear=True):
    os.environ['API_TOKEN']
KeyError: 'API_TOKEN'
getenv with default
import os
from unittest import mock
with mock.patch.dict(os.environ, clear=True):
    token = os.getenv('API_TOKEN', 'none')
token
'none'
2. Treating the string "0" as False
Every non-empty string is truthy — DEBUG=0 is still True under bool(). Compare with the values you accept.
bool(getenv(...))
import os
from unittest import mock
with mock.patch.dict(os.environ, {'DEBUG': '0'}):
    debug = bool(os.getenv('DEBUG'))
debug
True
== '1'
import os
from unittest import mock
with mock.patch.dict(os.environ, {'DEBUG': '0'}):
    debug = os.getenv('DEBUG', '0') == '1'
debug
False
3. Storing a number
Environment values are strings in the OS itself, so os.environ only accepts str.
int value
import os
from unittest import mock
with mock.patch.dict(os.environ):
    os.environ['WORKERS'] = 4
TypeError: str expected, not int
str value
import os
from unittest import mock
with mock.patch.dict(os.environ):
    os.environ['WORKERS'] = str(4)
    workers = int(os.environ['WORKERS'])
workers
4

When to use

Use it
  • Configuration and secrets supplied by the deployment (12-factor style)
  • Passing settings to child processes
Reach for something else
  • Structured or large configuration → a config file (tomllib, json)
  • Values shared between running processes — each process has its own copy
  • Changing the environment for one subprocess → pass env= to subprocess.run instead

Notes

CPython impl
os._Environ in Lib/os.py: a MutableMapping over a dict captured at import; __setitem__ calls putenv and __delitem__ calls unsetenv, so the real environment follows
Snapshot
Changes made by putenv directly, or by C code, are not reflected in os.environ
Windows
Keys are upper-cased (os.environ is case-insensitive there); supports_bytes_environ is False and environb / getenvb do not exist
putenv / unsetenv
Always available since 3.9 (unsetenv on Windows too); getenvb and environb are Unix only

FAQ

None in practice: os.getenv(key, default) is os.environ.get(key, default). Both return None (or the default) for a missing variable, while os.environ[key] raises KeyError.