Validate Enum and Log-Level Fields in Pydantic
A LOG_LEVEL of DEBGU or an APP_ENV of prd should stop the process at startup, not silently fall through to a default. Typing these fields as a Python Enum or a Literal turns a free-form string into a closed set that pydantic validates on construction. This builds on strict mode and type coercion and the pydantic-settings fundamentals.
Problem 1: accepting any string
# ANTI-PATTERN
log_level: str = "INFO" # "DEBGU" is accepted; logging silently misconfigures
A typo passes validation and only surfaces later as missing log output.
Problem 2: branching on raw strings
# ANTI-PATTERN
if settings.app_env == "prod": # "production" and "PROD" silently take the else branch
enable_tracing()
Comparing free-form strings scatters environment knowledge and breaks on any casing difference.
Secure implementation
Restrict the field to a closed set with a str Enum (reusable) or a Literal (lightweight).
# config.py
from enum import Enum
from typing import Literal
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppEnv(str, Enum):
dev = "dev"
staging = "staging"
prod = "prod"
class Settings(BaseSettings):
model_config = SettingsConfigDict(extra="forbid")
app_env: AppEnv = AppEnv.dev
# a Literal is enough when the values need no behaviour:
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
@field_validator("log_level", mode="before")
@classmethod
def upper(cls, v: str) -> str:
return v.upper() if isinstance(v, str) else v # accept "info" -> "INFO"
settings = Settings() # raises ValidationError naming log_level/app_env if out of set
Now settings.app_env is AppEnv.prod is a safe, typo-proof comparison, and an invalid LOG_LEVEL fails before logging is configured.
Gotchas & version-specific behaviour
- Subclass
str, Enumso the value serializes as its string inmodel_dump()and JSON. Literalvalidation is case-sensitive; add amode="before"validator to normalise casing.- pydantic v2 accepts either the enum member or its value from the environment; it rejects anything else.
- Enums give you exhaustive
matchstatements and IDE autocomplete that raw strings do not. - Keep the default inside the allowed set, or the model cannot construct without the variable set.
Production parity checklist
- Type every “one of a fixed set” field as an
EnumorLiteral, never a barestr. - Normalise casing in a
mode="before"validator if the environment is inconsistent. - Compare against enum members, not string literals, in application code.
- Add a CI test that constructs the model with an invalid value and asserts it raises.
- Keep the default value within the allowed set.
Frequently asked questions
Should I use an Enum or a Literal for a fixed set of config values?
Use a str-based Enum when the values have behaviour or are reused across the codebase, and a Literal when you just need to restrict a field to a handful of strings. Both make pydantic reject any value outside the set at startup.
How do I validate LOG_LEVEL from an environment variable?
Type the field as a Literal of the allowed names or a str Enum. pydantic coerces the incoming string and raises a ValidationError naming the field if it is not one of the allowed values, before logging is configured.
Why does my enum field reject a lowercase value?
Enum matching is case-sensitive on the member value. Normalise with a field_validator that upper-cases the input, or define the enum values in the exact casing your environment supplies.
Key takeaways
Model fixed-set configuration as an Enum or Literal so an invalid LOG_LEVEL or environment name fails at startup, and compare against enum members instead of raw strings in your code.