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.
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:
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.
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:
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:
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:
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:
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:
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:
.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.
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:
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:
Store the secret in an approved secret-management system.
Grant access only to the application identity that needs it.
Inject or retrieve the secret through the deployment environment.
Validate the received value during application initialization.
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:
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:
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:
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:
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:
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.
Join the conversation! Your thoughts help the community grow.