
You've just signed up for the OpenAI API, copied your shiny new API key, and pasted it directly into your Python script so you can start building. It works perfectly — the LLM responds, you feel the rush of progress, and you push your code to GitHub to share it with a colleague. Within hours, a bot has scraped your key from the public repository, and by morning you're staring at a $300 unexpected charge on your account from someone's crypto-mining script that found your key and put it to work.
This isn't a hypothetical horror story. It happens to developers at every experience level, every single week. API keys for LLM services like OpenAI, Anthropic, and Google are particularly attractive targets because they're easy to monetize — you pay per token, and attackers can rack up enormous bills before you even notice. Managing these secrets properly isn't an advanced topic you'll get to someday. It's foundational, and it needs to be done right from the very first line of code you write.
By the end of this lesson, you'll understand exactly what API keys are, why they're dangerous if mishandled, and how to manage them properly using environment variables and dedicated secret management tools. You'll have a system you can apply immediately to every LLM project you build.
What you'll learn:
.env files with python-dotenv for local developmentYou should be comfortable writing basic Python scripts and have used a terminal (command prompt on Windows, Terminal on macOS/Linux) before. You don't need to have used any LLM APIs yet — in fact, this lesson will help you start those integrations correctly from day one.
Before we can protect something, we need to understand what it is.
An API (Application Programming Interface) is a way for your code to talk to someone else's service over the internet. When you use the OpenAI API, your Python script sends a request to OpenAI's servers asking them to run a language model and return a response. Those servers need to know who's making the request — both to charge the right account and to enforce usage limits.
An API key is essentially a password for a machine. It's a long, random string of characters — something like sk-proj-a8f2k3... — that you include with every request. OpenAI's servers check that key, look up your account, and decide whether to process the request. No key, no access. Wrong key, no access.
Here's the crucial distinction that trips people up: unlike a regular password, an API key is meant to be used programmatically, automatically, without a human typing it in each time. That convenience is also what makes it dangerous. Your code needs to know the key in order to use it, which means the key has to live somewhere your code can find it.
The naive approach — and the wrong approach — is to put it directly in your code:
# DON'T DO THIS
import openai
client = openai.OpenAI(api_key="sk-proj-a8f2k3mxp9...")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Summarize this contract."}]
)
The moment that string literal lives in your code, it can end up in version control, log files, error messages, screenshots, and a dozen other places you didn't intend. Let's learn the right way.
Here's the principle that underlies all good secret management: your code describes what to do, and your environment provides what it needs to do it.
Think of it like a recipe versus your specific kitchen. The recipe says "add salt." It doesn't say "use the Morton salt in the blue container on the second shelf." Your kitchen provides the salt; the recipe just describes the action. Someone else can use the same recipe in their kitchen with their salt.
Similarly, your Python script should say "use the API key from the environment." The actual key lives in the environment — meaning the operating system's variable storage — not in the script. The same code can run in your local development environment, your colleague's machine, and a production server, each one supplying its own key without the key ever touching the codebase.
This separation is the core idea behind environment variables — a feature built into every operating system that lets you store key-value pairs that running programs can read.
Every operating system maintains a set of named values called environment variables that any program can read. You can think of them as a secure notepad that your operating system holds for you, separate from your files and code.
On macOS and Linux:
export OPENAI_API_KEY="sk-proj-a8f2k3mxp9yourkeyhere"
The export command makes this variable available to any program you launch from that terminal session. You can verify it worked by running:
echo $OPENAI_API_KEY
On Windows (Command Prompt):
set OPENAI_API_KEY=sk-proj-a8f2k3mxp9yourkeyhere
On Windows (PowerShell):
$env:OPENAI_API_KEY = "sk-proj-a8f2k3mxp9yourkeyhere"
Important limitation: Variables set this way only exist for the current terminal session. Close the terminal window, and they're gone. This is actually a useful safety property for temporary work, but it's inconvenient for daily development. We'll fix that shortly.
Python's standard library has a module called os that gives you access to environment variables through a dictionary-like object called os.environ. Here's how you use it:
import os
import openai
# Read the key from the environment
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError(
"No OPENAI_API_KEY found in environment. "
"Please set this variable before running the script."
)
client = openai.OpenAI(api_key=api_key)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Summarize this contract."}]
)
print(response.choices[0].message.content)
Notice we're using .get() instead of os.environ["OPENAI_API_KEY"]. The difference matters: if the variable isn't set, os.environ["OPENAI_API_KEY"] raises a KeyError with an unhelpful error message, while .get() returns None, which lets us write a clear, helpful error message ourselves.
Also notice we're raising an error immediately if the key is missing, rather than letting the code fail later with a confusing authentication error from the API. This is called failing fast — surfacing problems at the earliest possible moment, where they're easiest to understand and fix.
Tip: Many LLM libraries, including the official OpenAI Python client, will automatically look for a standard environment variable (like
OPENAI_API_KEY) if you don't explicitly pass a key. You can often just writeclient = openai.OpenAI()and it handles the lookup for you. Check your library's documentation — but even then, the variable still needs to be set in your environment.
Setting environment variables manually in a terminal every time you open a new session is tedious, and it doesn't help your teammates who need the same setup. The practical solution for local development is a .env file combined with a library called python-dotenv.
A .env file is a plain text file that lives in your project directory and contains your environment variables in a simple format:
OPENAI_API_KEY=sk-proj-a8f2k3mxp9yourkeyhere
ANTHROPIC_API_KEY=sk-ant-api03-youranthropickey
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
ENVIRONMENT=development
The python-dotenv library reads this file and loads those values into environment variables when your application starts. Your code doesn't change at all — it still reads from os.environ. The .env file just becomes a convenient way to set those variables.
Install it using pip:
pip install python-dotenv
Then add two lines to the top of your script:
from dotenv import load_dotenv
import os
import openai
# Load variables from .env file into the environment
load_dotenv()
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY not found. Check your .env file.")
client = openai.OpenAI(api_key=api_key)
load_dotenv() reads your .env file and populates os.environ with its contents. Everything after that line works exactly as before — os.environ.get() doesn't know or care whether the variable came from the operating system or from the .env file.
Here's where most people make the mistake that defeats the entire purpose. A .env file full of secrets is useless if it gets committed to your Git repository. You must tell Git to ignore it.
Your project should have a file called .gitignore in the root directory. Open it (or create it if it doesn't exist) and add this line:
.env
That's it. Git will now refuse to track that file, which means it will never be included in a commit and will never be pushed to GitHub, GitLab, or anywhere else.
To verify the file is being ignored, run:
git status
Your .env file should not appear in the output. If it does appear, it means it was already tracked by Git before you added the ignore rule. In that case, you need to remove it from tracking:
git rm --cached .env
git commit -m "Remove .env from tracking"
Warning: If you've already committed a file containing secrets to a repository — even if you delete the file afterward — the secret is in Git's history and should be considered compromised. Rotate the key immediately (generate a new one and revoke the old one through the API provider's dashboard), then clean the history using a tool like
git-filter-repo. Don't wait, don't hope no one noticed. Rotate the key.
Just because your .env is secret doesn't mean your teammates should have to guess what variables they need. Create a file called .env.example (or .env.template) that contains the variable names but not the actual values:
# Copy this file to .env and fill in your actual values
OPENAI_API_KEY=your_openai_api_key_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here
DATABASE_URL=your_database_connection_string_here
ENVIRONMENT=development
This file gets committed to the repository. Anyone cloning the project can copy it, fill in their own values, and be up and running immediately. The convention is clear: .env.example is a template that's safe to share; .env is the real thing and never leaves your machine.
Real applications don't just run on your laptop. They run in multiple environments — your local development setup, a staging server where you test before release, and the production server where real users interact with your application. Each environment needs its own secrets, and those secrets should be different.
For example:
The pattern for handling this depends on where your application runs.
To make variables persist across terminal sessions, add export statements to your shell's configuration file. For bash (the default on many systems), that's ~/.bashrc or ~/.bash_profile. For zsh (the default on modern macOS), it's ~/.zshrc.
Open the file with a text editor and add:
export OPENAI_API_KEY="sk-proj-a8f2k3mxp9yourkeyhere"
Save the file and run source ~/.zshrc (or the appropriate file) to apply the changes immediately without restarting your terminal. Now this variable will be available in every new terminal session automatically.
When your application lives on a server or runs in a cloud environment, you don't use .env files. Instead, you configure environment variables through the platform's interface.
Heroku: Navigate to your app's dashboard, click on the Settings tab, then find the "Config Vars" section. Click "Reveal Config Vars," then click "Add" to enter your key name and value. No terminal command needed.
AWS (using Lambda or EC2): In the Lambda console, open your function, scroll to the "Configuration" tab, and select "Environment variables." Click "Edit" to add key-value pairs. For EC2, you typically set variables in the systemd service file or in a secrets manager.
Railway, Render, or similar platforms: These modern deployment platforms all have a "Variables" or "Environment" section in your project's settings where you add key-value pairs through a web UI.
The principle is the same everywhere: the platform stores the secrets and injects them as environment variables when your application starts. Your code doesn't change.
Environment variables and .env files work well for individual developers and small teams. As your applications grow more complex — or as your organization scales — you may want more sophisticated tooling.
HashiCorp Vault is an open-source tool designed specifically for storing and controlling access to secrets. It encrypts secrets at rest, maintains detailed audit logs of who accessed what and when, supports automatic secret rotation (changing keys on a schedule), and integrates with many deployment platforms.
AWS Secrets Manager and Azure Key Vault are cloud-managed equivalents. If your application already runs on AWS or Azure, these services let you store secrets centrally and retrieve them programmatically. Your code fetches the secret at runtime using an SDK call rather than reading an environment variable, which means the secret never has to be stored on disk at all.
1Password Secrets Automation and Doppler are secret management platforms designed to make the developer experience smooth. Doppler, for example, can inject secrets into your local development environment automatically and sync them across your team — similar to .env files, but with centralized control and audit trails.
For most projects at the foundation level, environment variables with .env files are the right tool. Understanding that these more advanced tools exist — and why they exist — helps you recognize when you've outgrown the simpler approach.
Let's put this all together with a realistic exercise. You'll set up a properly secured LLM integration from scratch.
Step 1: Create a project directory
mkdir llm-secure-demo
cd llm-secure-demo
Step 2: Initialize a Git repository
git init
Step 3: Create your .gitignore file
Create a file named .gitignore and add the following content:
.env
__pycache__/
*.pyc
.DS_Store
Step 4: Create your .env.example template
Create a file named .env.example:
# Copy to .env and fill in your actual values
OPENAI_API_KEY=your_openai_api_key_here
APP_ENVIRONMENT=development
Step 5: Create your actual .env file
Create a file named .env and fill in your real API key:
OPENAI_API_KEY=sk-proj-youractualkey
APP_ENVIRONMENT=development
Step 6: Install dependencies
pip install openai python-dotenv
Step 7: Write the application
Create a file named main.py:
from dotenv import load_dotenv
import os
import openai
def load_config():
"""Load configuration from environment variables."""
load_dotenv()
api_key = os.environ.get("OPENAI_API_KEY")
environment = os.environ.get("APP_ENVIRONMENT", "development")
if not api_key:
raise ValueError(
"OPENAI_API_KEY is not set. "
"Copy .env.example to .env and add your API key."
)
return api_key, environment
def summarize_text(client, text):
"""Summarize a block of text using the LLM."""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "You are a concise summarizer. Summarize in 2-3 sentences."
},
{
"role": "user",
"content": f"Summarize the following:\n\n{text}"
}
]
)
return response.choices[0].message.content
def main():
api_key, environment = load_config()
print(f"Running in {environment} environment")
client = openai.OpenAI(api_key=api_key)
sample_text = """
The transformer architecture, introduced in the 2017 paper "Attention Is All You Need,"
fundamentally changed natural language processing. By replacing recurrent layers with
self-attention mechanisms, transformers can process entire sequences in parallel rather
than step-by-step, making them dramatically faster to train. This architectural shift
enabled the development of large language models like GPT and BERT, which power modern
AI assistants and language tools.
"""
summary = summarize_text(client, sample_text)
print(f"Summary: {summary}")
if __name__ == "__main__":
main()
Step 8: Verify your .gitignore is working
git status
You should see .env.example and main.py listed as untracked files. The .env file should not appear anywhere in the output. If it does, stop — review Step 3 and resolve it before proceeding.
Step 9: Make your first commit
git add .gitignore .env.example main.py
git commit -m "Initial commit: secure LLM integration setup"
Congratulations — you've built a properly structured, secrets-safe LLM application.
"My environment variable works in the terminal but not when I run my script."
This usually means the variable was set in one terminal session and you're running the script from a different one. Either set the variable in the current terminal, use a .env file with python-dotenv, or add the export to your shell configuration file.
"I added .env to .gitignore but git status still shows it."
The file was probably already tracked by Git before you added the ignore rule. Files that are already tracked aren't automatically untracked just because you add them to .gitignore. Run git rm --cached .env to remove it from tracking, then commit that change.
"My colleague cloned the repo and the app crashes immediately."
They don't have a .env file. This is exactly why you create .env.example — point them to that file and have them create their own .env with their credentials. Your ValueError with a helpful message makes this obvious.
"I accidentally committed my API key. What do I do?"
First, immediately rotate the key: go to your API provider's dashboard and revoke the exposed key, then generate a new one. Don't wait. Even if you delete the key in the next commit, it's in Git's history. After rotating, use git-filter-repo to scrub the history, and force-push the cleaned history. If the repository is public, assume the key was seen and used, regardless of how quickly you acted.
"I'm not sure if my .env file is being loaded."
Add a temporary debug line right after load_dotenv():
load_dotenv()
print("API key present:", bool(os.environ.get("OPENAI_API_KEY")))
This tells you whether the key is in the environment without actually printing the key itself. Remove the debug line before sharing your code.
You now have a solid, practical foundation for handling API keys and secrets in LLM applications. Here's what we covered:
.env files with python-dotenv make local development convenient while keeping secrets out of Git.gitignore is non-negotiable — always add .env before your first commit, not after.env.example bridges the gap between keeping secrets private and helping teammates set up quicklyWith your secrets management foundation in place, you're ready to build more confidently. Natural next topics in the Building with LLMs path include:
Every project you build from here benefits from the habits you've established in this lesson. Start right, and you'll never have to clean up the mess of a compromised key.
Learning Path: Building with LLMs