Skip to main content
Every application has configuration — a database URL, an API key, a port number, a feature flag. The conventional way to manage this in Python is os.getenv(), but it has a fundamental flaw: environment variables are always strings, and you have to manually parse them into the right types yourself. Miss a conversion and PORT=8000 stays a string, or DEBUG=False evaluates to True because any non-empty string is truthy. Pydantic Settings solves this by bringing the same runtime validation you’ve been using for request data to your application’s configuration — you declare what your settings look like, Pydantic reads the environment and validates everything for you, and your code works with typed, validated values from the start.

The problem with raw environment variables

You end up writing the same boilerplate parsing code everywhere, and it’s easy to get wrong.
os.getenv("DEBUG", "False") returns the string "False", not the boolean False. Always == "True" comparisons — or better yet, use Pydantic Settings to handle this automatically.

Installation

pydantic-settings is a separate package (it ships separately from Pydantic v2):

Basic usage

Inherit from BaseSettings and declare fields exactly as you would with BaseModel:
When you instantiate Settings(), Pydantic automatically reads your environment variables (case-insensitive):
1

API_KEY

Required — no default. If not set, Pydantic raises a ValidationError immediately.
2

PORT

Optional — defaults to 8000. If set, the string is automatically parsed to int.
3

DEBUG

Optional — defaults to False. Accepts "True", "False", "1", "0", "yes", "no".

Reading from a .env file

In development, store your configuration in a .env file rather than setting shell variables by hand:
Tell Pydantic Settings to read it with SettingsConfigDict:
Environment variables set in your shell always override values in .env. This is intentional — your CI/CD pipeline or production host sets real secrets via environment variables, while .env provides convenient local defaults.

Using an environment prefix

If your host environment has many variables from different applications, prefix your settings to avoid collisions:

Protecting secrets with SecretStr

Use SecretStr for sensitive values like passwords and API keys. Pydantic masks the value when the object is printed or logged, so secrets don’t leak into your output:
Use SecretStr for any value you wouldn’t want to appear in log files: API keys, database passwords, OAuth tokens, and similar credentials.

Caching settings with lru_cache

Instantiating Settings() reads files and environment variables. Call it once and reuse the result everywhere via lru_cache:
In FastAPI you’ll typically inject get_settings as a dependency:

Environment-specific behaviour

Add computed properties to your settings class for environment-aware logic:

A complete example

Here’s a realistic settings class for a FastAPI application:
And the corresponding .env file:
A plain dictionary or module-level constants give you no validation — a typo in a variable name silently returns None, and environment variable parsing is manual and error-prone. BaseSettings gives you:
  • Automatic type coercion — strings become ints, bools, lists
  • Validation errors for missing required settings at startup, not at runtime
  • Secret masking via SecretStr
  • Clear documentation of every setting your app needs
  • Overridable in tests — you can pass values directly: Settings(api_key="test-key")

Learn more


What’s next?

You now have a complete Pydantic toolkit — models, field constraints, nested structures, and settings. The next step is putting all of this to work inside a FastAPI application.

Project Handling

Learn how to structure Python projects, manage dependencies, and package your applications.