Applications need configuration values such as database connection strings, API keys, service endpoints, feature flags, and logging levels. During local development, these values may differ from those used in testing or production. Keeping configuration separate from application code makes it easier to deploy the same application across multiple environments.

However, configuration management introduces security risks. Hardcoding credentials in Python files can expose them through source control. Loading environment variables without validation can cause unexpected failures. Storing secrets in configuration files that are accidentally committed to a repository can also compromise an application.

Pydantic Settings helps solve these problems by loading configuration from supported sources, validating values, and making them available through a structured Python model.

This article explains how to configure Pydantic Settings, load environment variables, manage secrets, validate configuration, and avoid common mistakes in production applications.

What Is Pydantic Settings?

Pydantic Settings is a package that extends Pydantic's validation capabilities to application configuration.

Instead of accessing environment variables individually throughout an application, you can define a settings class containing the expected configuration fields.

For example, consider an application that needs a database URL, an API key, and a debug flag. Without a settings model, developers might repeatedly call os.getenv() and manually convert values.

Python
import os

database_url = os.getenv("DATABASE_URL")
api_key = os.getenv("API_KEY")
debug = os.getenv("DEBUG", "false").lower() == "true"

This approach can work for small scripts, but it becomes difficult to maintain as configuration grows. It also requires developers to handle missing values and type conversion consistently.

Pydantic Settings provides a more structured alternative.

Install the required packages:

Bash
pip install pydantic-settings

The examples in this article use the pydantic-settings package and its BaseSettings class. Configuration options can differ between package versions, so check the version installed in your project when adopting advanced settings.

Creating a Settings Model

Start by defining a settings class that describes the application's configuration.

Python
from pydantic import SecretStr, Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    database_url: SecretStr
    api_key: SecretStr
    debug: bool = False
    request_timeout: float = Field(default=10.0, gt=0)

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore"
    )

This model defines four configuration fields.

  • database_url stores the database connection string.

  • api_key stores an API credential.

  • debug determines whether debugging behavior is enabled.

  • request_timeout defines a positive timeout value.

The SecretStr type helps prevent sensitive values from appearing accidentally when the model is displayed or converted to a string. It does not encrypt the underlying value or secure the source from which it was loaded.

The Field declaration validates that request_timeout is greater than zero. If an invalid value is provided, settings initialization raises a validation error rather than allowing the application to continue with an invalid configuration.

The SettingsConfigDict configuration tells Pydantic Settings to read a .env file and ignore unrelated entries in that file. Whether ignoring extra entries is appropriate depends on your project. In a strictly managed configuration environment, rejecting unexpected values may help detect misspelled configuration keys.

Loading Environment Variables

Pydantic Settings can load values from environment variables automatically.

For the default field names, the following environment variables can populate the settings model:

Bash
export DATABASE_URL="postgresql://app_user:password@localhost:5432/app_db"
export API_KEY="replace-with-a-real-secret"
export DEBUG="false"
export REQUEST_TIMEOUT="15"

Create a settings instance:

Python
settings = Settings()

print(settings.debug)
print(settings.request_timeout)

Pydantic Settings converts supported environment-variable values to the types declared in the model. For example, the string "15" can be converted to the floating-point value 15.0.

Sensitive values should not be printed directly:

Python
print(settings.api_key)

Because api_key is a SecretStr, its normal string representation masks the secret. When an API client needs the actual credential, retrieve it explicitly:

Python
api_key = settings.api_key.get_secret_value()

Only reveal the underlying value when it is required by a trusted component. Avoid including it in exception messages, logs, debugging output, or HTTP responses.

Using a .env File

During local development, a .env file can hold configuration values:

Code
DATABASE_URL=postgresql://app_user:password@localhost:5432/app_db
API_KEY=replace-with-a-real-secret
DEBUG=false
REQUEST_TIMEOUT=15

Pydantic Settings reads this file because the settings model specifies env_file=".env".

The application can then initialize the settings model without manually opening the file.

A .env file is a configuration source, not a secure vault. Add it to .gitignore if it contains real credentials:

