Environment Variables in Python: A Complete Guide

Environment variables are one of those concepts that show up everywhere in production Python code, but most tutorials gloss over them entirely. You have seen them in Dockerfile entries and CI/CD pipelines. You have probably used os.environ without really understanding what is happening underneath. This is the guide I wish I had when I started building real systems. The short version is this. Environment variables are key value pairs that the operating system and other processes use to pass configuration into your Python program. They live outside your code, which means you can change how your app behaves without editing a single line of Python. That is useful for everything from switching between development and production setups to keeping secrets out of your repository. Let me walk you through how they actually work. ## What Exactly Is an Environment Variable An environment variable is a named string that lives in your operating system’s process environment. When Python starts up, it gets a copy of all the environment variables that exist at that moment. Your program reads them through the os module. The most common one you will see is PATH, which tells your shell where to look for executables.
import os

# Print all environment variables
for key, value in os.environ.items():
    print(f"{key} = {value}")

That loop prints every environment variable your process knows about. On a typical Linux or macOS machine, that is dozens of entries. On Windows, the same code behaves the same way, which is one reason os.environ is the portable choice. Python populates os.environ the first time the os module is imported. That happens during Python startup, before your main script runs. Any changes you make to os.environ live only inside your Python process. They do not bleed back to the parent shell or persist after your script finishes. That boundary is important to understand before you start using environment variables for configuration. ## Reading Environment Variables with os.environ The os.environ object looks like a dictionary, and you interact with it the same way. The simplest operation is reading a value by key.
import os

# Read a specific variable
db_host = os.environ.get("DB_HOST")
print(db_host)  # prints None if DB_HOST is not set

# Use a default value instead
db_port = os.environ.get("DB_PORT", "5432")
print(db_port)  # prints 5432 if DB_PORT is not set

I used .get() here instead of direct indexing. That is intentional. If you try to read a variable that does not exist using os.environ[“MISSING”], Python raises a KeyError and your program crashes. Using .get() with a default is the pattern you want for almost every real configuration value. The .get() approach also lets you distinguish between a variable that is set to an empty string and a variable that is not set at all. Both cases return None with .get(“VAR”) when the variable is absent, but you might need to handle empty strings differently in your configuration logic. ## Writing and Modifying Environment Variables Setting an environment variable from inside Python is straightforward.
import os

# Set a new environment variable
os.environ["MY_APP_DEBUG"] = "true"
os.environ["LOG_LEVEL"] = "WARNING"

# Confirm it is set
print(os.environ.get("MY_APP_DEBUG"))  # true
print(os.environ.get("LOG_LEVEL"))    # WARNING

When you assign to os.environ[“KEY”], Python internally calls the operating system’s putenv() function. On Unix systems, that immediately updates the process environment. On Windows, the behavior is similar. What this means in practice is that any child processes your program spawns after this point will inherit the new value. You can also remove a variable using del.

import os

os.environ["TEMP_API_KEY"] = "secret123"
print(os.environ.get("TEMP_API_KEY"))  # secret123

# Remove it
del os.environ["TEMP_API_KEY"]
print(os.environ.get("TEMP_API_KEY"))  # None

One gotcha worth noting is that direct calls to the operating system’s putenv() or unsetenv() from outside Python will not automatically update os.environ. If something external modifies the environment while your Python program is running, you may need to reload. For most web applications and scripts, this is not an issue you will encounter. ## Checking if a Variable Exists Before you read a variable, it is good practice to check whether it is set. Python gives you two ways to do this.
import os

# Option 1: using the `in` operator
if "PYTHON_ENV" in os.environ:
    print(f"Running in {os.environ['PYTHON_ENV']}")
else:
    print("PYTHON_ENV is not set, defaulting to development")

# Option 2: using a boolean check on the value
debug_mode = os.environ.get("DEBUG")
if debug_mode:
    print("Debug mode is on")
else:
    print("Debug mode is off or not set")

The `in` check tells you whether the key exists in the dictionary. The boolean check on the value handles both the case where the variable does not exist and the case where it is set to an empty string or the string “0”. Which one you want depends on your configuration contract. For flags that should be “true” or “false”, I prefer checking the specific string value rather than relying on truthiness. ## Converting Types Safely Environment variables always come in as strings. Your program needs to convert them to the right type before using them.
import os

