Skip to content

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.

pip install pydantic-settings

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__)
Output
gpt-4o-mini True bool

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
Output
**********
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)
Output
claude-sonnet-5-5 5
  • env_prefix="APP_" — only read APP_* variables, so different apps don't collide.
  • A real environment variable wins over the .env file — that's how you override a value in production.
  • Add .env to .gitignore; commit a .env.example with 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"])
Output
('pinecone_api_key',) Field required

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)
Output
url='http://localhost:6333' collection='support_kb'

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 Settings class for your RAG app: LLM key, embedding model, chunk size (int, default 800), vector DB URL.
  • Create a .env.example file listing every variable with no values.

Next: Pydantic for GenAI — structured LLM output and tool schemas.