omegaconf
OmegaConf is a hierarchical configuration library for Python. You build a config from YAML files, plain dicts, dataclasses, or command-line arguments, and get back one object with the same API regardless of the source. Its two signature features are merging (layer a base config, an experiment config, and CLI overrides into one tree, later sources winning) and interpolation (a value like ${server.port} or ${oc.env:DB_HOST} is resolved lazily when you read it). Back a config with a dataclass and it also type-checks every assignment and merge at runtime. It is the engine underneath Hydra, which is where most of its roughly 10.1M weekly downloads come from: the ML world configures experiments with it.
The de facto config layer of the ML ecosystem via Hydra, with a genuinely good merge-and-interpolate model, but the stable branch has shipped one packaging fix since December 2022 while 2.4.0 has been a dev prerelease for three years. Fine to adopt where Hydra already decides for you; for plain application settings, pydantic-settings is the safer bet.
Use it if
- You layer configuration from several sources: a defaults file, a per-environment file, and command-line overrides, and you want one merge call instead of hand-written dict recursion
- You want values that reference other values: ${paths.root}/data, ${oc.env:API_KEY}, or a computed value via a custom resolver, all resolved lazily at access time
- You want typed config without leaving dataclasses: OmegaConf.structured(MyConfig) gives runtime validation on every assignment and merge, plus IDE autocomplete through duck typing
- You already use Hydra, in which case you are using OmegaConf whether you like it or not, and learning its flags (struct, readonly, MISSING) explains most confusing Hydra behavior
- You want an actively shipping stable channel: 2.3.0 landed in December 2022 and the only stable release since is 2.3.1 in June 2026, a one-line packaging fix. Everything else, including Python 3.13/3.14 support, sits in 2.4.0 dev prereleases (dev0 in August 2023, dev14 in July 2026, still no final)
- Dependency hygiene matters to you: it hard-pins antlr4-python3-runtime==4.9.*, and that pin collides with anything else in your environment that needs a different ANTLR runtime version
- You read config values in hot loops: every attribute access goes through node lookup, type checking, and the interpolation machinery, which is far slower than a plain dict or dataclass. The fix (to_object/to_container once at startup) works, but then you are not really using OmegaConf at runtime
- You are already on pydantic: pydantic-settings does typed settings, env vars, and .env files with a much larger ecosystem, and you avoid a second validation model in the same codebase
- You just need to read one YAML file: PyYAML plus a dataclass covers that without a config framework, an ANTLR grammar, and three flag systems (struct, readonly, missing)
Setup reality
pip install omegaconf is pure Python and quick, but it drags in PyYAML and a hard-pinned antlr4-python3-runtime==4.9.*, which is the classic conflict when another package wants a different ANTLR runtime. Version signals are mixed: PyPI metadata for 2.3.1 still says Python >=3.6 with classifiers up to 3.11, while the README badge advertises 3.10 through 3.14 for the dev line, so on a new interpreter you may need pip install --pre to get the 2.4.0.dev wheels. If Hydra is in the picture, let Hydra pin the OmegaConf version; the two are released in lockstep and mismatches produce weird errors. Docs are versioned per branch, and Google often lands you on the wrong one.
Patterns
Create a config from a dict, YAML, or filecreate-config
from omegaconf import OmegaConf
cfg = OmegaConf.create({"server": {"port": 80}})
cfg = OmegaConf.create("server:\n port: 80\n") # YAML string
cfg = OmegaConf.load("config.yaml") # file or Path
print(cfg.server.port) # attribute style
print(cfg["server"]["port"]) # dict styleAll three return the same DictConfig type, so downstream code never cares where config came from. cfg.get("key", default) works like dict.get; plain attribute access on a missing key raises.
Create a typed config from a dataclasscreate-from-dataclass
from dataclasses import dataclass
from omegaconf import OmegaConf
@dataclass
class ServerConfig:
host: str = "localhost"
port: int = 80
cfg = OmegaConf.structured(ServerConfig)
cfg.port = "8080" # ok, converts to int 8080
cfg.port = "oops" # raises omegaconf.errors.ValidationErrorThe result is a DictConfig duck-typed as ServerConfig, not an instance of it, so isinstance checks fail. Annotate variables as ServerConfig anyway and your IDE autocompletes; use OmegaConf.get_type(cfg) to see the backing class.
Merge base, override, and CLI configsmerge-configs
from omegaconf import OmegaConf
base = OmegaConf.load("base.yaml")
prod = OmegaConf.load("prod.yaml")
cli = OmegaConf.from_cli() # parses sys.argv dotlist args
cfg = OmegaConf.merge(base, prod, cli)Later arguments win, dicts merge recursively, but lists are replaced wholesale, not concatenated. If the first argument is a structured config, the merge is validated against its schema, which is the standard way to type-check raw YAML.
Reference one value from anotherinterpolate-values
cfg = OmegaConf.create({
"paths": {"root": "/data"},
"train_dir": "${paths.root}/train",
"copy_of_paths": "${paths}",
})
print(cfg.train_dir) # /data/trainInterpolations resolve lazily on every access, so changing paths.root later changes train_dir too. Non-string interpolations keep their type (an int stays an int); mixed into a string they concatenate. Cycles are forbidden and blow up at access time, not creation.
Pull environment variables into configread-env-vars
cfg = OmegaConf.create({
"db": {
"host": "${oc.env:DB_HOST,localhost}",
"port": "${oc.decode:${oc.env:DB_PORT,5432}}",
"password": "${oc.env:DB_PASSWORD}",
}
})oc.env always returns a string (defaults are str()-ed too, unless the default is null), so wrap it in oc.decode to get real ints, floats, or bools. The old env: resolver from pre-2.1 tutorials is gone; use oc.env. Missing var without a default raises on access.
Add your own ${func:...} resolverregister-custom-resolver
from omegaconf import OmegaConf
OmegaConf.register_new_resolver("mul", lambda x, y: x * y)
cfg = OmegaConf.create({"gpus": 4, "workers": "${mul:${gpus},2}"})
print(cfg.workers) # 8Registering the same name twice raises ValueError unless replace=True, which bites in pytest where each test re-imports your setup module. Resolvers run on every access; pass use_cache=True if the function is expensive or must return a stable value.
Type-check a YAML file against a dataclass schemavalidate-with-schema
from dataclasses import dataclass
from omegaconf import OmegaConf
@dataclass
class Schema:
host: str = "localhost"
port: int = 80
schema = OmegaConf.structured(Schema)
loaded = OmegaConf.load("config.yaml")
cfg = OmegaConf.merge(schema, loaded) # validates types and keysThe schema must come first; merging schema into data does nothing useful. Wrong types raise ValidationError, unknown keys raise ConfigKeyError because structured configs act like struct mode. This merge-over-schema trick is exactly what Hydra does with its ConfigStore.
Require values with MISSING / ???mark-missing-values
from dataclasses import dataclass
from omegaconf import MISSING, OmegaConf
@dataclass
class Config:
api_key: str = MISSING # same as "???" in YAML
cfg = OmegaConf.structured(Config)
print(OmegaConf.missing_keys(cfg)) # {'api_key'}
cfg.api_key # raises MissingMandatoryValueMISSING is literally the string "???", so a YAML value of ??? means the same thing. The error fires at access, which can be deep inside a training run; call OmegaConf.missing_keys() right after your merges to fail at startup instead.
Make a config immutablefreeze-config-read-only
from omegaconf import OmegaConf, read_write
cfg = OmegaConf.create({"lr": 0.01})
OmegaConf.set_readonly(cfg, True)
cfg.lr = 0.1 # raises omegaconf.ReadonlyConfigError
with read_write(cfg):
cfg.lr = 0.1 # allowed inside the contextThe flag is recursive from the node you set it on. Freeze the config after composition so a stray assignment in library code cannot silently change an experiment; the read_write context is the sanctioned escape hatch, not a hack.
Reject unknown keys with struct modelock-keys-struct-mode
from omegaconf import OmegaConf, open_dict
cfg = OmegaConf.create({"model": {"lr": 0.01}})
OmegaConf.set_struct(cfg, True)
cfg.model.lrr = 0.1 # typo raises ConfigAttributeError
with open_dict(cfg):
cfg.model.dropout = 0.5 # deliberately add a new keyWithout struct mode a typo silently creates a new key and your real setting keeps its default. Hydra turns struct mode on for you, which is why 'Key X is not in struct' is one of the most googled Hydra errors; open_dict is the intended way to add keys anyway.
Override config from the command linecli-dotlist-overrides
from omegaconf import OmegaConf
cfg = OmegaConf.load("config.yaml")
# python train.py server.port=8080 log.level=debug
cfg.merge_with_dotlist(["server.port=8080", "log.level=debug"])
# or: cfg = OmegaConf.merge(cfg, OmegaConf.from_cli())Values are parsed with the YAML-ish grammar, so port=8080 becomes an int and flag=true a bool. from_cli() eats all of sys.argv[1:], which fights argparse; if you mix both, split the args yourself or use from_dotlist on the leftovers.
Convert back to plain dicts or dataclass instancesexport-plain-containers
from omegaconf import OmegaConf
plain = OmegaConf.to_container(cfg, resolve=True) # dicts and lists
obj = OmegaConf.to_object(cfg) # real dataclass instances
OmegaConf.resolve(cfg) # resolve interpolations in place
print(OmegaConf.to_yaml(cfg)) # serializeDo this at the boundary before json.dumps, multiprocessing, or a hot loop: DictConfig is not a dict and attribute access is slow. resolve=True bakes interpolations in; to_object always resolves and also fails on any remaining MISSING values, which makes it a good final validation step.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pydantic-settings | PyPI | Typed application settings from env vars and .env files, especially if pydantic is already in the stack |
| dynaconf | PyPI | Layered settings across toml/yaml/json/env with environment switching (dev/prod) and secrets files |
| hydra-core | PyPI | You want the full application framework on top: config composition, CLI, multirun sweeps. It is built on OmegaConf |
| pyyaml | PyPI | One YAML file, no merging or interpolation needed; parse it and feed a dataclass yourself |