Introduction
Modern Python applications often rely on configuration files to manage settings like database connections, application features, and environment-specific parameters. Popular formats include YAML and TOML because they are human-readable and easy to maintain. However, these files are prone to errors, like missing fields or invalid types, which can cause runtime crashes.
This article demonstrates how to use Pydantic, a Python data validation library, to validate YAML and TOML configurations — from basic usage to advanced production-ready techniques.
Understanding the Problem
Suppose we have a configuration file for the application:
app:
name: MyApp
version: 1.0
database:
host: localhost
port: 5432
enabled: trueWithout validation:
A typo in
port("5432a") would crash the app.Missing keys like
databasewould cause runtime errors.Invalid types (
enabled: "yes") could lead to unexpected behavior.
Goal: Automatically verify that all required fields exist, are of the correct type, and meet any constraints.
Introduction to Pydantic
Pydantic provides:
Type enforcement: Ensures the right type for each field
Field validation: Enforces constraints like value ranges
Nested models: Supports structured, hierarchical configs
Fail-fast validation: Errors are raised immediately if something is wrong
Basic Pydantic example:
from pydantic import BaseModel
class AppConfig(BaseModel):
name: str
version: float
app = AppConfig(name="MyApp", version=1.0)Loading YAML and TOML in Python
Python libraries:
YAML: Use
PyYAML(pip install pyyaml)TOML: Python 3.11+ has built-in
tomllib
import yaml
import tomllibDefining Pydantic Models for Configuration
Suppose your config has two sections: add and database
from pydantic import BaseModel, Field
class AppConfig(BaseModel):
name: str
version: float
class DatabaseConfig(BaseModel):
host: str
port: int = Field(gt=0, lt=65536) # port must be between 1-65535
enabled: bool
class Settings(BaseModel):
app: AppConfig
database: DatabaseConfigField (gt=0, lt=65536) ensures the port is valid.
Nested models enforce structure ( app and database).
Loading and Validating YAML
def load_yaml(file_path: str) -> Settings:
with open(file_path, "r") as f:
data = yaml.safe_load(f)
return Settings.model_validate(data) # Pydantic v2Example usage:
settings = load_yaml("config.yaml")
print(settings.app.name) # MyApp
print(settings.database.port) # 5432Loading and Validating TOML
def load_toml(file_path: str) -> Settings:
with open(file_path, "rb") as f:
data = tomllib.load(f)
return Settings.model_validate(data)
TOML example (config.toml):
[app]
name = "MyApp"
version = 1.0
[database]
host = "localhost"
port = 5432
enabled = trueUnified Loader for YAML and TOML
To simplify usage:
import os
import sys
def load_config(file_path: str) -> Settings:
if not os.path.exists(file_path):
raise FileNotFoundError(f"Config file not found: {file_path}")
try:
if file_path.endswith((".yaml", ".yml")):
return load_yaml(file_path)
elif file_path.endswith(".toml"):
return load_toml(file_path)
else:
raise ValueError("Unsupported config format")
except ValidationError as e:
print("Config validation failed!")
print(e)
sys.exit(1)Basic Usage in Your Application
from config import load_config
import os
def main():
config_file = os.path.join(os.path.dirname(__file__), "config.yaml")
settings = load_config(config_file)
print("App Name:", settings.app.name)
print("Database Host:", settings.database.host)
if __name__ == "__main__":
main()Loads and validates config on application startup.
Fails immediately if config is invalid.
Advanced Features
Environment Variable Overrides
With Pydantic v2, you can use BaseSettings to automatically override fields from environment variables:
from pydantic import BaseSettings
class Settings(BaseSettings):
app_name: str
database_port: int
class Config:
env_prefix = "MYAPP_"MYAPP_APP_NAMEin environment overridesapp_name.
Custom Validators
from pydantic import field_validator
class DatabaseConfig(BaseModel):
host: str
port: int
enabled: bool
@field_validator("host")
@classmethod
def host_not_empty(cls, v):
if not v.strip():
raise ValueError("Host cannot be empty")
return vDefault Values and Optional Fields
class DatabaseConfig(BaseModel):
host: str = "localhost"
port: int = 5432
enabled: bool = True
Makes some fields optional while still validating others.
Fail-Fast and User-Friendly Errors
Always wrap loading in a
try/except ValidationError.Print field-specific errors so developers know exactly what’s wrong.
try:
settings = load_config("config.yaml")
except ValidationError as e:
print("Invalid config:", e)
sys.exit(1)Benefits of Using Pydantic
Type safety: ensures correct types at startup
Structured configs: nested sections like
appanddatabaseFail-fast: invalid configs stop the app immediately
Easy to extend: environment variables, defaults, and validators
Works for YAML, TOML, JSON, and Python dictionaries
Full working example is available at my github Jayant0516 (Jayant Kumar)

Join the conversation! Your thoughts help the community grow.