Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 48 of 898 min

API keys: out of the code, out of the repository

Never in the source

python
API_KEY = "sk-proj-8f2a..."      # no

A key written in a file gets committed, and once it is in git history it stays there — deleting the line in a later commit does not remove it from the repository, and anyone who ever cloned it has a copy. Public repositories are scanned continuously by automated crawlers; keys committed to a public repository have been observed being used within minutes.

The same applies to notebooks, which store output as well as code, and to screenshots pasted into chats.

Environment variables

python
import os

key = os.environ["OPENAI_API_KEY"]        # raises KeyError if unset
key = os.getenv("OPENAI_API_KEY")         # returns None if unset
key = os.getenv("MODEL", "small-v1")      # with a default

Set it in the shell:

bash
export OPENAI_API_KEY="sk-proj-..."       # macOS, Linux
setx OPENAI_API_KEY "sk-proj-..."         # Windows, persistent

For a required secret, prefer the bracket form or fail explicitly:

python
key = os.getenv("OPENAI_API_KEY")
if not key:
    raise RuntimeError("OPENAI_API_KEY is not set; see README")

A missing key that surfaces as None produces a confusing 401 from the API three functions later. A missing key that stops the program with a sentence naming the variable is answered in ten seconds.

Two shells that disagree

Environment variables are set per shell session, and where you set them decides who can see them. A variable exported in a login-shell profile such as ~/.zprofile is read by login shells only, so an interactive terminal has it and a script, a scheduled job or an editor's built-in terminal does not. The result is a program that works when you type the command and fails when anything else runs it, with no difference in the code. This exact failure has cost real projects weeks.

When a key is "definitely set" and the program disagrees, print what the program actually sees:

python
print("key present:", bool(os.getenv("OPENAI_API_KEY")))

Print the boolean, never the value. That one habit keeps keys out of your logs and out of the screenshot you are about to paste.

.env files

For development, a file of key-value pairs is more convenient than exporting by hand:

# .env
OPENAI_API_KEY=sk-proj-...
DATABASE_URL=postgres://localhost/dev
python
from dotenv import load_dotenv     # pip install python-dotenv
load_dotenv()
key = os.environ["OPENAI_API_KEY"]

load_dotenv() reads the file into the process environment. It does not overwrite variables already set, so a real value in the environment beats the file — which is what you want when the same code runs in production.

Add .env to .gitignore before you create it. Commit a .env.example with the names and no values, so a new contributor knows what to set:

OPENAI_API_KEY=
DATABASE_URL=

In a notebook

Colab has a secrets panel: the key icon in the sidebar, then

python
from google.colab import userdata
key = userdata.get("OPENAI_API_KEY")

The value is stored against your account rather than in the notebook, so sharing the notebook does not share the key. In Jupyter, load_dotenv() works the same as anywhere else. What you must not do is type the key into a cell — the notebook file records it, and notebooks get emailed.

What to do when a key leaks

Assume the worst and act in this order:

  1. Revoke the key in the provider's dashboard. Immediately, before anything else. A revoked key is worthless to whoever has it.
  2. Issue a new one and update wherever it is configured.
  3. Check the usage or billing page for calls you did not make.
  4. Only then worry about cleaning history.

Rewriting git history with git filter-repo or BFG removes the string from the repository, and it does not undo the exposure — anyone watching already has it. Revocation is the fix; history cleaning is tidying afterwards.

A key committed to a public repositoryTue 14:20A key is pasted into config.py to get one script working, and committed witheverything else.14:35The branch is pushed. The key is now in the history, not only in the file.MinuteslaterScanners that watch the public commit feed find the string. This is automated andcontinuous, not bad luck.That eveningUsage rises on the account while nobody is running anything.FridayA bill, or a suspended account. Deleting the line in a later commit changednothing.The fixRevoke, reissue, check usage, then clean history — in that order.Rewriting history with git filter-repo removes the string from the repository and does not undo any ofthis. Revoke first; it is the only step that stops the clock. A scoped key and a spend limit setbeforehand decide what the rest of the week costs.
A key committed to a public repositoryTue 14:20A key is pasted into config.py to get onescript working, and committed with everythingelse.14:35The branch is pushed. The key is now in thehistory, not only in the file.Minutes laterScanners that watch the public commit feed findthe string. This is automated and continuous,not bad luck.That eveningUsage rises on the account while nobody isrunning anything.FridayA bill, or a suspended account. Deleting theline in a later commit changed nothing.The fixRevoke, reissue, check usage, then cleanhistory — in that order.Rewriting history with git filter-repo removes thestring from the repository and does not undo any ofthis. Revoke first; it is the only step that stopsthe clock. A scoped key and a spend limit setbeforehand decide what the rest of the week costs.

Reduce what a leak can cost

  • Give each key the narrowest scope the provider offers, and one key per application, so you can revoke one without stopping everything.
  • Set a spend limit on the account. Most model APIs support a hard monthly cap, and this is the difference between an embarrassing evening and a five-figure bill.
  • Never put a key in a URL or query string. URLs are logged by proxies, browsers and servers. Keys belong in a request header.
  • Rotate on a schedule, and whenever somebody with access leaves.

Configuration is not only secrets

The same mechanism carries anything that differs between machines: database URLs, model names, feature flags, output folders. Keeping them in the environment rather than in the code means the same artefact runs in development and production, which is the point of the convention. Keep the reading of them in one small module, so there is a single list of everything the program expects to be told.

The one thing to keep

Keys live in the environment, never in the source or a notebook, and when one leaks the fix is revocation first — cleaning git history afterwards does not undo the exposure.

Before you move on

A developer commits a key by accident, notices within ten minutes, deletes the line, and pushes a new commit saying "remove key". Why is that insufficient?

Pick the one you would defend. Nobody sees your answer.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly