Skip to content

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)

dracon show config.yaml

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

dracon show config.yaml -c -r

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

dracon show +base.yaml +prod.yaml

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

dracon show config.yaml -c -s database

The -s flag extracts a subtree by keypath. Output just the database section. Combine with -c to construct first.

JSON output

dracon show config.yaml -c -j

The -j flag outputs JSON instead of YAML (implies -c). Useful for piping to jq:

dracon show config.yaml -c -j -s database | jq '.port'

Inject context variables

dracon show config.yaml ++env=prod ++region=us-east-1

The ++name=value syntax sets context variables for ${...} expressions.

Override config values

dracon show config.yaml --database.port 5433

The --path.to.key value syntax overrides a specific config value at a dotted keypath.

Permissive mode

dracon show config.yaml -c -r -p

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:

dracon show myprogram

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:

dracon show config.yaml --trace db.port

This shows the provenance chain for db.port: which file defined it, which merge overwrote it, which CLI override changed it last.

Example output:

db.port:
  definition   base.yaml:12      5432
  file_layer   prod.yaml:8       5433
  cli_override --db.port=5434    5434

To trace everything at once:

dracon show config.yaml --trace-all

Tracing works on CLI programs too:

myapp +config.yaml --trace db.port

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:

dracon show config.yaml --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):

dracon show config.yaml --symbols-json

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:

dracon show config.yaml --show-vars

Or from your program:

DRACON_SHOW_VARS=1 myapp +config.yaml

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)

Check what variables are in context

loader = DraconLoader(context={'my_var': 42})
# loader.context is the full context dict, including builtins like getenv, Path, etc.
print(list(loader.context.keys()))