Building AI Agents

API Keys & .env

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-8f3a91c04b7e4d2fa6c15e08b39d7a42

Treat 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-something

Read 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-dotenv

load_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 status

If .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-e flags, 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#

ConceptJavaC#Python
Local configapplication.propertiesappsettings.json, user secrets.env
Reading it@Value, EnvironmentIConfigurationos.getenv
Secret storeVault, AWS Secrets ManagerKey Vaultsame services
Typed config@ConfigurationPropertiesOptions patterna 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".
  • .env not in .gitignore.
  • No check for a missing key, producing a confusing 401.
  • Forgetting os.getenv returns text, so MAX_ITERATIONS becomes 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 with os.getenv, and print it.
  • Add .env to .gitignore and confirm with git status that 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.

Try It Yourself

Copy this into a new Colab notebook and run it. Nothing to install.

example.py
1# Keys live in the environment, never in your code.
2# Needs: pip install python-dotenv
3
4import os
5
6# In a real project:
7# from dotenv import load_dotenv
8# load_dotenv() # reads a .env file beside your code
9#
10# .env contains:
11# DEEPSEEK_API_KEY=sk-your-real-key
12# MODEL_NAME=deepseek-chat
13# MAX_ITERATIONS=8
14#
15# .gitignore MUST contain `.env` - that is the line that actually protects you.
16
17# For this example, set a few variables so the code below runs anywhere:
18os.environ.setdefault("DEEPSEEK_API_KEY", "sk-demo-not-a-real-key")
19os.environ.setdefault("MAX_ITERATIONS", "8")
20
21
22# --- Reading a value ---
23api_key = os.getenv("DEEPSEEK_API_KEY")
24print("key loaded:", bool(api_key))
25
26# --- Fail loudly when it is missing ---
27if not api_key:
28 raise RuntimeError("DEEPSEEK_API_KEY is not set. Add it to your .env file.")
29
30# --- os.getenv ALWAYS returns text. Convert it. ---
31raw = os.getenv("MAX_ITERATIONS", "8")
32print("raw value :", repr(raw), type(raw))
33max_iterations = int(raw)
34print("converted :", max_iterations, type(max_iterations))
35
36# A missing variable returns the default you give:
37print("absent :", os.getenv("NOT_SET_ANYWHERE", "fallback value"))
38print()
39
40
41# --- Read everything once, at startup ---
42class Config:
43 api_key = os.getenv("DEEPSEEK_API_KEY")
44 model = os.getenv("MODEL_NAME", "deepseek-chat")
45 max_iterations = int(os.getenv("MAX_ITERATIONS", "8"))
46 log_level = os.getenv("LOG_LEVEL", "INFO")
47
48 @classmethod
49 def check(cls):
50 missing = [n for n in ("api_key",) if not getattr(cls, n)]
51 if missing:
52 raise RuntimeError(f"missing configuration: {missing}")
53 return True
54
55
56Config.check()
57print("model :", Config.model)
58print("max_iterations:", Config.max_iterations)
59print("log_level :", Config.log_level)
60print()
61
62# A misconfigured deployment now fails in the first second,
63# with a clear message, instead of twenty minutes into a run.
64
65
66# --- Never print or log the whole key ---
67def masked(secret: str) -> str:
68 return f"***{secret[-4:]}" if secret else "not set"
69
70print("safe to log:", masked(Config.api_key))
71
72# In Colab there is no .env - use the key icon in the sidebar:
73# from google.colab import userdata
74# api_key = userdata.get("DEEPSEEK_API_KEY")