Skip to content

envdrift init

Generate a Pydantic Settings class from an existing .env file.

Synopsis

envdrift init [ENV_FILE] [OPTIONS]

Description

The init command bootstraps your schema by generating a Pydantic Settings class from an existing .env file. It:

  • Infers types - Detects booleans, integers, and strings
  • Detects sensitive fields - Marks likely secrets with sensitive=True
  • Creates required fields - String values become required (no default)
  • Adds defaults - Boolean and integer values get defaults
  • Reports malformed content - Warns with the line numbers of non-comment content that could not become schema fields

This is useful when:

  • Starting with an existing project that has .env files
  • Migrating from dotenv to pydantic-settings
  • Creating a baseline schema to customize

Arguments

Argument Description Default
ENV_FILE Path to the .env file to read .env

Options

--output, -o

Output file path for the generated Settings class.

envdrift init .env --output config/settings.py
envdrift init .env -o settings.py

--class-name, -c

Name for the generated Settings class.

envdrift init .env --class-name AppConfig
envdrift init .env -c ProductionSettings

--detect-sensitive

Enable automatic detection of sensitive variables. This is on by default, so the flag is rarely needed; there is currently no CLI option to turn detection off.

# Enable detection (the default)
envdrift init .env --detect-sensitive

--force, -f

Overwrite an existing output file. Without it, init refuses to clobber an existing (possibly hand-edited) file and errors:

[ERROR] Output file already exists: settings.py (use --force to overwrite)

Re-run with --force (or -f) to regenerate over the existing file:

envdrift init .env -o settings.py --force
envdrift init .env -o settings.py -f

Examples

Basic Generation

envdrift init .env

Input .env:

DATABASE_URL=postgres://localhost/mydb
API_KEY=sk-123456
DEBUG=true
PORT=8000
LOG_LEVEL=INFO

Generated settings.py:

"""Auto-generated Pydantic Settings class."""

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Settings generated from .env."""

    model_config = SettingsConfigDict(
        env_file=".env",
        extra="forbid",
    )

    API_KEY: str = Field(json_schema_extra={"sensitive": True})
    DATABASE_URL: str
    DEBUG: bool = True
    LOG_LEVEL: str
    PORT: int = 8000

The console reports Detected 1 sensitive variable(s) — only API_KEY matches (its value starts with sk-). DATABASE_URL=postgres://localhost/mydb has no embedded credentials, so it stays a required str.

Custom Output Path

envdrift init .env.production --output config/production.py

Custom Class Name

envdrift init .env --output settings.py --class-name AppSettings

Type Inference

.env Value Inferred Type Has Default
true, false bool Yes
123, 8000 int Yes
hello, postgres://... str No (required)

Variable Names and Identifiers

The generated module is always valid, importable Python:

  • Keyword names get an alias. A key whose name is a valid identifier but a Python keyword (e.g. class, import) is emitted under a sanitized attribute name plus a Pydantic alias so the field still binds to the original variable:
class_: str = Field(alias='class')
  • Pydantic-reserved names get an alias. A key that is a valid identifier but collides with Pydantic's protected model_ namespace (e.g. model_dump, model_config) or with a BaseSettings/BaseModel attribute (e.g. schema, dict, json, validate) is renamed with a field_ prefix plus a Pydantic alias, so it cannot raise at import or shadow model internals:
field_model_dump: str = Field(alias='model_dump')
  • Non-identifier keys are aliased, not dropped. Keys that are not identifier-style (a leading digit like 2FA_ENABLED, a dash like MY-DASH-VAR, a dot, or an emoji) are emitted with a sanitized attribute name plus a Pydantic alias, so every key ends up in the schema and still binds to the original environment variable:
field_2FA_ENABLED: str = Field(alias='2FA_ENABLED')
MY_DASH_VAR: str = Field(alias='MY-DASH-VAR')
  • Valid non-ASCII identifiers are kept verbatim. A key that passes str.isidentifier(), does not start with _, and is already NFKC-normalized (e.g. CAFÉ, ΑΛΦΑ) becomes a bare field with no alias. Python folds identifiers with NFKC at compile time, so a key whose folded form differs from the raw key — an NFD-composed CAFÉ or the ligature file — is aliased instead, preserving the exact original variable name.

  • Leading-underscore keys are aliased, not kept bare. A key like _PRIVATE passes str.isidentifier(), but Pydantic rejects field names with a leading underscore ("Fields must not use names with leading underscores"), so the key is emitted with a sanitized attribute name plus an alias — exactly like the non-identifier and reserved-name cases above:

field__PRIVATE: str = Field(alias='_PRIVATE')
  • The round-trip holds. envdrift validate parses the .env the same way (recovering non-identifier / non-ASCII keys) and matches schema fields by their alias, so initvalidate passes instead of falsely flagging the aliased fields as missing.

  • Malformed content is not silent. If non-comment content cannot be parsed as a dotenv binding, init warns with its starting line number. The warning does not echo the source text, which may contain a secret. Blank lines, comments, and valid python-dotenv bare bindings are not reported as malformed.

  • Invalid class names fail fast. A --class-name that is not a valid Python identifier (e.g. 123Bad) or is a keyword (e.g. class) errors with a non-zero exit code instead of writing a module that would raise SyntaxError on import.

Sensitive Detection

Variables are marked sensitive if their name matches:

  • *_KEY, *_SECRET, *_TOKEN
  • *_PASSWORD, *_PASS
  • *_CREDENTIAL*, *_API_KEY
  • JWT_*, AUTH_*, PRIVATE_*, *_DSN

Or if their value matches patterns:

  • API keys: sk-*/sk_*, pk-*/pk_* (dash or underscore)
  • GitHub tokens: ghp_*, gho_*
  • Slack tokens: xox* (e.g. xoxb-, xoxp-)
  • AWS keys: AKIA*
  • Database URLs with embedded credentials (postgres://, mysql://, redis://, mongodb://)
  • JWT tokens

Generated Schema Features

The generated schema includes:

model_config = SettingsConfigDict(
    env_file=".env",      # Reads from .env by default
    extra="forbid",       # Rejects unknown variables
)

This provides strict validation out of the box.

Customization

After generating, you should customize:

  1. Add descriptions:
DATABASE_URL: str = Field(
    description="PostgreSQL connection string",
    json_schema_extra={"sensitive": True}
)
  1. Add validators:
@field_validator("PORT")
@classmethod
def validate_port(cls, v: int) -> int:
    if not 1 <= v <= 65535:
        raise ValueError("Invalid port number")
    return v
  1. Split by environment:
class BaseSettings(BaseSettings):
    DATABASE_URL: str = Field(json_schema_extra={"sensitive": True})

class DevelopmentSettings(BaseSettings):
    DEBUG: bool = True

class ProductionSettings(BaseSettings):
    DEBUG: bool = False

Workflow

  1. Generate initial schema:
envdrift init .env --output config/settings.py
  1. Review and customize:
  2. Add descriptions
  3. Adjust types if needed
  4. Add validators
  5. Mark additional sensitive fields

  6. Validate:

envdrift validate .env --schema config.settings:Settings
  1. Set up pre-commit:
envdrift hook --config

See Also

  • validate - Validate against schema
  • hook - Set up pre-commit hooks