Settings and companion packages
Configure pydantic-settings with environment variables and nested settings. Validate email addresses and use additional domain-specific types.
pydantic-settings: environment-based configuration#
Load configuration once, validate it early, and keep credentials out of output.
In v2, BaseSettings lives in the separate pydantic-settings package. It reads environment variables, dotenv files, and secret files into typed fields. Settings defaults are validated by default, unlike normal BaseModel defaults. Use env_prefix to avoid unrelated environment names and env_nested_delimiter for nested configuration.
import os
from pydantic import BaseModel, Field, SecretStr, ValidationError
from pydantic_settings import BaseSettings, SettingsConfigDict, SettingsError
class DatabaseSettings(BaseModel):
host: str
port: int = Field(default=5432, ge=1, le=65535)
password: SecretStr
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix='CHEATSHEET_', env_nested_delimiter='__',
env_file='.env', env_file_encoding='utf-8', extra='ignore'
)
database: DatabaseSettings
debug: bool = False
os.environ.update({
'CHEATSHEET_DATABASE': '{"host":"db.internal","password":"demo-password"}',
'CHEATSHEET_DATABASE__PORT': '6432',
'CHEATSHEET_DEBUG': 'true',
})
settings = Settings(_env_file=None)
print(settings.database.host, settings.database.port, settings.debug)
print(settings.database.password)
assert settings.database.port == 6432
os.environ['CHEATSHEET_DATABASE__PORT'] = '70000'
try:
Settings(_env_file=None)
except ValidationError as exc:
print(exc.errors()[0]['loc'], exc.errors()[0]['type'])
os.environ['CHEATSHEET_DATABASE'] = '{broken JSON'
try:
Settings(_env_file=None)
except SettingsError:
print('SettingsError: environment JSON could not be parsed')db.internal 6432 True
**********
('database', 'port') less_than_equal
SettingsError: environment JSON could not be parsed| Highest to lowest priority | Default settings behavior |
|---|---|
| CLI arguments | Only if CLI parsing is enabled |
| Constructor arguments | Explicit Settings(...) values |
| Environment variables | Nested variables override matching keys in a JSON environment value |
| Dotenv file | Loaded from the configured file; ordinary environment values win |
| Secret directory | Files configured with secrets_dir |
| Field defaults | Used when no source provides the field |
import os
from typing import Annotated
from pydantic import BeforeValidator, Field
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
def split_hosts(value):
if isinstance(value, str):
return [item.strip() for item in value.split(',') if item.strip()]
return value
HostList = Annotated[list[str], NoDecode, BeforeValidator(split_hosts)]
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='CHEATSHEET_')
allowed_hosts: HostList = Field(default_factory=list)
os.environ['CHEATSHEET_ALLOWED_HOSTS'] = 'api.journal.org, dashboard.journal.org'
print(Settings().allowed_hosts)['api.journal.org', 'dashboard.journal.org']Complex environment values are decoded as JSON by default. A CSV value can fail in the settings source before a field validator runs; NoDecode lets your own parser receive the raw text. ForceDecode opts a field back into JSON decoding. SettingsError reports source/parsing failures; ValidationError reports typed value failures.
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='CHEATSHEET_')
region: str = 'default'
api_key: SecretStr
@classmethod
def settings_customise_sources(cls, settings_cls, init_settings,
env_settings, dotenv_settings, file_secret_settings):
# Deliberately make deployment environment values outrank constructor values.
return env_settings, init_settings, dotenv_settings, file_secret_settings
os.environ['CHEATSHEET_REGION'] = 'asia'
with TemporaryDirectory() as folder:
Path(folder, 'CHEATSHEET_API_KEY').write_text('demo-key', encoding='utf-8')
settings = Settings(region='local', _secrets_dir=folder)
print(settings.region, settings.api_key)asia **********Cache settings at startup when the process should reuse one instance. Avoid mutating it, create a new instance when reloading, and clear the cache in tests. Keep dotenv secrets out of source control. Use extra="ignore" for shared dotenv files or extra="forbid" to catch unknown keys. Explicit aliases are normally unprefixed.
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class LocalSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='CHEATSHEET_FILE_', extra='forbid')
port: int = Field(default=8000, ge=1, le=65535)
debug: bool = False
with TemporaryDirectory() as folder:
dotenv = Path(folder, '.env')
dotenv.write_text('CHEATSHEET_FILE_PORT=9200\nCHEATSHEET_FILE_DEBUG=false\n', encoding='utf-8')
settings = LocalSettings(_env_file=dotenv)
print('dotenv:', settings.port, settings.debug)
os.environ['CHEATSHEET_FILE_DEBUG'] = 'true'
settings = LocalSettings(_env_file=dotenv)
print('environment wins:', settings.port, settings.debug)
print('constructor wins:', LocalSettings(port=9300, _env_file=dotenv).port)dotenv: 9200 False
environment wins: 9200 True
constructor wins: 9300Settings parses dotenv text into declared types. Constructor values normally outrank environment values, which outrank the dotenv file. _env_file selects a file for one call; None disables it. Relative paths resolve from the working directory.
Nested environment values override matching keys in the JSON object. Invalid JSON raises SettingsError before field validation; NoDecode allows custom parsing. Custom sources set precedence. env_prefix_target controls whether prefixes apply to variable names, aliases, or both.
Official reference: pydantic-settings: environment-based configuration.
email-validator and EmailStr#
Validate and normalize email syntax, then separately decide whether to check deliverability.
EmailStr and NameEmail use the optional email-validator package. Install it with pydantic[email] or email-validator. Pydantic’s built-in email validation checks syntax and normalization without a DNS deliverability check. It does not prove mailbox ownership; send a verification email for that.
from pydantic import BaseModel, EmailStr, NameEmail, ValidationError
from email_validator import validate_email, EmailNotValidError
class Contact(BaseModel):
email: EmailStr
sender: NameEmail
contact = Contact(email='Editor@JOURNAL.ORG', sender='Mira <editor@journal.org>')
print(contact.email, contact.sender.name, contact.sender.email)
normalized = validate_email('Editor@JOURNAL.ORG', check_deliverability=False)
print(normalized.normalized)
try:
Contact(email='not-an-email', sender='Mira <editor@journal.org>')
except ValidationError as exc:
print(exc.errors()[0]['loc'], exc.errors()[0]['type'])
try:
validate_email('not-an-email', check_deliverability=False)
except EmailNotValidError:
print('Direct email-validator rejected invalid syntax')Editor@journal.org Mira editor@journal.org
Editor@journal.org
('email',) value_error
Direct email-validator rejected invalid syntaxDirect validate_email(address, check_deliverability=True) can query DNS. Timeouts and DNS changes make this unsuitable for a deterministic syntax-only unit test or every hot-path model validation. Use a cached resolver and a separate workflow if you need it. DNS success still cannot prove that a particular mailbox exists or belongs to the user. Do not indiscriminately lowercase the local part.
Official reference: email-validator and EmailStr.
pydantic-extra-types and other companions#
Add specialized types only when they match your application’s domain.
python -m pip install pydantic-extra-types pycountry
# Only for a feature that needs these packages:
python -m pip install phonenumbers
# Schema-to-model generation:
python -m pip install datamodel-code-generator
# Validation-focused tests:
python -m pip install pytest hypothesis| Package | Useful task | Boundary |
|---|---|---|
pydantic-extra-types | Country codes, colors, coordinates, currency codes, phone numbers, and other domain values | Some modules need additional dependencies; inspect the type’s official reference |
annotated-types | Portable constraints such as Gt, Len, and predicates | Already installed with Pydantic; does not validate by itself |
datamodel-code-generator | Generate model code from JSON Schema or OpenAPI | Review generated defaults, unions, and business rules |
pytest / hypothesis | Regression and generated-input tests | Exercise boundary cases and serialize/validate round trips |
logfire | Optional validation observability | Review captured values and privacy settings before enabling instrumentation |
from pydantic import BaseModel, ValidationError
from pydantic_extra_types.country import CountryAlpha2
from pydantic_extra_types.coordinate import Latitude, Longitude
class ShippingLocation(BaseModel):
country: CountryAlpha2
latitude: Latitude
longitude: Longitude
location = ShippingLocation(country='BD', latitude=23.8103, longitude=90.4125)
print(location.model_dump(mode='json'))
try:
ShippingLocation(country='BD', latitude=100, longitude=90)
except ValidationError as exc:
print(exc.errors()[0]['loc'], exc.errors()[0]['type']){'country': 'BD', 'latitude': 23.8103, 'longitude': 90.4125}
('latitude', 'constrained-float') less_than_equalA country code validator cannot decide whether you ship there. A phone number parser cannot verify possession. Prefer a built-in or Annotated type when it already expresses your rule; add a companion package for a concrete domain need.
Official reference: pydantic-extra-types and other companions.
Help improve CodeRef
Allow Google Analytics to measure page visits and use of the guides? It uses cookies after you accept. Your choice won’t affect access to the site. Privacy notice