You’ve just deployed your FastAPI application to production, feeling confident. The local tests passed, the code looks clean, and you’re ready to celebrate. Then you check the logs and see this:
pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
DATABASE_URL
Field required [type=missing, input_value={}, input_type=dict]
Your app won’t even start. Meanwhile, it runs perfectly on your machine. Sound familiar?
This is one of the most frustrating issues FastAPI developers face when moving from development to production. The good news? Once you understand what’s happening, it’s straightforward to fix—and prevent from happening again.
Symptom Description
When Pydantic Settings validation fails, you’ll typically see one or more of these symptoms:
At Application Startup:
- The application crashes immediately when starting
- You see a
ValidationErrorfrompydantic_core - Error messages mention “Field required” or “validation error for Settings”
- The traceback points to your settings class instantiation
Common Error Patterns:
ValidationError: 1 validation error for Settings
API_KEY
Field required [type=missing, input_value={}, input_type=dict]
Or multiple fields at once:
ValidationError: 3 validation errors for Settings
DATABASE_URL
Field required [type=missing, input_value={}, input_type=dict]
SECRET_KEY
Field required [type=missing, input_value={}, input_type=dict]
REDIS_URL
Field required [type=missing, input_value={}, input_type=dict]
The frustrating part? Your app works fine locally, making this feel like some mysterious deployment curse. Spoiler: it’s not magic, just missing configuration.
Diagnostic Steps
Before we dive into fixes, let’s diagnose what’s actually happening. Here’s how to identify the root cause:
Step 1: Check Your Settings Class
Look at how you’re defining settings in your FastAPI app. Most developers use Pydantic’s BaseSettings (v1) or BaseSettings from pydantic_settings (v2+):
# Your settings file (config.py or similar)
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
API_KEY: str
DEBUG: bool = False
Step 2: Identify Required vs Optional Fields
Fields without default values are required. Pydantic will throw a ValidationError if they’re missing when the Settings class is instantiated. In the example above, DATABASE_URL, SECRET_KEY, and API_KEY are required, while DEBUG is optional (defaults to False).
Step 3: Check Where Settings Are Loaded
Find where your app instantiates the Settings class:
# Typically in main.py or config.py
settings = Settings() # This is where the error happens!
This single line of code is where Pydantic tries to load all environment variables and validate them. If any required field is missing, boom—ValidationError.
Step 4: Compare Environments
The reason it works locally but fails in production is simple: your local environment has the variables set (probably in a .env file), but your production environment doesn’t.
Cause #1: Missing Environment Variables in Production
The Problem:
This is the most common cause. You’ve set environment variables locally (often in a .env file that’s in .gitignore), but you forgot to configure them in your production environment—whether that’s Docker, Kubernetes, a cloud platform, or a traditional server.
The Fix:
You need to set environment variables in your deployment environment. The exact method depends on your platform:
For Docker:
# Dockerfile
FROM python:3.11-slim
# ... other setup ...
# Set environment variables directly
ENV DATABASE_URL="postgresql://user:pass@db:5432/myapp"
ENV SECRET_KEY="your-secret-key-here"
ENV API_KEY="your-api-key-here"
# Or use ARG for build-time variables
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
Or better yet, use a .env file with Docker Compose:
# docker-compose.yml
version: '3.8'
services:
api:
build: .
env_file:
- .env.production # Separate env file for production
ports:
- "8000:8000"
For Kubernetes:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-app
spec:
template:
spec:
containers:
- name: api
image: your-fastapi-image:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: app-secrets
key: secret-key
For Cloud Platforms (AWS, GCP, Azure, Heroku, etc.):
Most platforms have a UI or CLI for setting environment variables:
# Heroku
heroku config:set DATABASE_URL="postgresql://..." --app your-app
# AWS Elastic Beanstalk
eb setenv DATABASE_URL="postgresql://..." SECRET_KEY="..."
# Google Cloud Run
gcloud run deploy your-service \
--set-env-vars DATABASE_URL="postgresql://...",SECRET_KEY="..."
For systemd Services (Linux servers):
# /etc/systemd/system/fastapi-app.service
[Service]
Environment="DATABASE_URL=postgresql://..."
Environment="SECRET_KEY=your-secret-key"
Environment="API_KEY=your-api-key"
ExecStart=/path/to/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Quick Verification:
After setting the variables, verify they’re actually available in your container/environment:
# SSH into your container/server and run:
echo $DATABASE_URL
printenv | grep DATABASE_URL
# Or in your Python code temporarily:
import os
print(os.environ.get('DATABASE_URL'))
Cause #2: Case Sensitivity Mismatch
The Problem:
Environment variable names are case-sensitive on most systems. If your Settings class expects DATABASE_URL but you’ve set database_url (or vice versa), Pydantic won’t find it.
Example of the Issue:
# Your Settings class
class Settings(BaseSettings):
DATABASE_URL: str # Expects uppercase
But in your environment:
# You set it lowercase
export database_url="postgresql://..."
Result? ValidationError: Field required even though you did set the variable.
The Fix:
By default, Pydantic is case-insensitive when reading environment variables (it converts field names to uppercase). But this can be changed with configuration. Make sure you’re consistent:
Option 1: Use Uppercase (Recommended Convention)
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
class Config:
case_sensitive = False # Default, allows any case in env
Option 2: Explicitly Control Case Sensitivity
class Settings(BaseSettings):
database_url: str # Lowercase field name
secret_key: str
class Config:
case_sensitive = True # Strict matching
env_prefix = "" # No prefix
Best Practice: Stick with uppercase environment variables—it’s the widely accepted convention (see the Twelve-Factor App). This makes your config instantly recognizable across different languages and tools.
Cause #3: Using .env Files That Aren’t Loaded in Production
The Problem:
Locally, you’re using a .env file that Pydantic automatically loads. But in production, that file doesn’t exist (it’s in .gitignore, which is good for security), and you haven’t configured an alternative way to load environment variables.
How .env Loading Works:
By default, Pydantic Settings looks for a .env file in the current working directory:
# This works if you have a .env file locally
class Settings(BaseSettings):
DATABASE_URL: str
class Config:
env_file = ".env" # Pydantic loads this automatically
Your local .env file might look like:
DATABASE_URL=postgresql://localhost:5432/myapp
SECRET_KEY=dev-secret-key-not-for-production
API_KEY=test-api-key-123
But when you deploy, this file doesn’t exist, so all those variables are missing.
The Fix:
You have several options:
Option 1: Use Platform-Specific Environment Variables (Recommended)
Don’t rely on .env files in production at all. Instead, use your platform’s native environment variable system (as shown in Cause #1). This is more secure and follows best practices.
Update your Settings class to make .env optional:
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
API_KEY: str
DEBUG: bool = False
class Config:
# Load .env if it exists, but don't fail if it doesn't
env_file = ".env"
env_file_encoding = "utf-8"
extra = "ignore" # Ignore extra fields not in the model
# This won't crash if .env is missing, as long as env vars are set
settings = Settings()
Option 2: Use Different .env Files Per Environment
If you prefer using .env files everywhere, create environment-specific files and include them in your deployment:
import os
from pydantic_settings import BaseSettings
environment = os.getenv("ENVIRONMENT", "development")
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
class Config:
env_file = f".env.{environment}" # .env.production, .env.staging, etc.
env_file_encoding = "utf-8"
settings = Settings()
Then set the ENVIRONMENT variable in your deployment:
export ENVIRONMENT=production
And make sure .env.production is available (you might build it during CI/CD or mount it as a secret).
Option 3: Conditional .env Loading
Load .env only in development:
from pydantic_settings import BaseSettings
import os
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
class Config:
# Only load .env in development
env_file = ".env" if os.getenv("ENVIRONMENT") != "production" else None
settings = Settings()
Cause #4: Typos in Environment Variable Names
The Problem:
Sometimes the issue is embarrassingly simple: you’ve misspelled the variable name. Your Settings class expects DATABASE_URL, but you set DATABSE_URL (missing the ‘A’).
The Fix:
Double-check your spelling! This is easier said than done when dealing with long variable names. Here’s a systematic approach:
Step 1: List Expected Variables
Add logging to your Settings class to see exactly what Pydantic is looking for:
from pydantic_settings import BaseSettings
import os
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
API_KEY: str
REDIS_URL: str
class Config:
env_file = ".env"
# Before instantiating settings, print what's available
print("Environment variables available:")
for key in ['DATABASE_URL', 'SECRET_KEY', 'API_KEY', 'REDIS_URL']:
value = os.getenv(key)
print(f" {key}: {'SET' if value else 'MISSING'}")
# Now try to create settings
try:
settings = Settings()
print("✅ Settings loaded successfully!")
except Exception as e:
print(f"❌ Settings validation failed: {e}")
Step 2: Use a Validation Script
Create a simple script to validate your environment before deployment:
# validate_env.py
import sys
from typing import List
REQUIRED_VARS = [
'DATABASE_URL',
'SECRET_KEY',
'API_KEY',
'REDIS_URL',
]
def check_environment() -> List[str]:
"""Check which required environment variables are missing."""
import os
missing = []
for var in REQUIRED_VARS:
if not os.getenv(var):
missing.append(var)
return missing
if __name__ == "__main__":
missing = check_environment()
if missing:
print("❌ Missing required environment variables:")
for var in missing:
print(f" - {var}")
sys.exit(1)
else:
print("✅ All required environment variables are set!")
sys.exit(0)
Run this before starting your app:
python validate_env.py && uvicorn main:app
Cause #5: Field Type Validation Failures (Not Missing, Just Invalid)
The Problem:
Sometimes the environment variable is set, but it fails validation because the value doesn’t match the expected type. The error message looks similar but has subtle differences:
ValidationError: 1 validation error for Settings
DEBUG
Input should be a valid boolean [type=bool_parsing, input_value='true', input_type=str]
Notice it says “Input should be a valid boolean” rather than “Field required.” The variable exists but has the wrong format.
Common Type Mismatches:
class Settings(BaseSettings):
PORT: int # Expects integer
DEBUG: bool # Expects boolean
ALLOWED_HOSTS: list[str] # Expects list
DATABASE_POOL_SIZE: int # Expects integer
But in your environment:
export PORT="not-a-number" # ❌ String, not int
export DEBUG="yes" # ❌ Not a valid bool representation
export ALLOWED_HOSTS="localhost,example.com" # ❌ String, not list
export DATABASE_POOL_SIZE="10.5" # ❌ Float, not int
The Fix:
Pydantic is smart about type coercion, but you need to format your environment variables correctly:
For Booleans:
# ✅ These work:
export DEBUG=true
export DEBUG=false
export DEBUG=1
export DEBUG=0
export DEBUG=True
export DEBUG=False
# ❌ These don't:
export DEBUG=yes
export DEBUG=no
export DEBUG=enabled
For Integers:
# ✅ This works:
export PORT=8000
export DATABASE_POOL_SIZE=10
# ❌ This doesn't:
export PORT=8000.0 # Float, not int
export PORT="eight thousand" # String
For Lists:
Pydantic can parse JSON strings for complex types:
# ✅ Using JSON format:
export ALLOWED_HOSTS='["localhost", "example.com", "*.mydomain.com"]'
# Or use a comma-separated string with a custom validator:
export ALLOWED_HOSTS="localhost,example.com,*.mydomain.com"
If you want comma-separated values to work, add a validator:
from pydantic import field_validator
from pydantic_settings import BaseSettings
from typing import List
class Settings(BaseSettings):
ALLOWED_HOSTS: List[str]
@field_validator('ALLOWED_HOSTS', mode='before')
@classmethod
def parse_allowed_hosts(cls, v):
if isinstance(v, str):
return [host.strip() for host in v.split(',')]
return v
# Now this works:
# export ALLOWED_HOSTS="localhost,example.com"
For URLs and Paths:
Pydantic has special types for these:
from pydantic import HttpUrl, PostgresDsn, RedisDsn
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: PostgresDsn # Validates PostgreSQL URLs
REDIS_URL: RedisDsn # Validates Redis URLs
API_ENDPOINT: HttpUrl # Validates HTTP/HTTPS URLs
Make sure your environment variables are properly formatted URLs:
export DATABASE_URL="postgresql://user:pass@localhost:5432/dbname"
export REDIS_URL="redis://localhost:6379/0"
export API_ENDPOINT="https://api.example.com"
Still Not Working?
If you’ve tried all the above and still see validation errors, here are some edge cases to check:
Check for Whitespace:
Environment variables with leading/trailing whitespace can cause issues:
# ❌ Bad (notice the space):
export SECRET_KEY=" my-secret-key"
# ✅ Good:
export SECRET_KEY="my-secret-key"
Check Your .env File Format:
If using a .env file, make sure it’s formatted correctly:
# ✅ Good:
DATABASE_URL=postgresql://localhost/mydb
SECRET_KEY=abc123
# ❌ Bad (spaces around =):
DATABASE_URL = postgresql://localhost/mydb
SECRET_KEY = abc123
# ❌ Bad (quotes that become part of the value):
DATABASE_URL="postgresql://localhost/mydb"
Note: Some .env parsers handle quotes differently. Pydantic’s python-dotenv integration strips quotes, but if you’re using a different loader, they might be included in the value.
Check Environment Variable Precedence:
If you’re loading from multiple sources, understand the order of precedence. Pydantic loads in this order (later sources override earlier ones):
- Default values in your Settings class
- Variables from
.envfile (if configured) - Environment variables from the system
Use pydantic_settings’ Debugging:
Enable Pydantic’s debug mode to see exactly what it’s doing:
import os
os.environ['PYDANTIC_DEBUG'] = '1'
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
class Config:
env_file = ".env"
settings = Settings()
This will print detailed information about where each setting is coming from.
Summary Checklist
Here’s a quick checklist to run through when you encounter Pydantic Settings ValidationError:
- [ ] Are all required environment variables set in production? Check your deployment platform’s environment configuration.
- [ ] Is the variable name spelled correctly? Check for typos in both your Settings class and your environment.
- [ ] Is the case consistent? Use uppercase for environment variables (standard convention).
- [ ] Are variable values formatted correctly for their types? (e.g.,
true/falsefor booleans, numbers for integers) - [ ] If using .env files, are they present in production? Consider using platform-native env vars instead.
- [ ] Are there any whitespace issues? Trim leading/trailing spaces from values.
- [ ] Have you tested locally with the same environment as production? Try running without your
.envfile to simulate production. - [ ] Did you restart your application after setting new variables? Changes to environment variables require a restart.
Prevention: Design Settings for Resilience
Want to avoid these headaches in the future? Here are some best practices:
Use Sensible Defaults Where Possible:
class Settings(BaseSettings):
# Required fields for critical config
DATABASE_URL: str
SECRET_KEY: str
# Optional fields with sensible defaults
DEBUG: bool = False
PORT: int = 8000
LOG_LEVEL: str = "INFO"
WORKERS: int = 4
class Config:
env_file = ".env"
Create an Example .env File:
Add a .env.example file to your repo with all required variables (but no real secrets):
# .env.example
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
SECRET_KEY=your-secret-key-here
API_KEY=your-api-key-here
DEBUG=false
Developers and DevOps teams can copy this to .env or .env.production and fill in real values.
Document Your Configuration:
Add a CONFIG.md or section in your README listing all environment variables, their types, and whether they’re required:
| Variable | Type | Required | Default | Description |
|---|---|---|---|---|
DATABASE_URL |
str |
Yes | - | PostgreSQL connection string |
SECRET_KEY |
str |
Yes | - | Secret key for session signing |
DEBUG |
bool |
No | false |
Enable debug mode |
PORT |
int |
No | 8000 |
Port to run the server on |
Add Startup Validation:
Fail fast with clear error messages if critical config is missing:
from pydantic_settings import BaseSettings
import sys
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
class Config:
env_file = ".env"
try:
settings = Settings()
except Exception as e:
print("❌ Configuration Error:")
print(str(e))
print("\nPlease ensure all required environment variables are set.")
print("See .env.example for a template.")
sys.exit(1)
This gives you a clear, actionable error message instead of a cryptic traceback.
Pydantic Settings ValidationError is frustrating, but it’s almost always caused by one of the issues we’ve covered: missing variables, typos, case mismatches, wrong value types, or missing .env files in production. The key is systematic debugging—check what variables are actually available, verify their values and types, and ensure your production environment mirrors your development setup.
Need help debugging other FastAPI issues? Check out our guides on fixing 422 validation errors and troubleshooting CORS errors. And when you hit a Python traceback that’s hard to parse, use Debugly’s trace formatter to quickly analyze and understand the error stack.