Loading#
How Single-Source Loading Works#
graph TD
S["Source"] --> RAW["Read source data"]
RAW --> SK{"skip_invalid_fields?"}
SK -- yes --> DROP["Drop fields that fail\ntype or constraint check"]
SK -- no --> FV{"Source has field validators?"}
DROP --> ROOT
FV -- yes --> FP["Validate fields provided\nby this source"]
FV -- no --> ROOT
FP --> ROOT["Construct dataclass\n+ run all validators\n(root_validators + Annotated on defaults)"]
ROOT --> D["Dataclass"] dature reads your config from a single source, converts every raw value into the type its dataclass field declares, checks it against the rules you attached, and returns a ready, fully-validated instance. Validation happens in distinct tiers; if anything fails, dature gathers every error into one report naming the field, its location in the source, and why it failed.
- Read the source. Raw values are pulled from the source. With
expand_env_vars,${VAR}placeholders are substituted first; secrets are masked in error/debug output permask_secrets/secret_field_names. - Optionally drop invalid values (
skip_invalid_fields). When enabled, each provided value is probed against its field's type and rules, and anything that would fail is quietly dropped so the field can fall back to its dataclass default instead of failing the whole load. The field-validator pass (step 3) is then skipped — remaining fields are counted as already checked. Without the flag, nothing is dropped and failures surface as errors. - Field-level validation. Every value the source provided is coerced to the field's type and checked against that field's declared rules: declared type,
Annotatedconstraints (ranges, patterns, custom predicates), plus any per-sourcevalidators=attached to the source. Fields the source did not provide are left untouched here. - Construct and validate dataclass. Once the values are assembled into the instance,
root_validators=run — checks that span several fields at once (e.g. "start must be before end"). Fields no source supplied take their dataclass default; anyAnnotatedconstraints on those defaults are still enforced. - Result. A fully typed, fully validated config instance — or a single grouped error report of every field that failed across all tiers at once.
For multi-source loading, see Merging.
When a dature.load(...) call fails, the error message tells you which field broke, where in the source it came from, and why. This page walks through the failures you are most likely to hit while wiring up your first config — and one pattern for recovering from them.
All examples share the same schema
Source does not exist#
Wrong path or wrong working directory — the most common first error. dature raises a plain FileNotFoundError before any parsing happens.
Source exists but is broken#
The file is present but the parser can't read it (here: invalid YAML indentation). dature does not swallow parser errors — the underlying exception propagates with the original file and line.
Type mismatch#
The source parses, but a value can't be coerced to the field's annotated type. dature raises a FieldLoadError with the field path, the offending value, a caret pointing at it, and the source location.
| dature.errors.exceptions.DatureConfigError: Config loading errors (1)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [port] invalid literal for int() with base 10: 'not_a_number'
| ├── port: not_a_number
| │ ^^^^^^^^^^^^
| └── FILE '{SOURCES_DIR}type_mismatch.yaml', line 2
+------------------------------------
Required field missing#
A field with no default value is absent from the source. The error points at the file but has no line — there is nothing in the source to highlight.
Multiple errors at once#
dature does not stop at the first error — it keeps going and reports every failed field together as an ExceptionGroup. You fix the config in one pass instead of "fix, rerun, fix, rerun".
| dature.errors.exceptions.DatureConfigError: Config loading errors (2)
+-+---------------- 1 ----------------
| dature.errors.exceptions.FieldLoadError: [port] invalid literal for int() with base 10: 'not_a_number'
| ├── port: not_a_number
| │ ^^^^^^^^^^^^
| └── FILE '{SOURCES_DIR}multiple_errors.yaml', line 2
+---------------- 2 ----------------
| dature.errors.exceptions.FieldLoadError: [debug] Expected bool, got str
| ├── debug: maybe
| │ ^^^^^
| └── FILE '{SOURCES_DIR}multiple_errors.yaml', line 3
+------------------------------------
Recovering: skip an unavailable source#
When merging multiple sources, an absent or malformed one can be skipped so the next source supplies the values. Use skip_if_missing=True for files that may not exist, or skip_if_broken=True for files that exist but may be malformed:
from dataclasses import dataclass
import dature
@dataclass
class Config:
host: str
port: int
debug: bool = False
config = dature.load(
dature.Yaml12Source(
file=SOURCES_DIR / "does_not_exist.yaml",
skip_if_missing=True,
),
dature.Yaml12Source(file=SOURCES_DIR / "fallback.yaml"),
schema=Config,
)
assert config.host == "localhost"
assert config.port == 8080
assert config.debug is False
If every source fails, dature still raises — there is no value to load. See Skipping Sources with Parse Errors and Skipping Missing Sources for the full picture, including per-source overrides and skip_invalid_fields.