Skip to main content

configgle🤭

PyPI version CI Python 3.12+ License: Apache-2.0 Discord

Type-safe hierarchical experiment configuration using pure Python dataclass factories and dependency injection.

Quick Start

# Mac:
#   # Required for quick install.
#   brew install uv

# Ubuntu/Debian:
#   # Required for quick install.
#   sudo apt-get install -y curl
#   curl -LsSf https://astral.sh/uv/install.sh | sh

uv add configgle

# Alternatively: python -m pip install configgle

Hierarchical experiment configuration using pure Python dataclasses with typed factory methods, covariant protocols, inheritance support, and tooling for pretty printing, autodecorating, updating, and semi-deep copying.

Example

from configgle import Fig


class Model:
    class Config(Fig):
        hidden_size: int = 256
        num_layers: int = 4

    def __init__(self, config: Config):
        self.config = config


# Create and modify config
cfg = Model.Config()
cfg.hidden_size = 512

# Instantiate the parent class
model = cfg.make()
print(model.config)
assert isinstance(model, Model)

Configs are plain mutable dataclasses, so experiments are just functions that tweak a baseline:

def exp000() -> Model.Config:
    return Model.Config()


def exp001() -> Model.Config:
    cfg = exp000()
    cfg.hidden_size = 512
    cfg.num_layers = 8
    return cfg

Or use @autofig to auto-generate the Config from __init__:

from configgle import autofig
from torch import nn


@autofig
class Model(nn.Module):
    def __init__(self, hidden_size: int = 256, num_layers: int = 4):
        super().__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers


# Config is auto-generated from __init__ signature
model = Model.Config(hidden_size=512).make()
print(model.hidden_size)  # 512

Features

Type-safe make()

tl;dr: Both ty and basedpyright are first-class supported. Unfortunately neither is perfect:

