Python Configuration Management

Ma Mahalakshmi V Updated 19 Sep 2026
6 min read ·Lesson 203 of 224

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.output_text 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 Memorizing Names 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 Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
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.