Field Definitions¶
Every configuration field in a DotEnvConfig subclass is declared with a type annotation and a Field() descriptor (or the Required sentinel). This page covers all the ways to define fields, set defaults, use aliases, and document your configuration.
For the complete API reference, see Fields API.
The Field() Function¶
Field() returns a FieldInfo instance that the metaclass uses to discover and process your fields. It accepts a positional default argument plus keyword-only constraints and options.
from dotenvmodel import DotEnvConfig, Field
class Config(DotEnvConfig):
# Required field (no default)
database_url: str = Field()
# Optional with default
debug: bool = Field(default=False)
# With validation constraints
port: int = Field(default=8000, ge=1, le=65535)
Parameters¶
| Parameter | Type | Description |
|---|---|---|
default |
Any |
Default value if the env var is not set. Use ... (ellipsis) or omit for required fields. |
default_factory |
Callable[[], Any] \| None |
Callable that returns a default value. Use for mutable defaults (list, dict, set). |
alias |
str \| None |
Alternative environment variable name. Overrides env_prefix. |
description |
str \| None |
Human-readable description shown in describe() output and .env.example files. |
ge |
int \| float \| Decimal \| None |
Greater-than-or-equal (>=) constraint. |
le |
int \| float \| Decimal \| None |
Less-than-or-equal (<=) constraint. |
gt |
int \| float \| Decimal \| None |
Greater-than (>) constraint. |
lt |
int \| float \| Decimal \| None |
Less-than (<) constraint. |
min_length |
int \| None |
Minimum string length (inclusive). For str and SecretStr. |
max_length |
int \| None |
Maximum string length (inclusive). For str and SecretStr. |
regex |
str \| None |
Regular expression pattern the string must match. |
starts_with |
str \| None |
Required string prefix. For str and SecretStr. |
ends_with |
str \| None |
Required string suffix. For str and SecretStr. |
strip |
bool \| str \| re.Pattern \| None |
Strip mode applied to the raw string before coercion. None inherits the class-level strip_strings; True strips whitespace; False disables; a non-empty str is a char set (value.strip(chars)); a compiled pattern removes every match. |
choices |
list[Any] \| None |
List of allowed values (validated after type coercion). |
validator |
Callable[[Any, ValidatorContext], Any] \| None |
Custom hook receiving the coerced, validated value plus context; its return value becomes the final value. |
min_items |
int \| None |
Minimum items in a collection (list, set, tuple, dict). |
max_items |
int \| None |
Maximum items in a collection (list, set, tuple, dict). |
uuid_version |
int \| None |
Required UUID version (1, 3, 4, or 5). |
separator |
str |
Delimiter for parsing list/set/tuple/dict from a string. Default: ",". |
url_unquote |
bool |
Whether to URL-unquote SecretStr values. Default: True. |
resolve_path |
bool |
Whether to resolve Path values (expanduser + resolve). Default: True. |
require_exists |
bool |
Whether a Path field must point to an existing path. Default: False. |
Validation parameters
The validation parameters (ge, le, gt, lt, min_length, max_length, regex, starts_with, ends_with, choices, validator, min_items, max_items, uuid_version) are documented in detail in the Validation guide.
Required Fields¶
There are three equivalent ways to mark a field as required. All produce identical runtime behavior and have no type checker issues.
Pydantic-style ellipsis syntax. This is the recommended approach because it's consistent with Pydantic's API and makes it explicit that you're defining a field.
Field() with no arguments also marks a field as required.
Which should I use?
We recommend Field(...) — it's consistent with Pydantic's API and makes it visually obvious that you're defining a field with no default. Use Required if you prefer the more declarative syntax for fields without constraints.
Defaults¶
Use default for immutable values like str, int, float, bool, and None. Literal mutable defaults (list, dict, set) are safe too — each load() deep-copies them, so instances never share the same object. Literal defaults must be deep-copyable: a default holding state that copy.deepcopy cannot handle (a lock, a socket) raises at load time. A literal default containing a SecretStr (or other un-deepcopyable value) inside a collection raises TypeError on load — use default_factory for those.
Prefer default_factory for mutable defaults: the factory is invoked on every load and its result is handed out as-is — never copied — so there is no per-load deep-copy cost. Construct new values inside the factory: a factory that returns a shared object keeps that object shared across loads. Field() raises a ValueError if you specify both default and default_factory.
Mutable literal defaults are copied, not shared
Field(default=[...]) hands every load() an independent deep copy (pydantic-style smart_deepcopy), so mutating one instance's value cannot leak into other instances or future loads. Immutable defaults are returned as-is at zero cost. default_factory remains the idiomatic choice for mutable defaults because it skips the copy entirely.
str defaults are coerced and validated for non-str field types
A str default for a non-str field type is coerced to the declared type and run through validation at load — e.g. a bool default of 'false' becomes False, a SecretStr default becomes a masked SecretStr, and a PostgresDsn default is validated (a bad scheme raises at load) with its password redacted in repr. Non-str defaults (e.g. int 8000, default_factory=list) and str defaults for str-typed fields are left untouched.
str defaults are interpolation templates
${VAR} and ${VAR:-default} references in a str default resolve at load time against the fully merged .env cascade over the process environment — every merged key is visible to every reference, unlike .env values themselves, which resolve progressively in merged-key order. default_factory results are never interpolated, and resolution re-runs on every load() / reload(). See Variable Interpolation for the full semantics.
Aliases¶
Use alias to read from a different environment variable name than the field name. When alias is set, env_prefix is not applied — the alias is absolute.
class Config(DotEnvConfig):
env_prefix = "APP_"
# Field name: postgres_dsn
# Reads from: DATABASE_URL (alias is absolute, no prefix applied)
postgres_dsn: str = Field(alias="DATABASE_URL")
# Field name: api_token
# Reads from: SECRET_TOKEN (alias is absolute)
api_token: str = Field(alias="SECRET_TOKEN")
# Field name: name
# Reads from: APP_NAME (prefix applied, no alias)
name: str = Field()
Aliases override prefixes
When you set an alias, the env_prefix on the class is not applied to that field. See the Environment Prefixes guide for full details on prefix and alias interaction.
Descriptions¶
Add a description to document your fields. Descriptions appear in describe() output and generated .env.example files, making them valuable for team documentation and onboarding.
class Config(DotEnvConfig):
timeout: float = Field(
default=30.0,
ge=0.1,
description="API request timeout in seconds",
)
port: int = Field(
default=8000,
ge=1,
le=65535,
description="Server port number",
)
database_password: SecretStr = Field(
default=SecretStr("change_me_in_production"),
min_length=8,
description="Database connection password",
)
See the Configuration Documentation guide for examples of how descriptions appear in generated output.
String Stripping¶
The strip parameter cleans raw string values before coercion and validation. It applies to string-like fields: str, SecretStr, their Optional forms, str subclasses (HttpUrl, PostgresDsn, RedisDsn), and Literal["a", "b"] fields whose every member is str.
import re
class Config(DotEnvConfig):
# Whitespace strip: " hello " -> "hello"
name: str = Field(strip=True)
# Char-set strip (str.strip(chars) semantics): ",'hello'," -> "hello"
tag: str = Field(strip=",'\"")
# Regex strip: removes every match, anywhere in the string
key: SecretStr = Field(strip=re.compile(r"^['\"]+|['\"]+$"))
Set the strip_strings class attribute to strip every string-like field by default; per-field strip overrides it:
class Config(DotEnvConfig):
strip_strings: bool = True
name: str = Field() # stripped (inherits class setting)
literal: str = Field(strip=False) # per-field override wins
Stripping is processing, not validation
strip runs even with validate=False, and constraints see the stripped value — min_length checks the final length, and a whitespace-only value for an Optional[str] field strips to "", which maps to None.
Strip runs before URL-unquoting on SecretStr
For SecretStr fields, strip is applied to the raw value before url_unquote, so percent-encoded whitespace (e.g. %20) survives stripping — it is removed while still percent-encoded, then unquoted. Use a re.Pattern strip if you need to strip decoded whitespace.
Use linear-time regex patterns
Both the regex constraint and strip with an re.Pattern run developer-supplied patterns against env values, which can be operator-controlled. Avoid patterns with nested quantifiers (e.g. (a+)+, (a*)*) that can cause catastrophic backtracking (ReDoS). Prefer anchored, linear-time patterns.
Path Options¶
Two parameters control Path field behavior:
from pathlib import Path
class Config(DotEnvConfig):
# Resolved by default (expanduser + resolve)
# ~/logs becomes /home/user/logs
# ./output becomes /cwd/output
log_dir: Path = Field(default=Path("/var/log/app"))
# Keep paths raw (no resolution)
raw_path: Path = Field(resolve_path=False)
# Require the path to exist
config_file: Path = Field(require_exists=True)
See Also¶
- Supported Types — all types you can use with
Field() - Validation — detailed constraint reference
- Environment Prefixes — how
env_prefixinteracts withalias - Fields API — auto-generated API reference