Skip to content

envdrift validate

Validate one or more .env files against a Pydantic Settings schema.

Synopsis

envdrift validate [ENV_FILE]... --schema SCHEMA [OPTIONS]

Description

The validate command checks that your .env file matches your Pydantic Settings schema. It verifies:

  • Required variables - All required fields in the schema exist in the .env file
  • Type validation - Values are accepted by the field's type using real pydantic-settings semantics
  • Extra variables - Warns about variables not defined in the schema (errors if extra="forbid")
  • Encryption status - Optionally warns if sensitive fields are not encrypted

Type validation mirrors what the real app does at startup:

  • Booleans accept every spelling Pydantic v2 accepts (true/false, 1/0, yes/no, on/off, t/f, y/n, any case).
  • A present-but-empty value (PORT=) is validated as the empty string pydantic-settings actually passes through, so an empty int/float/bool field fails like the real app (unless the schema sets env_ignore_empty=True, in which case the value counts as unset).
  • SettingsConfigDict(env_prefix="MYAPP_") is applied to plain field names, so api_key binds to MYAPP_API_KEY under the default case-insensitive matching. Explicit field aliases and validation aliases bypass the prefix, including every candidate in AliasChoices. If several choices are present, the first declared choice supplies the value; every choice is still recognized as a schema variable and, for sensitive fields, as sensitive. case_sensitive=True requires each binding's casing to match exactly.
  • Complex fields (list, dict, nested models) are JSON-decoded first, exactly like the pydantic-settings env source: TAGS=a,b,c fails for TAGS: list[str] while TAGS=["a","b","c"] passes.
  • Integer parsing is ASCII-only, matching Pydantic (fullwidth digits are rejected).
  • Field and model validators run against the assembled settings values. A model-level rejection appears as a Model validation type error instead of a false pass.
  • If custom validation raises an unexpected non-validation exception, validation stays crash-safe and reports only the exception type as a warning; the exception message is omitted because it may contain setting values.
  • dotenvx's DOTENV_PUBLIC_KEY* artifact is exempt from the extra-variable check and the sensitive-name warning, so encrypted files validate cleanly against an extra="forbid" schema.

