4. Settings & secrets¶
Intermediate · 7 min read
Every GenAI app needs config: API keys, model names, a vector DB URL, timeouts. pydantic-settings
reads them from environment variables and .env files, converts the types, and stops the app at
startup if something is missing — instead of failing on the first user request.
4.1 A settings class¶
import os
from pydantic import SecretStr
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
openai_api_key: SecretStr # required — no default
llm_model: str = "gpt-4o-mini"
temperature: float = 0.2
request_timeout_s: int = 30
debug: bool = False
os.environ["OPENAI_API_KEY"] = "sk-test-123" # normally set in your shell or .env
os.environ["DEBUG"] = "true"
settings = Settings()
print(settings.llm_model, settings.debug, type(settings.debug).__name__)
Field names match environment variables case-insensitively: openai_api_key reads OPENAI_API_KEY.
Values arrive as strings and are converted like any Pydantic field ("true" → True).
4.2 SecretStr — keys that don't leak¶
print(settings.openai_api_key)
print(settings)
print(settings.openai_api_key.get_secret_value()) # only where you actually need it
**********
openai_api_key=SecretStr('**********') llm_model='gpt-4o-mini' temperature=0.2 request_timeout_s=30 debug=True
sk-test-123
Logs are where keys leak
A stray print(settings) or an error report that dumps config is the most common way API keys end
up in log files. SecretStr makes that harmless.
4.3 Reading a .env file¶
from pydantic_settings import SettingsConfigDict
with open(".env", "w") as f:
f.write("APP_LLM_MODEL=claude-sonnet-5-5\nAPP_ANTHROPIC_API_KEY=sk-ant-xyz\n")
class AppSettings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_", extra="ignore")
anthropic_api_key: SecretStr
llm_model: str = "gpt-4o-mini"
top_k: int = 5
s = AppSettings()
print(s.llm_model, s.top_k)
env_prefix="APP_"— only readAPP_*variables, so different apps don't collide.- A real environment variable wins over the
.envfile — that's how you override a value in production. - Add
.envto.gitignore; commit a.env.examplewith empty values instead.
4.4 Fail fast when something is missing¶
from pydantic import ValidationError
class NeedsKey(BaseSettings):
model_config = SettingsConfigDict(env_prefix="MYAPP_")
pinecone_api_key: SecretStr
try:
NeedsKey()
except ValidationError as e:
print(e.errors()[0]["loc"], e.errors()[0]["msg"])
4.5 Nested settings¶
Group related values; set them with a delimiter such as __:
from pydantic import BaseModel
class VectorDB(BaseModel):
url: str = "http://localhost:6333"
collection: str = "docs"
class RAGSettings(BaseSettings):
model_config = SettingsConfigDict(env_nested_delimiter="__")
vector_db: VectorDB = VectorDB()
os.environ["VECTOR_DB__COLLECTION"] = "support_kb"
print(RAGSettings().vector_db)
4.6 One settings object for the whole app¶
Create it once and import it everywhere. lru_cache makes get_settings() return the same object, and
lets tests swap it out:
# no-run — app/config.py
from functools import lru_cache
@lru_cache
def get_settings() -> AppSettings:
return AppSettings()
# anywhere else
from app.config import get_settings
client = OpenAI(api_key=get_settings().openai_api_key.get_secret_value())
FastAPI uses exactly this pattern with Depends(get_settings) — see Dependencies.
Practice¶
- Write a
Settingsclass for your RAG app: LLM key, embedding model, chunk size (int, default 800), vector DB URL. - Create a
.env.examplefile listing every variable with no values.
Next: Pydantic for GenAI — structured LLM output and tool schemas.