Skip to main content

configgle🤭

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

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 with make(), finalize(), and update(). 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

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
Pure Python (no YAML/strings) 🟡
Typed make()/build() return
Config inheritance 🟡 🟡 🟡
Covariant protocol
Nested finalization
pickle/cloudpickle 🟡 🟡
Auto-generated configs 🟡
GitHub stars -- 10.2k 4.4k 2.3k 2.1k 1.0k 374 21

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

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 string _target_ field to an import path -- the return type is Any. Config composition is done via YAML defaults lists, not class inheritance. Dataclass inheritance works at the schema level. configen is an experimental code-generation tool (v0.9.0.dev8) that produces structured configs from class signatures. Configs survive pickle trivially since _target_ is a string, not a class reference.

Sacred -- Experiment management framework. Config is defined via @ex.config scopes (local variables become config entries) or loaded from YAML/JSON files. Sacred auto-injects config values into captured functions by parameter name (dependency injection), but does not auto-generate configs from function signatures. No typed factory methods, no config inheritance, no pickle support for the experiment/config machinery.

OmegaConf -- YAML-native configuration with a "structured config" mode that accepts @dataclass schemas. Configs are always wrapped in DictConfig proxy objects at runtime (not actual dataclass instances). Supports dataclass inheritance for schema definition. Good pickle support (__getstate__/__setstate__). No factory method (to_object() returns Any), 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 dotted strings. No typed returns, no config inheritance. The docs state "gin-configurable functions are not pickleable," though a 2020 PR added __reduce__ methods that improve support.

ml_collections (Google) -- Dict-like ConfigDict with dot-access, type-checking on mutation, and FieldReference for lazy cross-references between values. Pure Python, no YAML. No factory method or typed instantiation. Pickle works for plain configs, but FieldReference operations that use lambdas internally (.identity(), .to_int()) 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), not inheritance-based -- there are no config subclasses. @auto_config rewrites a factory function's AST to produce a config graph automatically. Full pickle/cloudpickle support.

Confugue -- YAML-based hierarchical configuration. The configure() method instantiates objects from YAML dicts, with the class specified via a !type YAML tag. Returns Any. Partial config inheritance via YAML merge keys (<<: *base). No pickle support, no auto-generation, no protocols.

Citing

If you find our work useful, please consider citing:

@misc{dillon2026configgle,
      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},
}

License

Apache License 2.0

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.4.tar.gz (139.2 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.4-py3-none-any.whl (51.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: configgle-1.3.4.tar.gz
  • Upload date:
  • Size: 139.2 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.4.tar.gz
Algorithm Hash digest
SHA256 cc243b6320f274128fb7698a9b6f089efac79b0fca0aca0150b0013b132f45c8
MD5 b7bec1cc18b1bce5c2eb8fcf6b932f9f
BLAKE2b-256 abc2d341179cb3e7d02cfa194d5552c3fdb9ce4257d15a69cde61dacc4f92527

See more details on using hashes here.

Provenance

The following attestation bundles were made for configgle-1.3.4.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.4-py3-none-any.whl.

File metadata

  • Download URL: configgle-1.3.4-py3-none-any.whl
  • Upload date:
  • Size: 51.8 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.4-py3-none-any.whl
Algorithm Hash digest
SHA256 9e8a9a5bd7803d47372c56ae4545a515d5707aec5ff66e1f0ee10a273b276b98
MD5 5a88f7f2fa3458c6e5e0393cb7a11543
BLAKE2b-256 5c4c0aea1d3603fe3692c2a52daa22d5ee9cb448f49dc88ea3f648abcb539a57

See more details on using hashes here.

Provenance

The following attestation bundles were made for configgle-1.3.4-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

1.3.6

2 files

1.3.5

2 files

This release

1.3.4 This release

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