def get_int_env(key, default):
    value = os.environ.get(key)
    if value is None:
        return default
    try:
        return int(value)
    except ValueError:
        raise ValueError(f"Environment variable {key} must be an integer, got {value!r}")

def get_bool_env(key, default):
    value = os.environ.get(key)
    if value is None:
        return default
    return value.lower() in ("true", "1", "yes", "on")


# Examples
workers = get_int_env("WORKER_COUNT", 4)
print(workers)  # 4 if WORKER_COUNT is not set

caching = get_bool_env("ENABLE_CACHE", False)
print(caching)  # False by default

The type conversion pattern above is something I use in every non-trivial project. Without it, a misconfigured environment variable causes a runtime crash somewhere deep in your initialization code instead of a clear error message at startup. ## Using Environment Variables for Application Configuration The most practical use of environment variables in Python is configuring your application differently across development, staging, and production environments. This pattern is so common that it has become a de facto standard. Here is a practical example using Flask.
import os
from flask import Flask

app = Flask(__name__)

# Load configuration from environment variables
app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
app.config["DATABASE_URL"] = os.environ.get("DATABASE_URL", "sqlite:///app.db")
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY")

@app.route("/")
def index():
    return {"status": "ok", "debug": app.config["DEBUG"]}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "5000")))

Every web framework has a similar pattern. Django uses os.environ extensively through its settings module. FastAPI and other async frameworks do the same. The key insight is that environment variables let you change behavior through the deployment environment without touching code. ## os.environ vs os.getenv — What Is the Difference Python gives you two ways to read environment variables. os.environ.get() and os.getenv() look similar on the surface, but there are differences worth knowing. | Feature | os.environ.get() | os.getenv() | |—|—|—| | Returns None for missing key | Yes | Yes | | Takes default argument | Yes | Yes | | Available in all Python versions | Yes (forever) | Yes (since Python 3.5) | | Lookup order | Direct dictionary lookup | Calls os.environ.get() internally | | Case sensitivity on Windows | Keys converted to uppercase | Same as os.environ | Both functions do the same thing. os.getenv() was added later and is essentially a convenience wrapper around os.environ.get(). I use os.environ.get() in most of my code because it makes it obvious that the lookup goes through the environment dictionary. Pick whichever you prefer and be consistent. ## Accessing the Full Environment Dictionary Sometimes you need to work with the entire set of environment variables at once. os.environ.items() gives you everything.
import os

# Filter variables with a common prefix
app_vars = {k: v for k, v in os.environ.items() if k.startswith("MYAPP_")}
for key, value in app_vars.items():
    print(f"{key}: {value}")

# Pass all app-prefixed variables to a subprocess
import subprocess
filtered_env = {k: v for k, v in os.environ.items() if not k.startswith("OLD_")}
result = subprocess.run(["python", "worker.py"], env=filtered_env)

Filtering environment variables this way is useful when you are spawning subprocesses and want to control exactly what they can see. You might not want your database credentials visible to every child process. ## Security Best Practices Environment variables are convenient, but they come with real security considerations. The biggest one is that environment variables are exposed in process listings, which means anyone with access to the machine can see them. Never commit real secrets to your repository, even if it is private. API keys and database passwords belong in environment variables or a secrets manager, not in code. Tools like HashiCorp Vault, AWS Secrets Manager, and Doppler exist precisely because environment variables alone are not a secure storage mechanism for sensitive data. Here is a safer pattern for loading secrets.
import os

# Load required secrets, fail fast if missing
def require_env(key):
    value = os.environ.get(key)
    if value is None:
        raise RuntimeError(f"Required environment variable {key} is not set")
    return value

API_KEY = require_env("API_KEY")
DATABASE_PASSWORD = require_env("DB_PASSWORD")

# Optional secrets with defaults for local development
EMAIL_HOST = os.environ.get("EMAIL_HOST", "localhost")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "25"))

The fail-fast pattern above means your application crashes immediately with a clear message if a required secret is missing, rather than failing in a confusing way later. For local development, tools like python-dotenv can load a .env file into environment variables automatically. Just make sure that .env file is in your .gitignore and never gets committed. ## Setting Variables Before Python Starts Environment variables are often set in the shell before Python launches. This is common in Docker, systemd service files, and CI/CD pipelines. On Linux and macOS:
# Set for a single command
export DB_HOST="prod.example.com"
python myapp.py