The file itself is parsed with python-dotenv's quoting rules — the parser pydantic-settings uses — so quoted values (well-formed or malformed) are judged exactly as the real app loads them:

  • A value opened with " or ' continues across physical lines until the matching close quote (multiline PEM certificates and keys parse as one value); interior KEY=value-looking lines are part of the value, never separate variables.
  • Inside quoted values the python-dotenv escape sequences are decoded (\n, \t, \", \\, ... in double quotes; \\ and \' in single quotes).
  • Malformed quoted bindings are rejected exactly like python-dotenv: an unterminated quote, or non-comment content after the close quote, drops the whole binding (no truncated value, no phantom variables from interior lines). A validation warning identifies the binding's starting line so the omission is never silent.
  • ${VAR} and ${VAR:-default} references are expanded the way dotenv_values does: a name defined earlier in the same file wins over os.environ (looked up case-sensitively, exactly like python-dotenv), and an unset name falls back to the default (or ""), so PORT=${OFFSET}234 is judged as the value the app receives.
  • Unquoted values follow python-dotenv's comment rule: the whitespace right after = is consumed first, then the value is cut at the first whitespace-preceded #. MSG=user's data # comment is user's data even though the value contains a stray quote, while K= # c keeps the whole # c as its value (a leading # has no whitespace before it inside the value).
  • Physical lines end at \n / \r\n / \r only; other Unicode line boundaries (U+2028, form feed, ...) are value content.
  • A leading UTF-8 BOM is stripped instead of becoming part of the first key, and validate warns about it: pydantic-settings reads plain UTF-8, so the running app still sees the BOM-prefixed key unless the BOM is removed or env_file_encoding='utf-8-sig' is set.

Quoted keys and bare KEY bindings without = also follow python-dotenv. Bare bindings are omitted from validation because pydantic-settings filters their None values before model validation. Other non-comment content that the lexer cannot parse is ignored with a line-numbered warning; comments and blank lines do not warn.

Multiple env files can be validated in one invocation (each gets its own report; with --ci the exit code is 1 if any file fails). This keeps the command usable as a pre-commit pass_filenames: true hook, where every matched staged file is appended to a single command line.

Arguments

Argument Description Default
ENV_FILE... Path(s) to the .env file(s) to validate .env

Options

--schema, -s (required)

Dotted path to your Pydantic Settings class.

envdrift validate .env.production --schema config.settings:ProductionSettings
envdrift validate .env -s myapp.config:Settings

The format is module.path:ClassName where:

  • module.path is the Python import path
  • ClassName is the Settings class name

--service-dir, -d

Directory to add to Python's sys.path for imports. Defaults to the current directory, so a schema generated by envdrift init in your project root (e.g. settings.py) imports without this flag. Set it for monorepos or when the schema lives in a different directory.

# Schema in the project root (settings.py) — no --service-dir needed
envdrift validate .env -s settings:Settings

# Schema is in /app/backend/config/settings.py
envdrift validate .env -s config.settings:Settings -d /app/backend

# Monorepo structure
envdrift validate services/api/.env -s config:Settings -d services/api

--ci

CI mode: exit with code 1 if validation fails. Use this in CI/CD pipelines.

envdrift validate .env.production -s config.settings:ProductionSettings --ci

Without --ci, the command always exits with code 0 (unless there's a fatal error like missing file).

--check-encryption / --no-check-encryption

Control whether to check if sensitive variables are encrypted.

# Check encryption (default)
envdrift validate .env.production -s config.settings:ProductionSettings --check-encryption

# Skip encryption check
envdrift validate .env.production -s config.settings:ProductionSettings --no-check-encryption

When enabled, unencrypted sensitive fields are reported as warnings (not errors). Use envdrift encrypt --check for strict encryption enforcement. Sensitive fields are matched by every validation alias when present, so aliased fields (including all AliasChoices candidates) are not also reported as unmarked sensitive.

--fix

Output a template for missing variables. Useful for quickly adding required fields.

Note: --fix only prints a template when validation fails; it is a no-op when validation passes.

envdrift validate .env.production -s config.settings:ProductionSettings --fix

Example output:

Fix template:
# Missing required variables:
# API key for external service
NEW_API_KEY="encrypted:YOUR_VALUE_HERE"

# Database connection string
DATABASE_URL=

--verbose, -v

Show additional details including missing optional variables with their defaults.

envdrift validate .env.production -s config.settings:ProductionSettings -v

Examples

Basic Validation

envdrift validate .env.production --schema config.settings:ProductionSettings

Output when passing:

╭─────────────────────── envdrift validate ───────────────────────╮
│ Validating: .env.production                                     │
│ Schema: config.settings:ProductionSettings                      │
╰─────────────────────────────────────────────────────────────────╯

Validation PASSED

Validation with Errors

envdrift validate .env.production --schema config.settings:ProductionSettings

Output when failing:

╭─────────────────────── envdrift validate ───────────────────────╮
│ Validating: .env.production                                     │
│ Schema: config.settings:ProductionSettings                      │
╰─────────────────────────────────────────────────────────────────╯

Validation FAILED

MISSING REQUIRED VARIABLES:
  * DATABASE_URL - PostgreSQL connection string
  * API_KEY - Backend service API key

TYPE ERRORS:
  * PORT: Expected integer, got 'not_a_number'

Summary: 3 error(s), 0 warning(s)

Run with --fix to generate template for missing variables.

CI/CD Pipeline

# GitHub Actions
- name: Validate environment
  run: |
    pip install envdrift
    envdrift validate .env.production \
      --schema config.settings:ProductionSettings \
      --ci

Generate Fix Template

envdrift validate .env.production -s config.settings:ProductionSettings --fix

Validate Multiple Environments

# Development
envdrift validate .env.development -s config.settings:DevelopmentSettings --ci

# Staging
envdrift validate .env.staging -s config.settings:StagingSettings --ci

# Production
envdrift validate .env.production -s config.settings:ProductionSettings --ci

Skip Encryption Warnings

envdrift validate .env.development -s config.settings:Settings --no-check-encryption

Verbose Output

envdrift validate .env.production -s config.settings:ProductionSettings -v

Shows missing optional variables:

MISSING OPTIONAL VARIABLES (have defaults):
  * DEBUG (default: False)
  * LOG_LEVEL (default: INFO)
  * WORKERS (default: 4)

Schema Requirements

Your Pydantic Settings class should be importable. If you have module-level code that instantiates settings, check for ENVDRIFT_SCHEMA_EXTRACTION:

import os
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DATABASE_URL: str
    DEBUG: bool = False

# Skip instantiation during schema extraction
if os.getenv("ENVDRIFT_SCHEMA_EXTRACTION"):
    settings = None
else:
    settings = Settings()

What Gets Validated

Check Error/Warning Description
Missing required vars Error Fields without defaults must exist
Type mismatches Error Values must be accepted by pydantic for the type
Field/model validator rejection Error Custom Pydantic validators must accept the settings
Unexpected validator exception Warning Type is reported without echoing its potentially sensitive message
Empty values for non-str fields Error PORT= crashes a real int field at startup
Complex fields with invalid JSON Error list/dict/nested fields are JSON-decoded
Extra vars (with extra="forbid") Error Unknown variables not allowed (DOTENV_PUBLIC_KEY* exempt)
Extra vars (with extra="ignore") Warning Unknown variables allowed but noted
Unencrypted sensitive vars Warning Fields marked sensitive=True should be encrypted
Case-insensitive name collision Warning Two .env keys differ only in case
Unparsable non-comment content Warning Ignored content is reported by starting line number

Note: Variable names are matched against schema fields case-insensitively, mirroring Pydantic Settings (case_sensitive=False). So an UPPERCASE .env (e.g. API_KEY) satisfies a lowercase field (api_key). If a single .env defines two keys that differ only in case (e.g. both API_KEY and api_key), they collapse to one field: the last occurrence wins (matching Pydantic) and a warning reports which value was kept and which was ignored, so the collision is never silent.

See Also

  • diff - Compare .env files
  • encrypt - Check/perform encryption
  • init - Generate schema from .env