5.14. Environment Variables
Table of Contents
Why Environment Variables Matter
Environment variables are simple text values that your operating system provides to programs. Backend applications use them to store configuration that can change between machines and environments, for example:
- Database connection URLs
- API keys and secrets
- Debug / logging settings
- Service URLs (email provider, payment gateway, etc.)
The key idea: code stays the same, configuration changes via environment variables.
This gives you:
- Security: secrets are not hard coded into scripts or committed to Git.
- Flexibility: you can run the same code in development, staging, and production with different settings.
- Portability: your app can run on any machine or server that sets the right variables.
Always keep secrets and credentials in environment variables or other secure configuration, never directly in code or in your Git repository.
Common Environment Variables
You can define anything you like, but some names are common in backend projects:
| Variable | Example value | Purpose |
|---|---|---|
DATABASE_URL | postgresql://user:pass@localhost:5432/app | DB connection string |
SECRET_KEY | s3cr3t-r4nd0m-str1ng | For signing tokens, sessions, etc. |
DEBUG | true or false | Toggle debug features |
ENV / APP_ENV | development, staging, production | Current environment name |
PORT | 8000 | Port for web server |
REDIS_URL | redis://localhost:6379/0 | Redis connection |
SMTP_HOST | smtp.gmail.com | Email provider host |
You can pick your own names. Consistency across your project is what matters.
Viewing and Setting Environment Variables
How you set variables depends on the operating system and the shell.
On Linux and macOS (Bash / Zsh)
View all environment variables
printenv # or
envView a single value
echo "$HOME"
echo "$PATH"
echo "$DATABASE_URL"Set a variable for the current shell session
export DEBUG=true
export DATABASE_URL="postgresql://user:pass@localhost:5432/app"
After this, any program started from this shell (like python app.py) will see those variables.
Set a variable for a single command only
DEBUG=true python app.py
PORT=9000 uvicorn main:appAfter the command finishes, the variable is not kept.
Unset a variable
unset DEBUGOn Windows (Command Prompt and PowerShell)
Command Prompt (cmd)
set
set DEBUG
set DEBUG=true
python app.py
set DEBUG=PowerShell
Get-ChildItem Env:
$Env:DEBUG
$Env:DEBUG = "true"
python app.py
Remove-Item Env:DEBUGIn real projects you usually:
- Set environment variables in shell startup files (for development), or
- Use
.envfiles together with Python libraries (explained later), or - Configure them in your deployment platform (Docker, cloud provider, etc).
Reading Environment Variables in Python
Python provides the os module to access environment variables.
Using `os.environ`
import os
# Read a variable, may raise KeyError if not set
database_url = os.environ["DATABASE_URL"]
print(database_url)
If DATABASE_URL is not set, this will raise a KeyError. Use this when a variable is required and your program cannot run without it.
Using `os.getenv`
import os
# Read a variable, return None if not set
debug = os.getenv("DEBUG")
print(debug) # might be "true", "false", or NoneYou can also provide a default value:
debug = os.getenv("DEBUG", "false") # default "false" if DEBUG is not setThis is useful for optional settings.
Rule: Use os.environ["NAME"] for required variables, and os.getenv("NAME", default) when you can fall back to a safe default.
Converting to Proper Types
Environment variables are always strings. You must convert them to the correct types yourself.
Converting to integer
import os
port_str = os.getenv("PORT", "8000")
port = int(port_str) # "8000" -> 8000Converting to boolean
You decide which strings count as true or false. For example:
import os
def str_to_bool(value: str) -> bool:
return value.lower() in ("1", "true", "yes", "on")
debug_str = os.getenv("DEBUG", "false")
DEBUG = str_to_bool(debug_str)
Now DEBUG is a real bool.
Combining Environment Variables with Python Settings
In a backend project, you often create a dedicated module for configuration that reads environment variables once and exposes normal Python values.
For example, in config.py:
import os
def str_to_bool(value: str) -> bool:
return value.lower() in ("1", "true", "yes", "on")
class Settings:
def __init__(self) -> None:
self.env = os.getenv("APP_ENV", "development")
self.debug = str_to_bool(os.getenv("DEBUG", "true" if self.env == "development" else "false"))
self.database_url = os.environ["DATABASE_URL"] # required
self.secret_key = os.environ["SECRET_KEY"] # required
self.port = int(os.getenv("PORT", "8000"))
settings = Settings()Then elsewhere in your code:
from config import settings
print(settings.database_url)
print(settings.debug)This pattern keeps configuration logic in one place and makes refactoring easier.
Using `.env` Files in Development
Manually exporting lots of environment variables can be annoying. Many Python projects use a simple text file named .env that contains key value pairs.
Example .env in the project root:
APP_ENV=development
DEBUG=true
DATABASE_URL=postgresql://user:pass@localhost:5432/app
SECRET_KEY=dev-secret-key
PORT=8000This file is not automatically used by Python. You need a helper library to load it.
Using `python-dotenv`
Install:
pip install python-dotenv
Then in your application entry point, for example main.py:
from dotenv import load_dotenv
import os
# Load variables from .env into the process environment
load_dotenv()
# Now you can access them via os.getenv or os.environ
database_url = os.environ["DATABASE_URL"]
debug = os.getenv("DEBUG", "false")
print(database_url, debug)You can also load from a specific file:
load_dotenv(dotenv_path=".env.local")Typical pattern:
- Use
.envfor development defaults. - Use
.env.localfor developer specific overrides. - In production, do not use
.envfiles; set variables via the server or container configuration.
Never commit .env files that contain real secrets. Add them to .gitignore so they are not stored in your repository.
Example .gitignore entry:
.env
.env.localExample: Simple FastAPI App with Environment Variables
Here is a small example that shows environment variables used in a minimal FastAPI app.
.env:
APP_ENV=development
DEBUG=true
PORT=8000
GREETING=Hello from environment
config.py:
from dotenv import load_dotenv
import os
load_dotenv() # load from .env
class Settings:
def __init__(self) -> None:
self.env = os.getenv("APP_ENV", "development")
self.debug = os.getenv("DEBUG", "false").lower() == "true"
self.port = int(os.getenv("PORT", "8000"))
self.greeting = os.getenv("GREETING", "Hello")
settings = Settings()
main.py:
from fastapi import FastAPI
from config import settings
app = FastAPI(debug=settings.debug)
@app.get("/")
def read_root():
return {
"environment": settings.env,
"message": settings.greeting,
}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=settings.port, reload=settings.debug)Run:
python main.py
Then change values in .env and restart the app to see different behavior without touching the Python code.
Environment Variables Across Environments
The same backend code will often run in three or more environments:
| Environment | Example APP_ENV | Example DEBUG | Example DATABASE_URL |
|---|---|---|---|
| Development | development | true | postgresql://dev:dev@localhost:5432/dev |
| Staging | staging | false | postgresql://stag:stag@staging:5432/db |
| Production | production | false | postgresql://prod:prod@db:5432/prod |
You keep:
- The Python code identical, and
- Only change the environment variables per environment.
Typical workflow:
- Local development:
.envfile andpython-dotenv. - Docker: use
env_fileorenvironmentindocker-compose.yml. - Cloud: set environment variables in the dashboard or infrastructure config.
Common Pitfalls and Best Practices
Missing variables
If you blindly call os.environ["NAME"] and the variable is missing, your app will crash with KeyError.
A simple pattern:
import os
def require_env(name: str) -> str:
value = os.getenv(name)
if value is None:
raise RuntimeError(f"Required environment variable {name} is missing")
return value
DATABASE_URL = require_env("DATABASE_URL")This gives a clearer error message and fails early.
Confusing strings with other types
Remember, everything is a string. Possible mistakes:
- Trying
int(os.getenv("PORT"))whenPORTis not set, which raisesTypeError. - Using
if os.getenv("DEBUG"):and expecting it to beFalseif set to"false". It will be truthy because any non empty string is truthy.
Be explicit:
debug_str = os.getenv("DEBUG", "false")
DEBUG = debug_str.lower() == "true"Committing secrets
Never put values like real SECRET_KEY, DATABASE_URL with real passwords, or API keys into version controlled files.
Safer approaches:
- Use fake or dummy values in example files like
.env.example. - Use
.envlocally that is ignored by Git. - Use secret management in production (covered in a later chapter).
Example .env.example:
APP_ENV=development
DEBUG=true
DATABASE_URL=postgresql://user:pass@localhost:5432/app
SECRET_KEY=change-meDevelopers copy it:
cp .env.example .env
# then edit .env with real local valuesSummary
- Environment variables are key value strings provided by the operating system.
- Python reads them using
os.environandos.getenv. - Use them to control configuration such as database URLs, secrets, and debug flags.
- Convert them from strings to integers, booleans, and other types as needed.
- Use
.envfiles with tools likepython-dotenvfor local development convenience. - Do not commit real secrets to version control; keep them in environment variables or dedicated secret storage.
Environment variables are a small concept, but they are central to writing backend applications that are safe, configurable, and easy to deploy in different environments.
Views: 8
KAHIBARO