Search Topics
Search across all FastAPI topics
Environment Variables
FundamentalsYour database password is in your source code. Someone just pushed it to GitHub. You have about 30 minutes before things get bad.
You push your code to GitHub. Within 30 minutes, someone has scraped your DATABASE_URL from the commit history and is running queries against your production database.
[SECURITY ALERT] GitHub detected a potential secret in your repository. Commit: a1b2c3d — "Add database config" File: main.py — Line 5: DATABASE_URL = "postgresql://admin:p@ssw0rd@prod-db.example.com:5432/myapp"
Question
How does this happen? You write a database URL in your code because you need it to work. You push to GitHub because you need to deploy. And now the entire internet can see your production password. The fix isn't "be more careful" — it's to keep secrets out of your code entirely.
The Flow: Secrets Stay Outside Code
Here's the key idea: your code reads configuration from the environment, never from hardcoded values. This way, secrets live in a file that never gets committed.
.env file
Secrets live here (gitignored)
load_dotenv() or pydantic-settings
Reads the file
Environment variables
Available to your app
Your code
Uses settings.database_url
Why env vars matter
What you just learned
Secrets in source code get exposed when you push to GitHub
Environment variables keep configuration outside your codebase
.env files store secrets locally — and get gitignored so they're never committed
Why Not Just Hardcode It?
Beyond security, there's a practical reason: different environments need different values. Your local database URL isn't the same as staging or production. Hardcoding means changing code every time you deploy somewhere new.
Environment variables let you change configuration without changing code. Same codebase, different settings per environment.
The .env File
A .env file stores your configuration as simple key-value pairs. It lives in your project root and should never be committed to version control.
DATABASE_URL=postgresql://user:password@localhost/mydb
SECRET_KEY=super-secret-key-change-me
DEBUG=true
API_VERSION=v1Watch out
Add .env to your .gitignore before your first commit. If you add it after, the file is already in your git history — and removing it from history is a pain.
Loading with python-dotenv
The simplest approach: python-dotenv reads your .env file and makes the values available through os.getenv(). It works, but there's no type validation — everything comes back as a string.
from dotenv import load_dotenv
import os
load_dotenv() # Load .env file into environment
DATABASE_URL = os.getenv("DATABASE_URL")
SECRET_KEY = os.getenv("SECRET_KEY", "fallback-key")
# Careful — this is a string, not a boolean!
DEBUG = os.getenv("DEBUG", "false").lower() == "true"Pydantic Settings (The Better Way)
For production apps, pydantic-settings is the move. It reads your .env file, validates types automatically, provides defaults, and gives you a clean settings object with IDE autocomplete.
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str # Required — app crashes if missing
secret_key: str # Required — no forgetting this one
debug: bool = False # Optional with default — auto-parsed!
api_version: str = "v1" # Optional with default
class Config:
env_file = ".env"
settings = Settings()
# settings.database_url → reads DATABASE_URL from .env
# settings.debug → True (auto-converted from string "true")
# Typo in your env var name? Pydantic catches it at startup.Loading config
What you just learned
python-dotenv is simple but everything is a string — you parse types yourself
pydantic-settings validates types, provides defaults, and catches missing vars at startup
Both read from .env files — pydantic-settings just does more for you
The sneaky override trap
You set DATABASE_URL in both your .env file and a Docker environment variable. Your app uses the wrong one and you spend an hour debugging.
# .env file
DATABASE_URL=postgresql://user:pass@localhost/dev
# Docker compose
environment:
- DATABASE_URL=postgresql://user:pass@prod-db/prod
# config.py
from dotenv import load_dotenv
load_dotenv() # Which value wins?Try It: Config Validator
Edit the .env values and watch Pydantic Settings validate them in real-time.
Think about it...
If you set DATABASE_URL in both your .env file and your shell environment, which one does python-dotenv use by default?
Hint: Think about what 'override' means as a parameter...
Key Points
Never Commit Secrets
Passwords, API keys, and tokens should never appear in your source code
.env for Local Config
Store environment-specific values in a .env file at project root
pydantic-settings for Type Safety
Get automatic validation, defaults, and autocomplete for your config
.gitignore Your .env
Always add .env to .gitignore to prevent accidental commits
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
from fastapi import FastAPI
app = FastAPI()
DATABASE_URL = "postgresql://admin:p@ssw0rd@db.example.com/prod"
SECRET_KEY = "my-super-secret-jwt-key"
# These will end up on GitHub!# .gitignore
__pycache__/
*.pyc
# Oops — .env is not listed!