Python environment variables are accessed via os.environ (stdlib) or os.environ.get() with defaults. The team that uses python-dotenv for local dev loads .env files automatically. The team that uses pydantic-settings has type-validated config. Hardcoded secrets in source code (API keys, DB passwords) are the #1 mistake; the team that uses env vars has secrets only in the environment, never in code or git.
Table of contents
- Reading env vars
- The .env file pattern
- pydantic-settings for validation
- Production secrets
- Common pitfalls
- Twelve-factor app
- FAQ
Reading env vars
import os
# Required - raises KeyError if missing
db_url = os.environ['DATABASE_URL']
# Optional with default
log_level = os.environ.get('LOG_LEVEL', 'INFO')
# Cast
port = int(os.environ.get('PORT', '8000'))
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
The team that uses os.environ.get(..., default) always has working code (even with missing vars). The team that uses os.environ[...] crashes on missing vars (sometimes good, sometimes bad).
The .env file pattern
For local dev, use a .env file:
# .env (gitignored)
DATABASE_URL=postgresql://localhost/dev
SECRET_KEY=local-dev-only
DEBUG=true
Load it with python-dotenv:
from dotenv import load_dotenv
load_dotenv() # loads .env into os.environ
The team that uses .env files has local config that doesn’t leak to git. The team that hardcodes config in code has ‘works on my machine’ issues.
pydantic-settings for validation
The team that wants type-safe config:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
log_level: str = 'INFO'
debug: bool = False
port: int = 8000
class Config:
env_file = '.env'
settings = Settings() # raises ValidationError if required missing
The team that uses pydantic-settings gets validation at startup, not at first request. Errors surface immediately.
Production secrets
For production, don’t commit secrets anywhere:
- Cloud: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault.
- Container: Kubernetes Secrets (with encryption-at-rest) or external secret managers.
- Bare metal: HashiCorp Vault, or environment injection from a secrets manager.
The team that uses Vault Agent has secrets pulled at runtime, never in code or disk. The team that uses AWS Secrets Manager + ECS task role has secrets in IAM, not env vars on disk.
Common pitfalls
- Booleans: env vars are strings.
"false" == "false"is True, butos.environ.get('DEBUG')returns the string"false", not False. The team that castsvalue.lower() == 'true'has correct booleans. - Numbers:
PORT=8000is a string"8000", not int. The team that castsint(...)has correct types. - Whitespace:
os.environ.get('TOKEN')may include trailing newlines from.envfiles. The team that uses.strip()has clean values. - Empty strings:
KEY=in.envgives empty string, not None. The team that checksif not value:handles empty correctly.
Twelve-factor app
The 12-factor app methodology says: store config in environment variables. The team that follows this has the same config in dev/staging/prod - just different env vars. No code changes between environments.
FAQ
What’s the difference between os.environ and os.getenv?
Same thing. os.getenv('VAR') is equivalent to os.environ.get('VAR'). The team that uses either has consistent style.
Should I commit .env files to git?
No. Add .env to .gitignore. The team that commits .env has secrets in git history (forever, even after removing).
How do I validate env vars are set?
Use pydantic-settings with required fields, or check at startup: required = ['DATABASE_URL', 'SECRET_KEY']; missing = [v for v in required if v not in os.environ]; assert not missing, .... The team that uses validation catches config errors early.
Can I use env vars for arrays or nested config?
Yes - JSON in env vars. ITEMS='[1,2,3]' then json.loads(os.environ['ITEMS']). The team that uses JSON for complex config has flexibility without nested env vars.
What about per-environment .env files?
.env.development, .env.production. Load the right one based on FLASK_ENV or DJANGO_SETTINGS_MODULE. The team that uses per-env files has separate config without env var gymnastics.
If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.
Useful related references: