34 Environment Variables and Secrets
Prerequisites (read first if unfamiliar): Chapter 11, Chapter 31.
See also: Chapter 24, Chapter 15, Chapter 14.
Purpose

Sooner or later — usually the first time you use an API — you will need to put a secret in your code. An API key, a database password, an access token, a private URL. The question is where, and the answer is almost always “not in the source file, and definitely not in git.”
This chapter teaches you the small set of habits that will keep you from committing a credential to a public repository and getting an urgent email from GitHub at 2 a.m. telling you your AWS key is being used to mine cryptocurrency. (This happens. Regularly.) You will learn what environment variables are, how .env files work, how to use python-dotenv to load them, how to keep secrets out of git, and what to do if you do accidentally leak one.
Learning objectives
By the end of this chapter, you should be able to:
- Explain what an environment variable is and how a Python program reads one.
- Set environment variables in your shell and read them back.
- Use a
.envfile to store project-specific secrets and load them withpython-dotenv. - Explain why
.envmust be in.gitignoreand why a.env.exampleshould be committed. - Load secrets in a notebook without echoing them into cell output.
- Recognize the signs that you have leaked a secret into git and take the right emergency steps.
- Choose between a
.envfile, a system environment variable, and a password manager for different situations.
Running theme: a secret in a source file is already leaked
If a credential is in a file tracked by git, you should treat it as compromised the moment you stage it. Rotate it, remove it, and put the new one in an environment variable.
34.1 What an environment variable is
Every running process on your computer has a set of key-value pairs called its environment. These are inherited from the shell that started the process. Typical entries include PATH (where to find executables), HOME (your home directory), and USER (your login name). You can add anything else you like.
Python reads environment variables through the os module:
import os
api_key = os.environ["GITHUB_TOKEN"] # raises KeyError if missing
api_key = os.environ.get("GITHUB_TOKEN") # returns None if missing
api_key = os.getenv("GITHUB_TOKEN", "") # returns default if missingUse the os.environ[...] form when the program requires the variable — the KeyError will tell you immediately that something is misconfigured, instead of silently passing None to requests and failing with a confusing 401.
None
Three failure modes cover 90% of “my secret isn’t being read.” First, the .env file is not in the working directory the program is launched from — load_dotenv() looks for .env in the current directory, not the file’s directory. Launch from your project root, or call load_dotenv(dotenv_path="path/to/.env") explicitly. Second, the variable name has a typo — GITHUB_TOEKN looks fine at a glance. Print list(os.environ.keys()) to see what actually loaded. Third, the variable is set in a different shell session — environment variables set with export only apply to the shell they were set in, so a variable set before jupyter lab won’t be visible in a notebook started from a different terminal.
If you ever see the value itself in a traceback or log, rotate the secret immediately — assume it is compromised. Do not just remove the log line.
34.2 Setting environment variables in your shell
You can set them on the command line before running your program:
macOS / Linux (bash, zsh):
export GITHUB_TOKEN=ghp_abc123...
python fetch_issues.pyWindows (PowerShell):
$env:GITHUB_TOKEN = "ghp_abc123..."
python fetch_issues.pyWindows (cmd):
set GITHUB_TOKEN=ghp_abc123...
python fetch_issues.py
These set the variable for the current shell session only. Close the terminal and it is gone.
For a variable you want every time you open a shell, add the export line to your shell profile (~/.bashrc, ~/.zshrc, or ~/.profile). This is fine for truly personal, always-needed secrets. For project-scoped secrets, prefer a .env file so they stay with the project.
34.3 .env files
A .env file is a plain-text file in your project directory that contains KEY=value lines. It is the de facto standard for project-scoped environment variables.
# .env
GITHUB_TOKEN=ghp_abc123xyz...
OPENWEATHER_API_KEY=abc987def...
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
DEBUG=1
A few rules:
- One variable per line,
KEY=valuewith no spaces around the=. - Lines starting with
#are comments. - Values are strings. Do not quote them unless they contain spaces.
- Do not put secrets in comments. (Yes, people do this.)
34.4 python-dotenv: loading a .env file into your program
The python-dotenv package reads a .env file and populates os.environ from it, so the rest of your code can just read environment variables as usual.
Install it in your venv (see Chapter 15):
python -m pip install python-dotenvAt the top of your script or notebook:
from dotenv import load_dotenv
load_dotenv() # reads .env from the current directory by default
import os
token = os.environ["GITHUB_TOKEN"]Conventions:
- Call
load_dotenv()once, at the very top of your entry-point script, before any module that reads environment variables. - It is a no-op if
.envdoes not exist — your program still runs, it just relies on system environment variables instead. - By default,
load_dotenvdoes not overwrite variables that are already set in the environment. This is the behavior you want for CI — production overrides.env.
34.5 .gitignore: the line that matters most in this chapter
Every project that uses .env must have this line in .gitignore:
.env
Before you add a single secret to the file, run:
echo ".env" >> .gitignore
git add .gitignore
git commit -m "Ignore .env"Verify:
git status.env should not appear. If it does, .gitignore is wrong.
Equally important: commit a .env.example with the variable names but not the values:
# .env.example — copy to .env and fill in real values
GITHUB_TOKEN=
OPENWEATHER_API_KEY=
DATABASE_URL=
DEBUG=
This file is safe to commit and tells collaborators (and future you) which secrets a project needs. A new contributor runs cp .env.example .env, fills in their own keys, and is off to the races.
See Chapter 31 for the full .gitignore story.
34.6 Secrets in Jupyter notebooks
Notebooks have a special trap: cell output gets saved with the notebook. If you print(os.environ["API_KEY"]) in a cell, the key is now embedded in the .ipynb file and will be committed on your next git add.
Three rules for notebooks:
- Never print a secret. Even to check. If you must verify, print
len(api_key)or the first 4 characters (api_key[:4]). - Clear outputs before committing.
Cell → All Output → Clearin Jupyter Lab, or set up a git pre-commit hook withnbstripoutto strip outputs automatically (the pre-commit framework is covered briefly in Chapter 33). - Load secrets in the first cell and do not reference them by value again; pass them into function calls so they never become a standalone cell result.
# First cell
from dotenv import load_dotenv
load_dotenv()
import os
API_KEY = os.environ["OPENWEATHER_API_KEY"]
# Do NOT add `API_KEY` as the last expression in a cell.34.7 What if I already leaked a secret?
The bad news: if you committed and pushed a secret to a public repository, assume it is compromised. People actively scan GitHub for exposed credentials within seconds of a push.
The right emergency response, in order:
Rotate the credential immediately. Go to the service that issued it (GitHub, AWS, OpenWeather, etc.) and generate a new one. Revoke the old one. This is the only step that actually stops the damage; everything else is cleanup.
Update your local
.envwith the new value. Verify the program still works.Remove the secret from the commit. If it is the most recent commit and you have not pushed:
git reset HEAD~1 # unstages the commit # edit the file to remove the secret git add . git commit -m "Your message"If you have already pushed, the secret is in the public history and removing it from future commits does not erase it. You can rewrite history with tools like
git filter-repoor BFG Repo-Cleaner, but this rewrites every commit SHA and forces every collaborator to re-clone. In practice, rotating the credential is almost always the right move and you can leave the old one in history.Add
.gitignoreentries so it cannot happen again.Think about scope. If the key had broad access (a root AWS key, a production database URL), you may have more cleanup to do: check access logs, look for unauthorized usage, notify your team.
GitHub also offers secret scanning for public repos — it detects known secret formats and emails you (and sometimes the issuing service) automatically. Do not rely on it as your only line of defense, but it is a useful last-ditch safety net.
34.8 Stakes and politics
Secrets management is taught as a hygiene topic, but the stakes are very different depending on what the secret protects. Two things to notice. First, whose data leaks when a credential is exposed. A leaked AWS access key on a personal side project is mostly the developer’s problem — surprise charges, a compromised account, an embarrassing afternoon. A leaked API key into a healthcare system, a learning-management platform, a municipal records database, or a research dataset of human-subjects data is somebody else’s problem: the patients, the students, the residents, or the research participants whose information was protected by that key. The cost of carelessness scales with the sensitivity of the data, and that cost is very rarely paid by the person who was careless.
Second, the burden of doing this right is unevenly distributed. Regulated industries have compliance teams, dedicated secret stores (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager), formal rotation schedules, and audit trails. Indie developers, students, small open-source projects, and academic labs typically have none of those things and are expected to invent the equivalent for themselves with .env files and good intentions. The same accident that produces an internal incident report at a regulated company is a public Twitter screenshot for an indie developer. Knowing which threat model you are operating in is part of operating safely; pretending the differences do not exist is how most leaks happen.
See Chapter 8 for the broader framework. The concrete prompt to carry forward: when you handle a secret, ask whose data it actually protects — and let the answer set how careful you are.
34.9 Worked examples
A new project from scratch
mkdir weather-dashboard && cd weather-dashboard
python -m venv .venv
source .venv/bin/activate
python -m pip install requests python-dotenv
# Set up secrets hygiene BEFORE writing any code
echo ".env" >> .gitignore
echo ".venv/" >> .gitignore
# Record which variables the project needs
cat > .env.example <<'EOF'
OPENWEATHER_API_KEY=
EOF
# Create the real .env (git will ignore it)
cp .env.example .env
# Now edit .env and paste your real key
git init
git add .gitignore .env.example
git commit -m "Initial project with secrets hygiene"The first commit has no secrets in it, and cannot accidentally grow any because .env is ignored from the start.
A script that uses the secret
# fetch_weather.py
import os
import requests
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ["OPENWEATHER_API_KEY"]
def current_weather(city):
resp = requests.get(
"https://api.openweathermap.org/data/2.5/weather",
params={"q": city, "appid": API_KEY, "units": "metric"},
timeout=10,
)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
data = current_weather("Boulder,US")
print(f"{data['name']}: {data['main']['temp']}°C")Run it:
python fetch_weather.pyThe script works on your machine because .env is there. When a collaborator clones the repo, they will get a KeyError telling them exactly which variable is missing. That is the error message you want.
Verifying without printing
api_key = os.environ["OPENWEATHER_API_KEY"]
print(f"loaded key: length={len(api_key)}, starts with {api_key[:4]}...")Now you can tell the key loaded correctly without broadcasting it.
34.10 Templates
A minimal .env.example:
# Copy to .env and fill in. Never commit .env.
# API keys
OPENWEATHER_API_KEY=
# Database
DATABASE_URL=
# Feature flags
DEBUG=0
A safe “load and verify” snippet for the top of any script:
import os
from dotenv import load_dotenv
load_dotenv()
REQUIRED_VARS = ["OPENWEATHER_API_KEY", "DATABASE_URL"]
missing = [v for v in REQUIRED_VARS if not os.environ.get(v)]
if missing:
raise RuntimeError(
f"Missing required environment variables: {missing}. "
f"Copy .env.example to .env and fill them in."
)This gives you a useful error on day one instead of a confusing 401 on day three.
34.11 Exercises
- Create a new project folder, initialize a git repo, and set up
.env,.env.example, and.gitignorein the right order. Confirm withgit statusthat.envdoes not appear. - Write a
check_secrets.pythat loads.env, reads a variable, and prints its length (not its value). Run it and confirm the length matches your key. - Accidentally stage a
.envfile (git add -f .env), then recover: unstage it withgit restore --staged .env, and verifygit statusis clean again. - Write a script that requires two environment variables and fails with a clear error message if either is missing. Use the template from section 9.
- In a Jupyter notebook, load an API key from
.envand use it in arequestscall. Do not let the key appear in any cell output. Save the notebook and open the.ipynbfile in a text editor to verify the key is not in there. - Find a GitHub repo that has accidentally committed a
.env(they exist — search forfilename:.env DB_PASSWORD). Read the comments from other users. Note how the maintainer responded. Do not copy the credentials. - Set an environment variable in your shell profile and a conflicting one in
.env. Run a program and observe which one wins (hint: by default, system env wins overload_dotenv).
34.12 One-page checklist
.envgoes in.gitignorebefore you add any secrets.- Commit a
.env.examplewith variable names but no values. - Use
load_dotenv()at the top of your entry-point script. - Read secrets with
os.environ["KEY"]so missing values fail loudly. - Never
printa secret. In notebooks, never let a secret be a cell’s last expression. - If a secret leaks, rotate first, clean up history second.
- For shared team secrets, use a password manager or a dedicated secret store, not a shared
.envin chat. - Keep separate
.envfiles per project; do not reuse one across projects. - When in doubt,
grep -ri "password\|token\|key" .before everygit add.
- Adam Wiggins, The Twelve-Factor App: Config — the source of the “environment variables for secrets” convention; the whole 12-factor manifesto is worth a half-hour of reading even if you only build one factor’s worth of apps.
- Saurabh Hooda,
python-dotenvdocumentation — the library most projects use to load.envfiles into process environments. - GitHub, Removing sensitive data from a repository — the official guide to rotating and erasing leaked credentials.
- HashiCorp, Vault documentation — the canonical free, self-hostable secret store; useful to know exists when
.envfiles stop being adequate. - AWS, Secrets Manager and Google Cloud, Secret Manager — the hosted options most cloud-native teams use; the right tier of tool when secrets need rotation, audit logs, and IAM-based access control.
- OWASP, Cheat Sheet: Secrets Management — practitioner consensus on threat-model-driven secrets handling; useful when you graduate from “do not commit secrets” to “design a rotation policy.”
- Troy Hunt, Have I Been Pwned — search-by-email service for credential leaks; not a secrets-management tool but useful context for the scale of the underlying problem.