Practical guide · verified against the real thing
Environment variables: what they are and why your API keys live there
In one line: Configuration that changes per machine without changing code: how env vars work in shells, .env files and hosting dashboards — and the rules that keep secrets out of repos.
An environment variable is a named value the operating system hands to every program it starts: KEY=value, invisible in the code, set per machine. That one mechanism is why the same application runs unchanged on your laptop, in CI, and in production — and it is the standard answer to the most important question in configuration: what differs between those places? Database URLs, API keys, feature flags, ports. The principle has a name in the Twelve-Factor methodology — keep config in the environment, never in the code — and it exists because code goes places config must not: repositories, forks, clones, screenshots.
The shell: set, read, list
On Linux and macOS, export API_KEY="abc123" sets it for this terminal session and everything launched from it; echo "$API_KEY" reads it; env (or printenv) lists what's currently set. Windows: setx API_KEY "abc123" persists it for future sessions (new terminals only — a classic trap), set for the current one. In Python you read them with os.environ["API_KEY"] — and the deliberate choice of bracket-access over .get() matters: an app should crash at startup without its secrets, not limp on and discover the hole in production (the deploy walkthrough builds exactly this way).
The .env file: local convenience, with rules
Real projects use a .env file — one KEY=value per line — loaded into the environment at startup by the language's dotenv library. Two rules are non-negotiable. One: .env goes in .gitignore before the first commit — the file's entire purpose is to hold values the repo must never contain; most ecosystems ship a .env.example with the keys and dummy values for teammates instead. Two: .env is a local pattern. In production the host's dashboard or secret manager injects the same variables at runtime — no files to leak, and rotation is a form field, not a deploy. A committed .env is how repositories end up in credential-scanning botnets within hours; if it happens, the key is burned regardless of the cleanup — rotate first, tidy second.
The two gotchas that cost everyone an hour
Scope: variables set in one terminal don't exist in another, don't reach services the system already started, and don't survive a reboot unless persisted (export lines in your shell profile, or setx/Task Scheduler equivalents on Windows). "Works in my terminal, not in the app" is scope, almost every time. Name drift: API_KEY and APIKEY are different variables; the failure mode is a silent empty string, which is why the crash-at-startup read beats the default-to-empty read everywhere it's practical.
One closing boundary, because this piece is about secrets: environment variables beat hardcoded keys and beat .env-in-repo, and they are still not a vault — anyone with code execution on the machine can read the process environment. They are the right tool for keeping secrets out of code and repos; actual secret-management systems (the host's encrypted stores) are the next tier, when the stakes justify it. For the reasoning about what should never sit in a plain file at all, see the plain-text password rule.
Sources
Next