Code
.env
.env.*
!.env.example

Keep an .env.example file containing placeholder values when it helps developers understand the required configuration. Never copy real production credentials into the example file.

Environment variables generally take precedence over values loaded from the .env file when both provide the same setting. Other configuration sources and custom source ordering can affect precedence, so verify the behavior of your installed version if your application customizes settings sources.

Managing Secrets Safely

Configuration values do not all have the same security requirements.

A debug flag is usually not a secret. An API key, database password, signing key, or private certificate may be sensitive and must be protected.

Use SecretStr for sensitive fields

Pydantic's SecretStr reduces the chance of accidental disclosure in ordinary model representations.

Python
from pydantic import SecretStr
from pydantic_settings import BaseSettings


class PaymentSettings(BaseSettings):
    payment_api_key: SecretStr

When the application needs to send an authenticated request, it can retrieve the underlying value:

Python
api_key = settings.payment_api_key.get_secret_value()

This type is a defensive measure, not a complete secret-management solution. Code that explicitly extracts the value can still log or expose it.

Use a secret manager in production

For production systems, consider a dedicated secret-management service or the secret-management facilities provided by your deployment platform.

Examples include cloud secret managers, container orchestration secrets, and managed deployment environments that inject secrets at runtime.

The application can still use Pydantic Settings to validate the values it receives. The important difference is that the secret's storage, access controls, and rotation are managed outside the application repository.

A typical configuration flow looks like this:

  1. Store the secret in an approved secret-management system.

  2. Grant access only to the application identity that needs it.

  3. Inject or retrieve the secret through the deployment environment.

  4. Validate the received value during application initialization.

  5. Rotate credentials according to the organization's security policy.

Avoid placing secrets in Docker image layers, source code, public build logs, or version-controlled deployment files.

Keep configuration separate from secrets

Not every setting requires the same protection. For example:

Configuration value

Typical handling

Application name

Ordinary configuration

Debug mode

Environment-specific configuration

Request timeout

Validated configuration

Database password

Secret-management system

External API key

Secret-management system

Private signing key

Restricted secret storage

This separation helps teams apply appropriate controls without making ordinary configuration unnecessarily difficult to manage.

Validating Configuration Before Startup

One major benefit of Pydantic Settings is that invalid configuration can be detected when the settings object is created.

Consider this model:

Python
from pydantic import Field
from pydantic_settings import BaseSettings


class AppSettings(BaseSettings):
    port: int = Field(default=8000, ge=1, le=65535)
    workers: int = Field(default=2, ge=1)
    request_timeout: float = Field(default=10.0, gt=0)

The model validates that the port falls within the valid TCP/UDP port-number range, the worker count is positive, and the timeout is greater than zero.

If an environment variable contains an invalid value, initialization fails with a validation error.

This behavior is useful because configuration problems are discovered before the application begins handling requests.

For example:

Bash
export PORT=99999

If the settings model uses port: int = Field(default=8000, ge=1, le=65535), the supplied value fails validation.

For a production service, this is usually preferable to starting the application with a value that cannot work correctly.

However, validation should reflect actual application requirements. Do not impose arbitrary limits without understanding how the application uses a setting.

Integrating Pydantic Settings With FastAPI

FastAPI applications commonly use Pydantic Settings to manage configuration for database access, external APIs, and runtime behavior.

Create a reusable settings module:

Python
from functools import lru_cache

from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    service_name: str = "Inventory API"
    database_url: SecretStr
    external_api_key: SecretStr
    debug: bool = False

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore"
    )


@lru_cache
def get_settings() -> Settings:
    return Settings()

The @lru_cache decorator caches the settings object after its first creation. Subsequent calls to get_settings() return the same instance rather than reloading configuration each time.

An endpoint or service can use the cached settings:

Python
from fastapi import Depends, FastAPI

app = FastAPI()


@app.get("/health")
def health_check(
    settings: Settings = Depends(get_settings)
):
    return {
        "service": settings.service_name,
        "status": "healthy",
        "debug": settings.debug
    }

