Python Configuration Management

Ma Mahalakshmi V Updated 19 Sep 2026
6 min read

Configuration Management with Python Settings

Every service class built so far in this unit takes a model parameter with a hardcoded default like "gpt-5.6-terra", and every real client reads OPENAI_API_KEY implicitly from the environment. This works for a single script. It breaks down once an application needs different settings for local development, automated tests, staging, and production — different API keys, different default models, different timeout values, different feature flags. Configuration management is the discipline of collecting these values in one well-defined place instead of scattering os.environ.get(...) calls and hardcoded literals throughout the codebase.

Why Scattered Configuration Is a Problem

Consider code without centralized configuration:

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
DEFAULT_MODEL = "gpt-5.6-terra"
REQUEST_TIMEOUT = 30

If these three lines are duplicated — even slightly differently — across five files, changing the timeout means finding and editing five places, and it is easy to miss one. Worse, os.environ["OPENAI_API_KEY"] raises a raw KeyError with no helpful message if the variable is missing, and there is no single place that documents every configuration value the application actually needs.

A Simple Settings Object

The most basic fix is a single class or dataclass that gathers every configuration value in one place, with typed fields and validation:

import os
from dataclasses import dataclass


@dataclass(frozen=True)
class Settings:
    openai_api_key: str
    default_model: str = "gpt-5.6-terra"
    request_timeout_seconds: float = 30.0

    @classmethod
    def from_env(cls) -> "Settings":
        api_key = os.environ.get("OPENAI_API_KEY")
        if not api_key:
            raise RuntimeError(
                "OPENAI_API_KEY is not set. Set it in your environment or .env file."
            )
        return cls(
            openai_api_key=api_key,
            default_model=os.environ.get("DEFAULT_MODEL", "gpt-5.6-terra"),
            request_timeout_seconds=float(os.environ.get("REQUEST_TIMEOUT_SECONDS", "30")),
        )

Now, application startup calls Settings.from_env() exactly once, and every part of the application that needs configuration receives a Settings instance (via dependency injection, as covered in Lesson 2) instead of reading environment variables directly:

def build_client(settings: Settings) -> OpenAI:
    return OpenAI(api_key=settings.openai_api_key, timeout=settings.request_timeout_seconds)

This is already a large improvement: there is one clear error message if the API key is missing, one place listing every configuration value the application uses, and no repeated os.environ calls scattered through business logic.

Why Use pydantic-settings Instead of Hand-Rolled Parsing

The hand-written Settings.from_env() above works, but it re-implements, by hand, several things a dedicated library already does well: type coercion (turning the string "30" into the float 30.0), validation error messages, support for .env files, and nested configuration. The pydantic-settings package (a companion to Pydantic) provides all of this:

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

    openai_api_key: str = Field(alias="OPENAI_API_KEY")
    default_model: str = "gpt-5.6-terra"
    request_timeout_seconds: float = 30.0


settings = Settings()

Note: pydantic-settings is a separate package from core Pydantic (pip install pydantic-settings), and its configuration API (SettingsConfigDict, field aliasing, .env loading behavior) has changed between major versions — check the installed version's documentation for exact field names and defaults.

Here, Settings() automatically reads matching environment variables (and, if present, a .env file) and coerces each one to its declared type, raising a clear pydantic.ValidationError listing every missing or invalid field if construction fails — rather than failing on the first KeyError encountered, which might hide a second, unrelated missing variable.

The .env File Pattern

For local development, environment variables are conventionally stored in a .env file at the project root rather than being exported manually in a shell session:

OPENAI_API_KEY=sk-your-development-key-here
DEFAULT_MODEL=gpt-5.6-terra
REQUEST_TIMEOUT_SECONDS=45

pydantic-settings (via the env_file option shown above) or the standalone python-dotenv package can load this file automatically at startup. The .env file itself must never be committed to version control — a .gitignore entry for .env is essential, since these files typically hold real API keys.

# .gitignore
.env

A companion file, .env.example, is committed instead, listing every variable name the application needs with placeholder or empty values, so a new developer knows exactly what to fill in without ever seeing a real secret:

OPENAI_API_KEY=
DEFAULT_MODEL=gpt-5.6-terra
REQUEST_TIMEOUT_SECONDS=30

Environment-Specific Configuration

Different environments (local development, CI test runs, staging, production) often need different defaults — a cheaper or faster model for local iteration, stricter timeouts in production. One common pattern is an environment field that selects among preset defaults:

from enum import Enum

from pydantic_settings import BaseSettings


class Environment(str, Enum):
    DEVELOPMENT = "development"
    TESTING = "testing"
    PRODUCTION = "production"


class Settings(BaseSettings):
    environment: Environment = Environment.DEVELOPMENT
    openai_api_key: str = ""
    default_model: str = "gpt-5.6-terra"

    @property
    def request_timeout_seconds(self) -> float:
        return 10.0 if self.environment == Environment.TESTING else 30.0

This keeps environment-dependent logic declarative and in one place, rather than as if os.environ.get("ENV") == "production": checks spread across the codebase.

Testing Code That Depends on Settings

Because a well-designed application receives a Settings object via dependency injection rather than reading the environment directly inside business logic, tests can construct a Settings instance with fixed values, with no environment variables or .env file involved at all:

def test_summarizer_uses_configured_default_model() -> None:
    settings = Settings(openai_api_key="fake-key-for-test", default_model="gpt-5.6-terra")

    assert settings.default_model == "gpt-5.6-terra"
    assert settings.openai_api_key == "fake-key-for-test"
    print("PASS: Settings exposes the configured model and key without reading the environment")


test_summarizer_uses_configured_default_model()

This test demonstrates the same principle from Lesson 2 applied to configuration: because Settings is just a plain object constructed with explicit values, no test needs a real .env file or real environment variables to verify behavior that depends on configuration.

Common Mistakes

Reading os.environ directly inside business logic. Scattering os.environ.get("DEFAULT_MODEL") throughout service classes makes it impossible to know, from one place, every configuration value the application depends on, and makes those classes harder to test without manipulating real environment variables.

Committing .env to version control. This leaks API keys and other secrets into git history, which is difficult to fully remove even after deleting the file in a later commit. Always .gitignore it and commit only .env.example.

Failing silently on missing configuration. A default like api_key: str = "" that is never validated lets the application start successfully and fail confusingly later, on the first real API call, instead of failing immediately and clearly at startup.

Best Practices

Centralize all configuration in one Settings object, constructed once at startup. Every other class receives configuration values (or the whole Settings object) via constructor injection, never by reading the environment itself.

Fail fast and clearly on missing required configuration. Validate required fields (like an API key) at construction time, with an error message that says exactly which variable is missing and how to set it.

Keep secrets out of defaults and out of source code. Required secrets should have no default value in code — they must come from the environment, a .env file (development only), or a secrets manager (production) — never a hardcoded fallback string.

Document every configuration variable in .env.example. This file acts as living documentation of what the application needs to run, and should be updated in the same commit that introduces a new setting.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.out Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Mem Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources
Ask about this post
AI Ask about this post

Ask questions about Python Configuration Management and get answers drawn from it.

Signed-in readers only.