ty basedpyright
Bare Fig infers parent type ❌ (Any fallback)
Explicit Fig["Parent"] specifies parent type
Inheritance infers parent type
Explicit Makes["Child"] narrows inferred inherited parent type
Inherited Config child fields ✅ (workaround for #3282)
@autofig .Config access ✅ (fixed #143)

Details:

When Config is defined as a nested class, MakerMeta.__get__ uses the descriptor protocol to infer the parent class automatically. The return type of __get__ is Intersection[type[Config], type[Makeable[Parent]]], so make() knows the exact return type with zero annotation effort:

class Model:
    class Config(Fig):
        hidden_size: int = 256

    def __init__(self, config: Config):
        self.hidden_size = config.hidden_size


model = Model.Config(hidden_size=512).make()  # inferred as Model

Type checkers that support Intersection (like ty) resolve this fully -- bare Fig is all you need. For type checkers that don't yet support Intersection (like basedpyright), parameterize with the parent class name to give the checker the same information explicitly:

class Model:
    class Config(Fig["Model"]):  # explicit type parameter only for basedpyright
        hidden_size: int = 256

    def __init__(self, config: Config):
        self.hidden_size = config.hidden_size


model: Model = Model.Config(hidden_size=512).make()  # returns Model, not object

Without ["Model"], non-ty checkers fall back to Any (so attribute access works without typecheck suppressions).

ty gets full inference from Intersection -- bare Fig and inherited configs just work. basedpyright doesn't support Intersection yet, so it needs explicit Fig["Parent"] and Makes["Child"] annotations. ty honors class decorator return types (#143, fixed in 0.0.49), so @autofig-decorated classes resolve .Config with no suppression on both checkers. When Intersection lands in the type spec, Makes becomes unnecessary and both checkers will infer everything from bare Fig.

Inheritance with Makes (only for basedpyright)

When a child class inherits a parent's Config, the make() return type would normally be the parent. Use Makes to re-bind it (again, only needed for basedpyright):

from configgle import Makes


class Animal:
    class Config(Fig["Animal"]):
        name: str = "animal"

    def __init__(self, config: Config):
        self.config = config
        self.name = config.name


class Dog(Animal):
    class Config(Makes["Dog"], Animal.Config):
        breed: str = "mutt"

    def __init__(self, config: Config):
        super().__init__(config)
        self.breed = config.breed


dog: Dog = Dog.Config(name="Rex", breed="labrador").make()  # returns Dog, not Animal

Makes contributes nothing to the MRO at runtime -- it exists purely for the type checker (see the type checker table above). When Intersection lands, Makes becomes unnecessary.

Covariant Makeable protocol

Makeable[T] is a covariant protocol satisfied by any Fig, InlineConfig, or custom class exposing make(), finalize(), update(), plus the _finalized and parent_class members (Maker and InlineConfig provide all five). Because it's covariant, Makeable[Dog] is assignable to Makeable[Animal]:

from configgle import Makeable


def train(config: Makeable[Animal]) -> Animal:
    return config.make()


# All valid:
train(Animal.Config())
train(Dog.Config(breed="poodle"))

This makes it easy to write functions that accept any config for a class hierarchy without losing type information.

Nested config finalization -- pre / super / post

Override finalize() to compute derived fields. super().finalize() cascades into the child configs, so it splits the method into a pre phase (before children finalize -- push values down) and a post phase (after -- derive values up):

from configgle import Configurable  # Just an alias to Makeable.
from dataclasses import field


class Encoder:
    class Config(Fig):
        c_in: int = 256
        mlp: Configurable[nn.Module] = field(default_factory=MLP.Config)

        def finalize(self) -> Self:
            self.mlp.c_in = self.c_in  # pre: push down into the child
            self = super().finalize()  # children finalize here
            self.out = self.mlp.out  # post: derive up from the child
            return self

Inject into a child before super() (so it finalizes with the value); derive from a child after (so you read its finalized result). Pushdown is the common case, so super() is usually last -- but it need not be.

finalize() mutates in place; the copy that protects the original happens once at the make() / pprint boundary (copy_tree().finalize()), so a config is finalized exactly once on a fresh tree (never re-finalized) and the config passed to make() is left untouched.

update() for bulk mutation

Configs support bulk updates from another config or keyword arguments:

cfg = Model.Config(hidden_size=256)
cfg.update(hidden_size=512, num_layers=8)

# Or copy from another config (kwargs take precedence):
cfg.update(other_cfg, num_layers=12)

InlineConfig / PartialConfig

InlineConfig wraps an arbitrary callable and its arguments into a config object with deferred execution. Use it for classes where all constructor arguments are known at config time:

from configgle import InlineConfig
import torch.nn as nn

cfg = InlineConfig(nn.Linear, in_features=256, out_features=128, bias=False)
cfg.out_features = 64  # attribute-style access to kwargs
layer = cfg.make()  # calls nn.Linear(in_features=256, out_features=64, bias=False)
y = layer(x)  # use the constructed module

PartialConfig is shorthand for InlineConfig(functools.partial, fn, ...) -- use it for functions where some arguments aren't known at config time:

from configgle import PartialConfig
import torch.nn.functional as F

cfg = PartialConfig(F.cross_entropy, label_smoothing=0.1)
loss_fn = cfg.make()  # returns functools.partial(F.cross_entropy, label_smoothing=0.1)
loss = loss_fn(
    logits, targets
)  # calls F.cross_entropy(logits, targets, label_smoothing=0.1)

Nested configs in args/kwargs are finalized and make()-d recursively, so both compose naturally with Fig configs.

copy_tree()

copy_tree() is a "semi-deep" copy: nested configs and mutable containers holding configs are duplicated, while leaf values (primitives, tensors, loggers) are aliased. make() and pprint apply it before finalizing so the source config stays pristine. Reach for it directly to finalize a copy without touching the source:

finalized = cfg.copy_tree().finalize()  # cfg unchanged

pprint / pformat

Config-aware pretty printing that hides default values, auto-finalizes before printing, and scrubs memory addresses:

from configgle import Configurable, Fig, pformat


class MLP:
    class Config(Fig):
        c_in: int = 256
        c_out: int = 256
        num_layers: int = 2
        dropout: float = 0.1
        use_bias: bool = True

    def __init__(self, config: Config): ...


class Model:
    class Config(Fig):
        hidden_size: int = 256
        num_layers: int = 4
        mlp: Configurable[nn.Module] = field(default_factory=MLP.Config)
        output_mlp: Configurable[nn.Module] = field(default_factory=MLP.Config)

    def __init__(self, config: Config): ...


def exp001():
    cfg = Model.Config()
    cfg.hidden_size = 512
    cfg.num_layers = 12
    cfg.mlp.c_in = 512
    cfg.mlp.c_out = 1024
    cfg.mlp.num_layers = 4
    cfg.mlp.dropout = 0.2
    cfg.mlp.use_bias = False
    cfg.output_mlp.c_in = 1024
    cfg.output_mlp.c_out = 256
    cfg.output_mlp.dropout = 0.3
    return cfg


print(pformat(exp001(), continuation_pipe=0))
# Model.Config(
#    hidden_size=512,
#    num_layers=12,
#    mlp=MLP.Config(
#    │       c_in=512,
#    │       c_out=1_024,
#    │       num_layers=4,
#    │       dropout=0.2,
#    │       use_bias=False
#    ),
#    output_mlp=MLP.Config(c_in=1_024, dropout=0.3)
# )

Default values are hidden, continuation pipes show where nested blocks belong, large numbers get underscores (1_024), and short sub-configs collapse onto one line. pformat and pprint are also available as methods on any Fig config:

cfg = exp001()
cfg.pprint()  # prints to stdout
s = cfg.pformat()  # returns string

CLI overrides

apply_overrides edits a config from PATH=VALUE strings. Dotted paths reach into nested configs, every hop is checked against the node's declared fields (so a typo raises instead of silently creating an attribute), and the value is cast to the field's declared type:

from configgle import apply_overrides

cfg = Model.Config()
apply_overrides(cfg, ["hidden_size=512", "mlp.dropout=0.2"])

configgle/launch.py wires that to argparse, so any factory function returning a config is runnable as-is:

# myproject/experiments.py
def baseline() -> Makeable[Trainer]:
    return Trainer.Config()
python -m configgle myproject.experiments.baseline --override mlp.dropout=0.2

The launcher is deliberately small -- it exists to make --override usable out of the box and to show the pattern. A real project usually wants its own entry point (hardware logging, distributed setup, run naming); build it on resolve_config and apply_overrides rather than copying the file.

Dataclass base

Dataclass provides the auto-dataclass metaclass (with the same opinionated defaults as Fig: kw_only=True, slots=True, etc.) but without Maker or make(). Use it for plain data objects that don't need the factory pattern.

@autofig for zero-boilerplate configs

When you don't need a hand-written Config, @autofig generates one from __init__ (see Example above).

Pickling and cloudpickle

Configs are fully compatible with pickle and cloudpickle, including the parent class reference. This is important for distributed workflows (e.g., sending configs across processes):

import cloudpickle, pickle

cfg = Model.Config(hidden_size=512)
cfg_ = pickle.loads(cloudpickle.dumps(cfg))
model = cfg_.make()  # parent_class is preserved

Comparison

configgle Hydra Sacred OmegaConf Gin ml_collections Fiddle Confugue
Python-based 🟡 🟡 🟡 🟡
YAML-based
CLI overrides 🟡 🟡
Sweeps / multirun 🟡
Typed make()/build() return
Derived fields 🟡 🟡 🟡 🟡
Config from signature 🟡
JSON round-trip (typed) 🟡 🟡 🟡 🟡
pickle/cloudpickle 🟡 🟡 🟡 🟡 🟡
Active 🟡 🟡 🟡 🟡

✅ = yes, 🟡 = partial, ❌ = no. Corrections welcome -- open a PR.

Row notes, since several are easy to read the wrong way:

  • Python-based / YAML-based are independent axes, not opposites. Gin is neither -- it has its own .gin DSL. configgle is Python-only by design, so it loses the YAML row outright: if your collaborators edit configs without touching Python, that is a real reason to pick Hydra or OmegaConf instead.
  • Sweeps / multirun means launching many runs from one command. Hydra's --multirun is the only first-class implementation; configgle has none (write a loop over config functions). Fiddle's DEFINE_fiddle_sweep is on main only, absent from the latest PyPI release, and emits configs without running them.
  • Derived fields means computing one field from others. configgle's finalize() is a user-overridable hook that cascades through child configs; the 🟡 libraries re-run __post_init__ (Hydra, OmegaConf) or re-execute config scopes (Sacred) at a conversion boundary, and ml_collections has lazy FieldReference.
  • JSON round-trip (typed) means dump to JSON and load back into live typed objects. Every 🟡 reloads to an untyped dict: ml_collections has to_json() but no from_json, and OmegaConf's reload yields OmegaConf.get_type(...) == dict.
  • Active is measured from the last PyPI release, not repo activity. 🟡 marks a library whose repo still moves but whose last release is aging (Gin 0.5.0, 2021-11-03; Fiddle 0.3.0, 2024-04-09; Sacred 0.8.7, 2024-11-26; ml_collections 1.1.0, 2025-04-17). Confugue's last release was 2020-04-22 and its last commit 2021-09-13.
How each library works

Hydra (Meta) -- YAML-centric with optional "structured configs" (Python dataclasses registered in a ConfigStore). Instantiation uses hydra.utils.instantiate(), which resolves a _target_ field -- typically a string import path, though a class object is also accepted -- and returns Any. Composition is done via defaults lists (usually YAML, optionally a defaults field on a dataclass), not class inheritance; dataclass inheritance works at the schema level. configen is an experimental code-generation tool (latest release v0.9.0.dev8) that produces structured configs from class signatures. Its --multirun sweeper is the most complete in this table.

Sacred -- Experiment management framework. Config is defined via @ex.config scopes (local variables become config entries) or loaded from YAML/JSON/pickle files, and overridden on the command line with with 'a.b=5'. Sacred auto-injects config values into captured functions by parameter name (dependency injection), but does not auto-generate configs from function signatures. Reuse is by composition -- ingredients nest, and stacked config scopes override a reusable ingredient's defaults -- rather than class inheritance. No typed factory methods; Experiment objects are not picklable, though config files may be pickles.

OmegaConf -- YAML-native configuration with a "structured config" mode that accepts @dataclass schemas. Configs are DictConfig proxy objects at runtime, not dataclass instances; OmegaConf.to_object() converts them back into real instances (re-running __post_init__ recursively as it goes). Supports dataclass inheritance for schema definition. Good pickle support (__getstate__/__setstate__). to_object() acts as a factory but is typed Any, so callers lose static types. OmegaConf.from_cli() parses a dotlist but leaves the merge to you. No auto-generation, no protocols.

Gin (Google) -- Global string-based registry. You decorate functions with @gin.configurable and bind parameters via .gin files or gin.bind_parameter('fn.param', val). There are no config objects -- parameter values live in a global dict keyed by (scope, selector). No typed returns, and no config-object inheritance (though .gin files compose via include, and scoped bindings inherit from the root scope). The docs still state "Gin-configurable functions are not pickleable," but as of 2021 Gin wraps the metaclass __call__ so that instances of configurable classes pickle fine; a community PR proposing __reduce__ was closed unmerged.

ml_collections (Google) -- Dict-like ConfigDict with dot-access, type-checking on mutation, and FieldReference for lazy cross-references between values. Config files are Python, not YAML (the library itself depends on PyYAML for printing). config_flags gives --config.foo.bar=3e-4 overrides for free. No factory method or typed instantiation. Pickle works for plain configs, but FieldReference operations that build lambdas internally (.identity() -- used by get_oneway_ref() -- and the .to_int()/.to_float()/.to_str() casts) fail with standard pickle; cloudpickle handles them.

Fiddle (Google) -- Python-first. You build config graphs with fdl.Config[MyClass] objects and call fdl.build() to instantiate them. build(Config[T]) -> T is typed via @overload. Config modification is functional (fdl.copy_with) -- you don't subclass a config to override values. @auto_config rewrites a factory function's AST to produce a config graph automatically. Full pickle/cloudpickle support, and serialization.dump_json/load_json round-trip a config graph faithfully (though that module lives under _src.experimental).

Confugue -- YAML-based hierarchical configuration. The configure() method instantiates objects from YAML dicts, with the class overridden via a class: key whose value uses PyYAML's !!python/name: tag. Returns Any. Partial config inheritance via YAML merge keys (<<: *base). No CLI, no auto-generation, no protocols. Pickling is undocumented and untested -- configured instances do pickle, but bind() results do not. Unmaintained: last release 2020-04-22, last commit 2021-09-13.

Citing

If you find our work useful, please consider citing:

@misc{rekursivai2026configgle,
      title={Configgle - Type-safe hierarchical experiment configuration using pure Python dataclass factories and dependency injection.},
      author={Joshua V. Dillon},
      year={2026},
      howpublished={Github},
      url={https://github.com/rekursiv-ai/configgle},
}

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

configgle-1.3.6.tar.gz (165.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

configgle-1.3.6-py3-none-any.whl (66.9 kB view details)

Uploaded Python 3

File details

Details for the file configgle-1.3.6.tar.gz.

File metadata

  • Download URL: configgle-1.3.6.tar.gz
  • Upload date:
  • Size: 165.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for configgle-1.3.6.tar.gz
Algorithm Hash digest
SHA256 b754081d21905dcf2db57cc981a4a32f03cf92445d3775476fc46ab7d36bd3de
MD5 c1ce3c59c7e088f1f1072ca32552ddcb
BLAKE2b-256 62f92d6e1e44573a9b5f05653b18272e87e940b25c2a74ffc7ba4d5d53435786

See more details on using hashes here.

Provenance

The following attestation bundles were made for configgle-1.3.6.tar.gz:

Publisher: publish-pypi.yml on rekursiv-ai/configgle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file configgle-1.3.6-py3-none-any.whl.

File metadata

  • Download URL: configgle-1.3.6-py3-none-any.whl
  • Upload date:
  • Size: 66.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for configgle-1.3.6-py3-none-any.whl
Algorithm Hash digest
SHA256 8918aff2acc3bf6f6f299b3c4ad221155ef6ac913f7dad3f9ed3e7b3040591c8
MD5 4b04aead723b7be29af1266466cb95cc
BLAKE2b-256 1aa22d4bacf9294c1609468242696d39a1d70f9f1d898bed73b3d6c005a559b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for configgle-1.3.6-py3-none-any.whl:

Publisher: publish-pypi.yml on rekursiv-ai/configgle

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.4.0

2 files

1.3.7

2 files

This release

1.3.6 This release

2 files

1.3.5

2 files

1.3.4

2 files

1.3.3

2 files

1.3.2

2 files

1.3.1

2 files

1.3.0

2 files

1.2.4

2 files

1.2.3

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.15

2 files

1.1.14

2 files

1.1.13

2 files

1.1.12

2 files

1.1.11

2 files

1.1.10

2 files

1.1.9

2 files

1.1.8

2 files

1.1.7

2 files

1.1.6

2 files

1.1.5

2 files

1.1.4

2 files

1.1.3

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page