Manage API keys and settings¶
Beginner · 10 min · tested with python-dotenv 1.2 and pydantic-settings 2.16
Every GenAI project starts with an API key, and the most common beginner mistake is pasting it into the code. Then it ends up on GitHub, bots find it within minutes, and someone else runs up your bill. This tutorial sets up the pattern professionals use: keys live in the environment, settings are typed, and the app refuses to start if something is missing.
1. Put keys in a .env file¶
Then, before anything else, make sure Git can never pick it up:
Read the values with load_dotenv():
import os
from dotenv import load_dotenv
load_dotenv() # copies .env values into environment variables
print(os.getenv("MODEL")) # → gpt-4o-mini
print(os.getenv("TEMPERATURE")) # → 0.2 (a string, not a float!)
print(os.getenv("MISSING_KEY")) # → None (no error: easy to miss)
key = os.getenv("OPENAI_API_KEY")
print(f"key loaded: {key[:7]}...{key[-4:]}") # → key loaded: sk-proj...i789
Two things to notice: everything comes back as a string, and a missing key silently becomes None, so the
error only shows up later as a confusing "authentication failed". Section 3 fixes both.
load_dotenv() doesn't overwrite variables that are already set. That's what you want: in production the real
environment wins, and the .env file is only for your laptop.
2. Commit a .env.example instead¶
Teammates (and future you) need to know which variables exist. Commit a template with the names and safe defaults, but no secrets:
# Copy to .env and fill in. Never commit .env.
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
MODEL=gpt-4o-mini
TEMPERATURE=0.2
DEBUG=false
New setup becomes cp .env.example .env, then fill in the keys.
3. Typed, validated settings with pydantic-settings¶
For anything bigger than a script, define your settings once as a class. You get type conversion, defaults, validation and hidden secrets for free:
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
openai_api_key: SecretStr # required: no default
model: str = "gpt-4o-mini"
temperature: float = Field(0.3, ge=0, le=2) # converted from "0.2", range-checked
max_tokens: int = 1000
debug: bool = False # "true", "1", "yes" all become True
settings = Settings()
from config import settings
print(settings.model, settings.temperature, settings.debug) # → gpt-4o-mini 0.2 True
print(settings.openai_api_key) # → **********
print(repr(settings))
# → Settings(openai_api_key=SecretStr('**********'), model='gpt-4o-mini', temperature=0.2, max_tokens=1000, debug=True)
key = settings.openai_api_key.get_secret_value() # the real value, only where you need it
SecretStr means the key never appears in logs, tracebacks or print statements by accident. Environment
variable names match the field names, case-insensitively (OPENAI_API_KEY fills openai_api_key).
And if the key is missing or a value is wrong, the app stops at startup with a clear message instead of failing on the first user request:
2 validation errors for Settings
openai_api_key
Field required [type=missing, input_value={'temperature': '5'}, input_type=dict]
temperature
Input should be less than or equal to 2 [type=less_than_equal, input_value='5', input_type=str]
4. Use the settings with an LLM client¶
from openai import OpenAI
from config import settings
client = OpenAI(api_key=settings.openai_api_key.get_secret_value())
reply = client.chat.completions.create(
model=settings.model,
temperature=settings.temperature,
max_tokens=settings.max_tokens,
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(reply.choices[0].message.content)
Now switching models or temperature is a change to .env (or the server's environment), not to the code.
5. Where keys live outside your laptop¶
| Where the app runs | Put keys in | Read them with |
|---|---|---|
| Your laptop | .env (gitignored) |
load_dotenv() or pydantic-settings |
| Streamlit Community Cloud | App settings → Secrets (TOML) | st.secrets["OPENAI_API_KEY"] (also exposed as env vars) |
| Docker | docker run --env-file .env ... (never COPY .env into the image) |
Environment variables |
| Railway, Render, Fly.io | The service's Variables / Environment page | Environment variables |
| GitHub Actions | Settings → Secrets and variables → Actions | env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
| Hugging Face Spaces | Space settings → Variables and secrets | Environment variables |
Because every platform ends up setting environment variables, the same Settings class works everywhere
without changes.
6. If a key leaks¶
- Revoke it immediately in the provider's dashboard and create a new one. This is the step that matters.
- Remove it from the code and load it from the environment.
- If it reached GitHub, it's in the history: see When a secret gets committed.
- Check the provider's usage page for spending you don't recognise.
Set a monthly spending limit on every provider account so a leaked key can only cost so much.
Checklist¶
-
.envis in.gitignore, and.env.exampleis committed. - No key appears anywhere in the code (search for
sk-before every push). - Settings are a typed class; the app fails at startup if a required key is missing.
- Keys are
SecretStr, so they never get logged. - Each environment (dev, CI, production) has its own key, with a spending limit.
Next: Stream LLM responses →