# Set for a single line
DB_HOST="prod.example.com" python myapp.py

On Windows:
# Command Prompt
set DB_HOST=prod.example.com
python myapp.py

# PowerShell
$env:DB_HOST = "prod.example.com"
python myapp.py

In Docker, you pass environment variables through the docker run command or your Dockerfile.
# Pass individual variables
docker run -e DB_HOST=prod.example.com -e DB_PORT=5432 myapp

# Or load from a file
docker run --env-file .env myapp

Understanding where the variable gets set versus where it gets read is a common source of confusion. Variables set in your shell RC file are not visible inside a Docker container unless you explicitly pass them. ## Common Pitfalls One mistake I see often is treating environment variables as typed. They are always strings. Your code must handle the conversion.
<pre class="wp-block-syntaxhighlighter-code">import os

# This will NOT do what you expect
timeout = os.environ.get("TIMEOUT", 30)
print(type(timeout))  # <class 'str'>, not int

# You must convert explicitly
timeout = int(os.environ.get("TIMEOUT", "30"))
print(type(timeout))  # <class 'int'>
</pre>
Another pitfall is assuming that environment variables persist across different invocations of your script. Each time you run your program, Python starts fresh. Any changes made during a previous run are gone. A third issue is case sensitivity. On Windows, environment variable names are case-insensitive but Python still treats them as case-sensitive in os.environ. Windows converts all keys to uppercase internally, so os.environ[“path”] and os.environ[“PATH”] refer to the same value on Windows. On Linux and macOS, the names are case-sensitive. ## TLDR – Environment variables are key value pairs the OS makes available to your Python process – Read with os.environ.get(“KEY”) or os.getenv(“KEY”), always prefer .get() with a default – Set with os.environ[“KEY”] = “value”, delete with del os.environ[“KEY”] – All values are strings, convert to int, bool, or other types explicitly – Changes to os.environ only affect the current process and its children – Use the fail-fast pattern for required secrets, use python-dotenv for local development – Never commit secrets to version control, even in private repositories ## FAQ **How do I list all environment variables in Python?** Use os.environ.items() to iterate over all key value pairs. This returns a dictionary-like view of every environment variable your Python process knows about. **Can I modify environment variables in a running Python program?** Yes. Assigning to os.environ[“KEY”] updates the variable for the current process and any child processes spawned after the change. It does not affect the parent process or persist after the program exits. **Why should I use os.environ.get() instead of os.environ[“KEY”]?** Using .get() with a default value prevents your program from crashing if a variable is not set. Direct indexing raises KeyError when the key is missing, which makes debugging harder for configuration issues. **Are environment variables secure for storing API keys and passwords?** Environment variables are more secure than hardcoding secrets in source code, but they are not fully secure because they are visible in process listings and can leak through logging. For production secrets, use a dedicated secrets manager like AWS Secrets Manager, HashiCorp Vault, or Doppler. **What is the difference between os.environ and os.getenv?** They do the same thing. os.getenv() was added in Python 3.5 as a convenience wrapper around os.environ.get(). Use whichever you prefer. **How do I pass environment variables to a subprocess in Python?** Pass an env parameter to subprocess.run() or subprocess.Popen(). This accepts a dictionary that replaces the current environment entirely, or you can filter the existing os.environ and pass that. **Why are my environment variables different inside a Docker container?** Docker containers start with a clean environment. Variables set in your host shell are not visible inside the container unless you explicitly pass them with docker run -e or through an env-file. Check your Dockerfile ENV statements as well. **Can I use environment variables as a replacement for a config file?** For simple deployments, yes. For complex applications with many configuration options, a config file combined with environment variable overrides often works better. Tools like Pydantic Settings and Dynaconf provide structured ways to handle this combination.
Ninad
Ninad

A Python and PHP developer turned writer out of passion. Over the last 6+ years, he has written for brands including DigitalOcean, DreamHost, Hostinger, and many others. When not working, you'll find him tinkering with open-source projects, vibe coding, or on a mountain trail, completely disconnected from tech.

Articles: 114