API keys and secrets
Authenticate safely and keep keys out of your code.
Why it matters
An API key for an LLM provider is a direct line to your credit card. Anyone who has it can run up charges in your name. Leaked keys are found and abused within minutes of being pushed to a public repository. Handling keys properly is a small habit every AI tutorial assumes you already have. It's also the one beginners most often get wrong.
What you'll be able to do
- Send an API key in the header an API expects
- Read a key from an environment variable instead of writing it in code
- Keep keys out of git with a .env file and .gitignore
- Respond correctly when a key leaks
What it covers
- What an API key is
- Keep the key out of your code
- Environment variables
- .env files and .gitignore
- Worked example: a script that never contains the key
- Deleting isn't revoking
What an API key is
An API key is a long random string that identifies your account to an API, something like sk-ant-api03-Xk2.... Every request you send carries it. That way the provider knows whose request it is, what that account may do, and who to bill.
Think of a hotel key card. Whoever holds it gets into the room, and nobody checks their face. An API key is a password for programs, usually with no second check. Whoever holds the key is you, as far as the API is concerned. They can spend your credits, read your data, and get your account suspended for what they do with it.
The practice API has a key too: gk_demo_7Hq2_not_a_real_secret. It's printed here because it's fake. A real key never appears in a lesson, a screenshot, a support ticket or a chat with an AI assistant.
Sending a key
APIs expect the key in a header. The two common patterns:
X-Api-Key: gk_demo_7Hq2_not_a_real_secret Authorization: Bearer sk-...
- The first is a custom header. Anthropic's API uses
x-api-key(header names ignore capitals). - The second is the standard
Authorizationheader, with the wordBearerand a space before the key. OpenAI and many others use it.
The API's documentation tells you which one it wants.
Never put a key in the URL's query string. URLs end up in server logs, browser history and analytics tools.
import requests url = "https://api.gradient.test/v1/report" no_key = requests.get(url) print(no_key.status_code, no_key.json()["error"]["message"]) wrong_key = requests.get(url, headers={"X-Api-Key": "guess-123"}) print(wrong_key.status_code, wrong_key.json()["error"]["message"]) right_key = requests.get(url, headers={"X-Api-Key": "gk_demo_7Hq2_not_a_real_secret"}) print(right_key.status_code, right_key.json())
401 means no valid key, and 403 means the key was rejected. Both are your cue to check how the key is being sent, before anything else.
Show the answer
In a request header, such as X-Api-Key or Authorization: Bearer
Headers are where APIs expect keys, and they don't get written into logs and browser history the way URLs do.
Keep the key out of your code
The tempting version looks like this:
API_KEY = "sk-ant-api03-Xk2..." # don't do this
It works, and it's dangerous. The key is now in every copy of the file:
- in your git history, even after you delete the line,
- on GitHub, if you push,
- in the notebook you share with a colleague,
- in the screenshot you post asking for help,
- in every AI assistant that reads your project.
Automated bots scan public GitHub repositories for keys around the clock.
The fix is to keep the key outside the code, and have the code look it up when it runs.
key = "sk-abc123def456" print(key[:6] + "...")
Show the answer
sk-abc...
key[:6] takes the first six characters, sk-abc, and + adds the dots: sk-abc... Showing only the first few characters is a safe way to log which key was used.
Environment variables
An environment variable is a named value the shell hands to every program it starts. You set one in the terminal with export. You read it with echo and a $ in front of the name:
export ANTHROPIC_API_KEY=sk-ant-api03-Xk2... echo $ANTHROPIC_API_KEY
No spaces around the =. A variable set this way lasts only as long as that terminal window.
Python reads environment variables through os.environ, which behaves like a dictionary:
import os os.environ["GRADIENT_API_KEY"] = "gk_demo_7Hq2_not_a_real_secret" # stands in for: export GRADIENT_API_KEY=... key = os.environ.get("GRADIENT_API_KEY") print("Found a key:", key is not None) missing = os.environ.get("SOME_OTHER_KEY") print(missing)
os.environ["NAME"] raises a KeyError if the variable isn't set. os.environ.get("NAME") returns None instead, so you can show a clear message.
In the browser there's no terminal to run export in. So lessons set the variable in code on the first line, with a comment. In your own projects, that line doesn't exist.
Programs started from this terminal can now read GRADIENT_API_KEY. Close the window and it's gone.
.env files and .gitignore
Typing export every time gets old. The usual pattern for a project is a file called .env in the project folder, with one NAME=value per line:
ANTHROPIC_API_KEY=sk-ant-api03-Xk2... GRADIENT_API_KEY=gk_demo_7Hq2_not_a_real_secret
A small package, python-dotenv, loads that file into the environment when your program starts. The rest of your code uses os.environ.get as before:
from dotenv import load_dotenv import os load_dotenv() # reads .env into os.environ api_key = os.environ.get("ANTHROPIC_API_KEY")
The .env file must never be committed. Add it to .gitignore before your first commit:
.env .venv/
Then commit a .env.example with the names but empty values. Anyone setting up the project then knows which keys they need. Hosting services, such as Railway, Vercel and AWS, have a settings page for environment variables. That's where keys go for code running in the cloud.
If a key leaks
Assume it's compromised the moment it's somewhere public, even for a minute.
- Revoke it in the provider's dashboard and create a new one. This is the only step that actually stops misuse.
- Update your
.envand any hosting settings with the new key. - Check your usage and billing pages for activity you don't recognize.
Deleting the commit or making the repository private afterwards doesn't help. The key is still in git history, in copies of the repository, and in whatever the scanners already saved.
Two habits limit the damage in advance. Set a monthly spending limit with each provider. And use a separate key per project, so you can revoke one without breaking everything.
Worked example: a script that never contains the key
Fetch the practice API's report using a key read from the environment. The script stops with a clear message if the key is missing, and never prints the key itself.
import os import requests os.environ["GRADIENT_API_KEY"] = "gk_demo_7Hq2_not_a_real_secret" # stands in for your .env file def get_report(): key = os.environ.get("GRADIENT_API_KEY") if key is None: raise RuntimeError("GRADIENT_API_KEY isn't set. Copy .env.example to .env and fill it in.") response = requests.get( "https://api.gradient.test/v1/report", headers={"X-Api-Key": key}, timeout=10, ) response.raise_for_status() return response.json() report = get_report() print("Status: OK") print("Report fields:", sorted(report))
It prints Status: OK, then the report's four fields: ['active_campaigns', 'revenue', 'roas', 'spend']. The key never appears in the output.
The key lives in one place, the environment, and the code only looks it up. Missing keys fail early, with a message that says how to fix them, instead of as a confusing 401 later. Delete the first line and run it again to see that message. On your own computer, load_dotenv() would fill the environment from .env instead.
key = os.environ["ANTHROPIC_API_KEY"], but you never set that variable. What happens?Show the answer
A KeyError: the variable doesn't exist
Square brackets raise a KeyError for a missing name. os.environ.get("ANTHROPIC_API_KEY") would return None instead, so you can show a clearer message.
Show the answer
Revoke the key in the provider's dashboard and create a new one
Only revoking the key stops someone who already copied it. The old commit stays in history even if you remove the line.
Deleting isn't revoking
A key that was ever committed stays in the repository's history. Removing it in a later commit doesn't help, and neither does making the repository private. Revoke the key first.
Practice what you just read
- 5 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
What's the difference between os.environ["API_KEY"] and os.environ.get("API_KEY") when the variable isn't set?