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.

Key APIs in Problem 1: accepting any string The main identifiers used in Problem 1: accepting any string INFODEBGU
The APIs and identifiers this section uses.

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.

Key APIs in Problem 2: branching on raw strings The main identifiers used in Problem 2: branching on raw strings PRODsettings.app_envenable_tracing
The APIs and identifiers this section uses.

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.

Key APIs in Secure implementation The main identifiers used in Secure implementation EnumLiteralNowBaseSettingsSettingsConfigDictAppEnv
The APIs and identifiers this section uses.

Gotchas & version-specific behaviour

  • Subclass str, Enum so the value serializes as its string in model_dump() and JSON.
  • Literal validation is case-sensitive; add a mode="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 match statements and IDE autocomplete that raw strings do not.
  • Keep the default inside the allowed set, or the model cannot construct without the variable set.
Gotchas & version-specific behaviour Key points from Gotchas & version-specific behaviour Subclass str, Enum so theval…Literal validation iscase-se…pydantic v2 accepts eitherth…Enums give you exhaustivemat…Keep the default inside thea…
The essentials of Gotchas & version-specific behaviour.

Production parity checklist

  • Type every “one of a fixed set” field as an Enum or Literal, never a bare str.
  • 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.
Production parity checklist Key points from Production parity checklist Type every "one of a fixedse…Normalise casing in amode="b…Compare against enummembers,…Add a CI test thatconstructs…Keep the default valuewithin…
Every item to tick off for Production parity checklist.

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.