Defining a config

Subclass Config and declare fields with a type annotation and Field(). Each field carries its default value, validation constraints, and documentation in one place:

from cfx import Config, Field
from typing import Literal

class ProcessingConfig(Config):
    """Configuration for the main processing pipeline."""
    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")

print(cfg) renders the full schema as a table - no extra code required:

cfg = ProcessingConfig()
print(cfg)
ProcessingConfig: Configuration for the main processing pipeline.
Config           | Key        | Value  | Description
-----------------+------------+--------+-------------------------
ProcessingConfig | iterations | 100    | Number of iterations
ProcessingConfig | threshold  | 0.5    | Acceptance threshold
ProcessingConfig | label      | run_01 | Human-readable run label
ProcessingConfig | mode       | fast   | Processing mode
ProcessingConfig | verbose    | False  | Enable verbose logging

In a Jupyter notebook the same table is rendered as HTML automatically via the standard _repr_html_ protocol.

Dot-access and dict-style access are interchangeable. Both route through the same field validation:

cfg.iterations = 200  # dot-access
cfg["mode"] = "thorough"  # dict-style

cfg.threshold = 1.5  # ValueError: Expected value <= 1.0, got 1.5
cfg["mode"] = "turbo"  # ValueError: Expected 'turbo' to be one of ...
cfg["no_such"] = 1  # KeyError: 'no_such'

See Fields for the full annotation → field type mapping and Field Modifiers for callable defaults, static fields, and environment variable support.

Inheritance

A child class inherits every field from its parent and can add new ones or change defaults. Fields from the full MRO are collected automatically:

from cfx import Config, Field
from typing import Literal

class DetailedConfig(ProcessingConfig):
    """Adds output and diagnostics fields to the processing pipeline."""
    confid = "detailed"

    output_dir: str = Field("./results", "Directory for output files")
    max_rows: int = Field(10_000, "Maximum rows per output file", minval=1)

cfg = DetailedConfig()
cfg.iterations   # 100 - inherited from ProcessingConfig
cfg.output_dir   # './results' - new field

A child can also change the default of a parent field by re-declaring it:

class QuietConfig(ProcessingConfig):
    confid = "quiet"
    verbose: bool = Field(True, "Enable verbose logging")  # default changed

QuietConfig().verbose  # True

Composition

Multiple configs can be combined into a single parent using components=:

from cfx import Config, Field

class FormatConfig(Config):
    """Output formatting settings."""
    confid = "format"
    precision: int = Field(6, "Decimal places in numeric output")
    encoding: str = Field("utf-8", "Output file encoding")

class PipelineConfig(Config, components=[ProcessingConfig, FormatConfig]):
    """Groups processing and formatting as nested sub-configs."""
    pass

cfg = PipelineConfig()
cfg.processing.iterations = 500
cfg.format.precision = 3

Each sub-config is fully independent — cfg.processing and cfg.format are separate objects with their own field values. See Config composition for flat merging, serialization, and all other composition options.