envdrift init¶
Generate a Pydantic Settings class from an existing .env file.
Synopsis¶
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.
--class-name, -c¶
Name for the generated Settings class.
--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.
--force, -f¶
Overwrite an existing output file. Without it, init refuses to clobber an
existing (possibly hand-edited) file and errors:
Re-run with --force (or -f) to regenerate over the existing file:
Examples¶
Basic Generation¶
Input .env:
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¶
Custom Class Name¶
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 Pydanticaliasso the field still binds to the original variable:
- 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 aBaseSettings/BaseModelattribute (e.g.schema,dict,json,validate) is renamed with afield_prefix plus a Pydanticalias, so it cannot raise at import or shadow model internals:
- Non-identifier keys are aliased, not dropped. Keys that are not
identifier-style (a leading digit like
2FA_ENABLED, a dash likeMY-DASH-VAR, a dot, or an emoji) are emitted with a sanitized attribute name plus a Pydanticalias, so every key ends up in the schema and still binds to the original environment variable:
-
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-composedCAFÉor the ligaturefile— is aliased instead, preserving the exact original variable name. -
Leading-underscore keys are aliased, not kept bare. A key like
_PRIVATEpassesstr.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 analias— exactly like the non-identifier and reserved-name cases above:
-
The round-trip holds.
envdrift validateparses the.envthe same way (recovering non-identifier / non-ASCII keys) and matches schema fields by their alias, soinit→validatepasses 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,
initwarns 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-namethat 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 raiseSyntaxErroron import.
Sensitive Detection¶
Variables are marked sensitive if their name matches:
*_KEY,*_SECRET,*_TOKEN*_PASSWORD,*_PASS*_CREDENTIAL*,*_API_KEYJWT_*,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:
- Add descriptions:
DATABASE_URL: str = Field(
description="PostgreSQL connection string",
json_schema_extra={"sensitive": True}
)
- 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
- 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¶
- Generate initial schema:
- Review and customize:
- Add descriptions
- Adjust types if needed
- Add validators
-
Mark additional sensitive fields
-
Validate:
- Set up pre-commit: