Fields¶
Fields can broadly be categorized into three groups:
based on field declaration style,
based on field modifiers and
as field views
Based on field declaration styles we have three types of fields:
Inferred fields - the field type is inferred from the type-annotation:
field: int = Field(100, "a field")Explicit fields - a concrete
cfx.typesused directly:threshold = Float(0.5, "a field")Custom fields - user provided implementation of
ConfigField: See Custom Fields.
Based on field modifiers we have:
computed fields - fields invoking a function to calculate a return value
static fields - immutable fields set once at definition time
env fields - fields deriving their value from an environment variable
There is nothing special separating these types. This taxonomy exists purely for context throughout the text. All of these types of fields share the common definition and validation pipeline:
Inferred field -> resolves annotation to -> Explicit field -> subclasses -> ConfigField <- subclassed by <- Custom field
The distinction matters in practice because each style makes a different tradeoff:
Inferred fields support static type checking, but can not re-define normalization, validation, serialization or CLI behaviour.
Explicit fields can override this behaviour, but can’t be statically type-checked.
Custom fields exist to cover those cases that can not be expressed by modifying an explicit type: non-standard coercion, domain-specific validation, custom serialization, or bespoke CLI behaviour. See Custom Fields.
The third group — field views — are the odd ones out. Alias and
Mirror look and behave like fields but are implemented against
Config or ConfigView directly. This is completely
transparent to you and their application makes their special nature and function
obvious. See Views.
In this and the following sections the details and important information related to these fields are discussed:
See Fields (this document) for the inferred vs explicit fields
See Custom Fields for details on custom fields
See Computed fields for details on computed fields (see also Sharp edges for edge-cases)
See Views for more details (and Cross-field validation for more details on operations over a schema of fields)
Explicit field types¶
These are the concrete types that inferred fields resolve to at class-definition time. Using these fields directly does not support static type checking — the value type is not visible to the type checker. Therefore it’s more recommended to rely on type annotated fields described in Inferred fields.
However, when annotation inference is not enough — custom validation logic, domain-specific coercion, or constraint combinations not expressible via annotations - import, inherit, overload etc. and use the concrete field classes directly:
from cfx import Config
from cfx.types import Float, Options, String
class ProcessingConfig(Config):
threshold = Float(0.5, "Acceptance threshold", minval=0.0, maxval=1.0)
mode = Options(("fast", "balanced"), "Processing mode")
label = String("run_01", "Human-readable run label", maxsize=64)
Note
Options and MultiOptions have a different
constructor order: (options, doc, default_value=None, ...) — the
allowed choices come first. When using Field() this is handled
automatically.
Type |
Description |
|---|---|
Base class. No validation; accepts any value. Use when you intend to subclass it (see Custom Fields). |
|
Explicit escape hatch. No validation; signals to readers that the absence of constraints is intentional. |
|
|
|
Integer. Optional |
|
Float. Also accepts |
|
Either |
|
Text string. Optional |
|
One value from a fixed set. Default is the first option unless
|
|
A |
|
A |
|
An |
|
A |
|
A list. Optional |
|
An untyped dict. Useful for free-form sub-structure that is too loose
to warrant a nested |
|
A |
|
A |
|
A |
Inferred fields¶
The recommended way to declare config fields is with a type annotation and
Field():
from cfx import Config, Field
from typing import Literal
class ProcessingConfig(Config):
confid = "processing"
iterations: int = Field(100, "Number of iterations", minval=1)
threshold: float = Field(0.5, "Acceptance threshold", minval=0.0, maxval=1.0)
label: str = Field("run_01", "Human-readable run label")
mode: Literal["fast", "balanced", "thorough"] = Field("fast", "Processing mode")
verbose: bool = Field(False, "Enable verbose logging")
The concrete field type is inferred from the annotation at class-definition
time. Any constraint keyword accepted by the underlying type can be passed
directly to Field() (e.g. minval=, maxval=, env=,
static=).
Static and runtime validation¶
Adding a type annotation enables static type checking: mypy, pyright, and
IDEs can verify that only the right type is assigned to the field without
running any code. All field types — inferred, explicit, and custom — perform
runtime validation at every assignment, enforcing constraints such as
minval= or option membership. Value constraints (e.g. “must be > 0”) are
inherently runtime concerns and are not expected to be checked statically.
Explicit field types give dynamic checking but lose the static annotation, since type checkers see the descriptor rather than the value type.
Mapping to field types¶
At definition time, the type annotation is parsed, alongside any required validation data, and, under the hood, this lookup mapping is used to construct the matching explicit field type.
Annotation |
Resolved type |
Notes |
|---|---|---|
|
||
|
Optional |
|
|
Optional |
|
|
Optional |
|
|
Optional |
|
|
Default must be one of the literals |
|
|
||
|
Optional |
|
|
Validates |
|
|
|
|
|
||
|
||
|
||
|
Rejects bare |
|
|
Untyped free-form dict |
|
|
No validation; signals intentional opt-out |
|
|
Same as |
Cannot be set on instances |