Debugging¶
Your config isn't what you expected. Something got overridden, a merge went wrong, an interpolation didn't resolve. Here's how to figure out what happened.
dracon show: your main tool¶
dracon show loads config files and displays the result at various stages of processing.
Compose only (default)¶
Shows the composed YAML tree after includes and merges, but before construction into Python objects. This is the most common starting point.
Compose + construct + resolve¶
The -c flag constructs Python objects. The -r flag resolves all lazy ${...} interpolations. Together, -c -r gives you the fully resolved config.
Layer multiple files¶
Files are merged left-to-right, just like they would be in your program. This shows you what the final result looks like after layering.
Select a subtree¶
The -s flag extracts a subtree by keypath. Output just the database section. Combine with -c to construct first.
JSON output¶
The -j flag outputs JSON instead of YAML (implies -c). Useful for piping to jq:
Inject context variables¶
The ++name=value syntax sets context variables for ${...} expressions.
Override config values¶
The --path.to.key value syntax overrides a specific config value at a dotted keypath.
Permissive mode¶
The -p flag enables permissive resolution: unresolvable ${...} expressions are left as strings instead of raising errors. Useful when you want to see what resolves with partial context.
Program-aware mode¶
If you have a @dracon_program, dracon show can inspect it directly:
This discovers the program's Pydantic model, shows its defaults, and applies any auto-discovered config files.
dracon show myprogram --full # expand all nested defaults
dracon show myprogram --schema # dump the JSON Schema of the model
dracon show myprogram --diff # show delta from bare defaults
Tracing provenance¶
When you need to know where a value came from, use --trace:
This shows the provenance chain for db.port: which file defined it, which merge overwrote it, which CLI override changed it last.
Example output:
To trace everything at once:
Tracing works on CLI programs too:
On a color terminal, the trace output gets syntax-highlighted with rich.
Symbol introspection¶
When you need to see what's in scope (types, templates, callables, values), use --symbols:
Example output:
!Service(name, port=8080) template infra.yaml:12
!Experiment(name, model) template ml.yaml:8
ResNet type models.py
train_vit(data, epochs=50) callable train.py
For stable JSON output (for tooling and editor integration):
This outputs directly from the symbol table -- no separate catalog. The same runtime model that drives invocation also drives the output.
Self-documenting configs¶
Your config can introspect its own scope using __scope__:
# check that a vocabulary was loaded
!assert ${__scope__.has('Service')}: "infra vocabulary not loaded"
# list all templates in scope
_templates: ${__scope__.names(kind='template')}
# inspect a specific symbol's interface
_service_iface: ${__scope__.interface('Service')}
The __scope__ object is the symbol table itself and exposes names(), has(), interface(), kinds(), exported(), describe(), and to_json().
Variable inspection¶
To see all defined variables (!define, !set_default, context vars) and their sources:
Or from your program:
This prints a table to stderr showing each variable name, its value, and where it was defined.
Error messages¶
Dracon errors include source context and interface information whenever possible.
When a tag can't be resolved, the error shows what symbols are available in scope. When a callable gets wrong arguments, the error shows the expected interface (parameters and their defaults). When a deferred node is constructed with missing runtime inputs, the error shows which !require variables were not provided.
Source location¶
dracon.diagnostics.EvaluationError: Error evaluating expression: name 'typo' is not defined
in config.yaml:14, column 8
keypath: database.host
${typo}
^^^^^
Hint:
Variable 'typo' is not defined in this context
Did you mean: type?
Errors tell you the file, line, column, and the keypath where the problem occurred.
Include traces¶
When an error happens inside an included file, you get the include chain:
CompositionError: Anchor 'missing' not found in document
in db-config.yaml:3
included from config.yaml:7
included from base.yaml:2
Error types¶
| Error | When |
|---|---|
CompositionError |
Something went wrong during composition (includes, merges, instructions) |
EvaluationError |
A ${...} expression failed to evaluate |
UndefinedNameError |
A ${...} expression referenced an undefined variable (subclass of EvaluationError) |
DraconError |
Base class for all Dracon errors; catch this to catch everything |
SchemaError |
JSON Schema or type validation issue |
Python debugging¶
Force-resolve all lazy values¶
from dracon import resolve_all_lazy
config = dracon.load('config.yaml')
# resolve everything, raise on failures:
resolved = resolve_all_lazy(config)
# resolve what you can, leave the rest as strings:
partial = resolve_all_lazy(config, permissive=True)
Inspect composed tree before construction¶
from dracon import compose, construct, DraconLoader
loader = DraconLoader()
composed = loader.compose('config.yaml')
# composed is a CompositionResult; poke at composed.root (YAML node tree)
# then construct when ready:
result = loader.load_node(composed.root)
Inspect a deferred node¶
node = config['deferred_field'] # a DeferredNode
# compose it with context to see the intermediate state:
from dracon import compose
composed = compose(node, context={'key': 'value'})
# composed.root is the YAML node tree, pre-construction
# now construct:
from dracon import construct
result = construct(composed)