In a real project, import Settings and get_settings from the module where they are defined. The endpoint deliberately does not return database URLs or API credentials.

Caching is useful when configuration should remain stable for the lifetime of the process. If configuration must change without restarting the application, a different refresh strategy is required. A cached settings object will not automatically reflect subsequent changes to environment variables.

Also, a health endpoint should report only the status it can actually verify. The example above reports application-level availability; it does not prove that the database or external services are reachable.

Handling Environment-Specific Configuration

Development, testing, staging, and production environments often need different values.

For example, a development environment might use a local database, while production uses a managed database service.

Pydantic Settings allows the same settings model to load values from different sources without requiring separate application implementations.

A development .env file might contain:

Code
DATABASE_URL=postgresql://localhost/inventory_dev
EXTERNAL_API_KEY=development-placeholder
DEBUG=true

Production can provide the corresponding environment variables through the deployment platform.

The application code remains the same. The configuration source changes according to the environment.

For production, debug mode should normally be disabled. Secrets should be supplied through an approved secret-management process rather than copied from a developer's local environment.

If you need different .env files for different environments, select the file explicitly through deployment configuration or application settings. Avoid silently loading several environment files without a clear precedence policy.

Common Mistakes to Avoid

Mistake

Why it is a problem

Recommended approach

Hardcoding API keys in Python files

Credentials may enter source control

Load secrets from an approved source

Committing a real .env file

Repository history may retain exposed credentials

Ignore local secret files and rotate exposed credentials

Assuming SecretStr encrypts secrets

The underlying value remains accessible to the application

Protect the source and restrict access

Printing the full settings object

Configuration may be disclosed through logs

Log only safe, explicitly selected fields

Using invalid default values

The application may fail or behave incorrectly

Validate values against application requirements

Caching settings when live updates are required

Cached values remain unchanged

Implement an explicit refresh or reload strategy

Returning credentials from an API endpoint

Clients may gain access to sensitive configuration

Return only non-sensitive operational information

Best Practices for Production Applications

Validate required values at startup

Make important configuration fields required when the application cannot function without them. This allows invalid deployments to fail early rather than producing errors during requests.

Apply least-privilege access

An application should receive only the secrets and configuration values it needs. Restrict access to secret stores, deployment settings, and runtime environments.

Rotate exposed credentials

If a credential appears in source control, logs, or an error report, removing it from the current file may not be enough. Revoke or rotate the credential and investigate where it was exposed.

Avoid leaking configuration through errors

Validation errors can sometimes include information about invalid inputs. Review how errors are logged and returned, especially when settings contain credentials. Keep detailed diagnostics in appropriately protected logs and avoid exposing them to clients.

Test configuration behavior

Test missing required values, invalid types, invalid ranges, and environment-specific settings. Also verify that tests do not depend unintentionally on a developer's local .env file.

Advantages and Disadvantages

Pydantic Settings offers several advantages:

  • Centralizes application configuration in a typed model.

  • Validates configuration values during initialization.

  • Supports environment variables and .env files.

  • Provides secret types that help prevent accidental disclosure.

  • Makes configuration easier to test and maintain.

There are also trade-offs. The settings model introduces another dependency, validation errors can prevent startup, and cached configuration does not automatically refresh. Furthermore, Pydantic Settings does not replace a secret manager or provide encryption and access control for secret storage.

These limitations are manageable when configuration validation and secret storage are treated as separate responsibilities.

Summary

Pydantic Settings provides a structured way to load, validate, and manage application configuration in Python projects. It reduces repeated environment-variable parsing and helps detect invalid values before the application starts serving requests.

Use typed settings fields, validate important values, and separate ordinary configuration from sensitive credentials. For local development, a properly ignored .env file can be convenient. For production, prefer an approved secret-management system and restrict access to credentials.

Finally, remember that SecretStr helps reduce accidental exposure but does not encrypt secrets. Secure configuration depends on both application-level validation and the systems that store, deliver, and protect sensitive values.