API Keys & .env
The moment you call a real model, you need an API key. How you handle it is the difference between a working project and a story about a bill.
This page is short and every line of it matters.
What an API key is
A long string that identifies you to a paid service. Anyone holding it can spend your money.
sk-proj-8f3a91c04b7e4d2fa6c15e08b39d7a42Treat it exactly like a password, with one extra worry: passwords are usually typed, whereas keys get pasted into code and then travel wherever that code goes.
Never put a key in your code
api_key = "sk-proj-8f3a91c04b7e4d2fa6c15e08b39d7a42"Do not do this, not even briefly, not even in a notebook you intend to delete.
Once a key is in a file it ends up in your git history, and removing it later does not remove it from history. Bots scan public repositories for key patterns within minutes of a push. People have woken up to five-figure bills from a key committed the night before.
The same applies to notebooks. A Colab notebook you share carries its cell contents with it.
Put it in the environment instead
The standard approach: keep keys in a file that never leaves your machine, and read them at runtime.
Create a file called .env beside your code:
DEEPSEEK_API_KEY=sk-your-real-key-here
OPENAI_API_KEY=sk-your-other-key
LANGSMITH_API_KEY=ls-somethingRead it in Python:
import os
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("DEEPSEEK_API_KEY")Install the helper once:
pip install python-dotenvload_dotenv() reads the file and puts each line into the environment. os.getenv then fetches it. Your code contains the name of the key, never the key itself.
The line that actually protects you
Add .env to .gitignore:
.env
.venv/
__pycache__/Without this the whole exercise is pointless — you have moved the key to a different file in the same repository.
Check it is working before your first commit:
git statusIf .env appears in that list, it is not ignored yet. Fix it before you commit.
Fail loudly when a key is missing
api_key = os.getenv("DEEPSEEK_API_KEY")
if not api_key:
raise RuntimeError("DEEPSEEK_API_KEY is not set. Add it to your .env file.")Without this check, api_key is None, the request goes out unauthenticated, and you get a confusing 401 from the provider instead of a clear message about your own setup. Five lines that save an hour.
Commit an example, not the real thing
Keep a .env.example in the repository with the names and no values:
DEEPSEEK_API_KEY=
OPENAI_API_KEY=Anyone cloning the project copies it to .env and fills in their own. It documents what the project needs without leaking anything.
In Colab
There is no .env file. Colab has a secrets panel — the key icon in the left sidebar.
from google.colab import userdata
api_key = userdata.get("DEEPSEEK_API_KEY")Secrets are stored against your Google account, not inside the notebook, so sharing the notebook does not share the key.
If you ever type a key directly into a Colab cell, treat that key as compromised and rotate it. The cell output and the notebook file both retain it.
In production
.env files are for your own machine. Deployed applications get their environment from the platform:
- Vercel — Environment Variables in project settings
- Docker —
-eflags, or an env file passed at run time - GitHub Actions — repository secrets
- AWS, Azure, GCP — their secret managers
Your Python code does not change. It still reads os.getenv("DEEPSEEK_API_KEY"). Only the source of the value differs, which is the whole point of using the environment.
Other things that belong in the environment
Not only keys. Anything that differs between your laptop and production:
DEEPSEEK_API_KEY=sk-...
MODEL_NAME=deepseek-chat
MAX_ITERATIONS=8
LOG_LEVEL=INFO
DATABASE_URL=postgresql://...Note MAX_ITERATIONS. Agents loop, and a loop with no ceiling is an open tab at the model provider. Making the cap configurable means you can tighten it in production without a code change.
max_iterations = int(os.getenv("MAX_ITERATIONS", "8"))os.getenv always returns text, so convert it. The second argument is the default when the variable is absent.
Keep the reading in one place
Scattering os.getenv through a codebase means you discover a missing variable halfway through a run. Read everything once, at startup:
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
api_key = os.getenv("DEEPSEEK_API_KEY")
model = os.getenv("MODEL_NAME", "deepseek-chat")
max_iterations = int(os.getenv("MAX_ITERATIONS", "8"))
@classmethod
def check(cls):
if not cls.api_key:
raise RuntimeError("DEEPSEEK_API_KEY is not set")
Config.check()Now a misconfigured deployment fails in the first second with a clear message, rather than twenty minutes in.
If you leak a key
It happens. Act quickly and it costs nothing.
- Rotate it immediately in the provider's dashboard. This invalidates the old one and is the only step that genuinely matters.
- Then remove it from the code and commit the fix.
- Do not rely on deleting the commit. Assume anything pushed to a public repository was captured.
- Check the provider's usage page for spending you did not do.
Rotating first is the point. Cleaning up the repository while the key is still live is the wrong order.
Coming from Java or C#
| Concept | Java | C# | Python |
|---|---|---|---|
| Local config | application.properties | appsettings.json, user secrets | .env |
| Reading it | @Value, Environment | IConfiguration | os.getenv |
| Secret store | Vault, AWS Secrets Manager | Key Vault | same services |
| Typed config | @ConfigurationProperties | Options pattern | a Config class, or Pydantic Settings |
Two differences worth noting. Python has no built-in configuration framework, so .env plus os.getenv is the community default rather than a language feature. And os.getenv returns text always — there is no automatic binding to an int or a bool, so convert explicitly. pydantic-settings gives you the typed, validated version if you want it.
Common mistakes
- A key pasted into code "just for a moment".
.envnot in.gitignore.- No check for a missing key, producing a confusing 401.
- Forgetting
os.getenvreturns text, soMAX_ITERATIONSbecomes the string"8"and comparisons behave oddly. - A key typed into a Colab cell and the notebook then shared.
- Cleaning the repository before rotating the key. Rotate first.
Practise this
- Create a
.env, add one variable, read it withos.getenv, and print it. - Add
.envto.gitignoreand confirm withgit statusthat it is ignored. - Write the missing-key check and trigger it by renaming the variable.
- Read a number from the environment with a default and confirm you converted it.