API keys: out of the code, out of the repository
Never in the source
API_KEY = "sk-proj-8f2a..." # noA key written in a file gets committed, and once it is in git history it stays there — deleting the line in a later commit does not remove it from the repository, and anyone who ever cloned it has a copy. Public repositories are scanned continuously by automated crawlers; keys committed to a public repository have been observed being used within minutes.
The same applies to notebooks, which store output as well as code, and to screenshots pasted into chats.
Environment variables
import os
key = os.environ["OPENAI_API_KEY"] # raises KeyError if unset
key = os.getenv("OPENAI_API_KEY") # returns None if unset
key = os.getenv("MODEL", "small-v1") # with a defaultSet it in the shell:
export OPENAI_API_KEY="sk-proj-..." # macOS, Linux
setx OPENAI_API_KEY "sk-proj-..." # Windows, persistentFor a required secret, prefer the bracket form or fail explicitly:
key = os.getenv("OPENAI_API_KEY")
if not key:
raise RuntimeError("OPENAI_API_KEY is not set; see README")A missing key that surfaces as None produces a confusing 401 from the API three functions later. A missing key that stops the program with a sentence naming the variable is answered in ten seconds.
Two shells that disagree
Environment variables are set per shell session, and where you set them decides who can see them. A variable exported in a login-shell profile such as ~/.zprofile is read by login shells only, so an interactive terminal has it and a script, a scheduled job or an editor's built-in terminal does not. The result is a program that works when you type the command and fails when anything else runs it, with no difference in the code. This exact failure has cost real projects weeks.
When a key is "definitely set" and the program disagrees, print what the program actually sees:
print("key present:", bool(os.getenv("OPENAI_API_KEY")))Print the boolean, never the value. That one habit keeps keys out of your logs and out of the screenshot you are about to paste.
.env files
For development, a file of key-value pairs is more convenient than exporting by hand:
# .env
OPENAI_API_KEY=sk-proj-...
DATABASE_URL=postgres://localhost/devfrom dotenv import load_dotenv # pip install python-dotenv
load_dotenv()
key = os.environ["OPENAI_API_KEY"]load_dotenv() reads the file into the process environment. It does not overwrite variables already set, so a real value in the environment beats the file — which is what you want when the same code runs in production.
Add .env to .gitignore before you create it. Commit a .env.example with the names and no values, so a new contributor knows what to set:
OPENAI_API_KEY=
DATABASE_URL=In a notebook
Colab has a secrets panel: the key icon in the sidebar, then
from google.colab import userdata
key = userdata.get("OPENAI_API_KEY")The value is stored against your account rather than in the notebook, so sharing the notebook does not share the key. In Jupyter, load_dotenv() works the same as anywhere else. What you must not do is type the key into a cell — the notebook file records it, and notebooks get emailed.
What to do when a key leaks
Assume the worst and act in this order:
- Revoke the key in the provider's dashboard. Immediately, before anything else. A revoked key is worthless to whoever has it.
- Issue a new one and update wherever it is configured.
- Check the usage or billing page for calls you did not make.
- Only then worry about cleaning history.
Rewriting git history with git filter-repo or BFG removes the string from the repository, and it does not undo the exposure — anyone watching already has it. Revocation is the fix; history cleaning is tidying afterwards.
Reduce what a leak can cost
- Give each key the narrowest scope the provider offers, and one key per application, so you can revoke one without stopping everything.
- Set a spend limit on the account. Most model APIs support a hard monthly cap, and this is the difference between an embarrassing evening and a five-figure bill.
- Never put a key in a URL or query string. URLs are logged by proxies, browsers and servers. Keys belong in a request header.
- Rotate on a schedule, and whenever somebody with access leaves.
Configuration is not only secrets
The same mechanism carries anything that differs between machines: database URLs, model names, feature flags, output folders. Keeping them in the environment rather than in the code means the same artefact runs in development and production, which is the point of the convention. Keep the reading of them in one small module, so there is a single list of everything the program expects to be told.
The one thing to keep
Keys live in the environment, never in the source or a notebook, and when one leaks the fix is revocation first — cleaning git history afterwards does not undo the exposure.
Before you move on
A developer commits a key by accident, notices within ten minutes, deletes the line, and pushes a new commit saying "remove key". Why is that insufficient?
Pick the one you would defend. Nobody sees your answer.