Concepts: Interpolation Engine¶
Dracon's interpolation feature (${...}) allows embedding dynamic Python expressions within YAML strings. Understanding how this works, especially regarding lazy evaluation and security, is important.
How Interpolation Works¶
Expressions written as ${...} (or $(...) — both syntaxes are identical in behavior) are evaluated lazily at construction time, when the value is accessed in Python after the configuration is loaded and composed.
Mechanism: During YAML composition, Dracon detects interpolation patterns and creates internal LazyInterpolable placeholder objects. When you access config.my_key, if my_key holds a LazyInterpolable, its expression is evaluated at that moment using the captured context and references. If using Dracon's default containers (Mapping, Sequence), this happens automatically. If using standard dict/list, resolution might require manual triggering (e.g., resolve_all_lazy(config)).
References: Expressions can use @ to reference final constructed values of other keys and & to reference nodes during composition (primarily for templating).
Note
Both ${...} and $(...) behave identically — they are both lazy. There is no "immediate" interpolation mode.
The Evaluation Engine: asteval vs. eval¶
Dracon offers two engines for evaluating expressions:
-
asteval(Default & Recommended):- Mechanism: Uses the asteval library.
astevalparses the expression into an Abstract Syntax Tree (AST) and evaluates it in a sandboxed environment. - Safety: Significantly safer than
eval(). It prevents the execution of arbitrary code that could perform dangerous operations like filesystem access (import os; os.remove(...)), network calls, or accessing sensitive system information. It provides a controlled environment with access only to specified symbols (context variables, safe built-ins). - Limitations: Might not support every single Python syntax feature or complex metaclasses, but covers the vast majority of use cases needed for configuration. Also I (Jean) couldn't get it to output a clean traceback that shows where in your code an error occured if an expression fails. (If you know how to do this, please let us know)
- Mechanism: Uses the asteval library.
-
eval(Use with Extreme Caution):- Mechanism: Uses Python's built-in
eval()function. - Safety: Well,
eval()can execute any arbitrary Python code provided in the expression string. If your YAML files come from untrusted sources or could be manipulated, usingeval()opens a significant security vulnerability. Malicious code could be injected and executed with the permissions of your application. - Use Case: Only suitable if you have absolute trust in the source and integrity of your configuration files and require features not supported by
asteval. Can give nice tracebacks if an expression fails.
Warning
Security Risk: Choosing
evalas the interpolation engine can lead to severe security vulnerabilities if the configuration files are not fully trusted. Avoid usingeval()unless you are certain of the source and content of the configuration files. Useevalat your own risk. - Mechanism: Uses Python's built-in
Choosing the Engine:
from dracon import DraconLoader
# Default, safe engine
loader_safe = DraconLoader() # interpolation_engine='asteval'
# Use Python's raw eval (DANGEROUS if config is untrusted, or if you're not careful)
loader_raw = DraconLoader(interpolation_engine='eval')
# Disable interpolation entirely
loader_no_interp = DraconLoader(enable_interpolation=False)
There's also a DRACON_EVAL_ENGINE environment variable that can be set to asteval or eval to control the default engine.
Recommendation: Stick with the default asteval engine unless you have a very specific, well-understood need for eval and fully control the configuration sources.
Permissive Evaluation (Two-Phase Resolution)¶
By default, if an expression references an undefined variable, Dracon raises an EvaluationError. This is usually what you want — typos get caught early.
But sometimes you need two-phase resolution: some ${...} variables are known at config load time, others are injected later at runtime. Without permissive mode, you'd have to wrap every runtime-dependent expression in !deferred, even when the expression is a simple string template.
With permissive=True, Dracon resolves what it can and leaves the rest as literal ${...} strings:
from dracon.interpolation import evaluate_expression
# phase 1: only 'prefix' is known
result = evaluate_expression(
"${prefix + '_' + version}",
context={'prefix': 'deploy'},
permissive=True,
)
# result: "${\'deploy_\' + version}" (partially folded)
# phase 2: 'version' is now available
final = evaluate_expression(
result,
context={'prefix': 'deploy', 'version': '1.2.3'},
)
# final: "deploy_1.2.3"
How it works¶
When permissive=True and an expression hits an undefined name:
- Dracon catches the
UndefinedNameError(a subclass ofEvaluationError) - It runs constant folding on the AST — substituting known variables and simplifying sub-expressions that can be computed (arithmetic, string concat, comparisons, boolean ops,
if/elsewith known conditions) - The result is either a
PartiallyResolvedexpression (some progress made) or left completely unchanged
Multi-interpolation strings work naturally — known parts resolve, unknown parts stay:
result = evaluate_expression(
"${a} and ${b}",
context={'a': 1},
permissive=True,
)
# result: "1 and ${b}"
Where to use permissive¶
The permissive parameter is available on:
evaluate_expression(..., permissive=True)— the core functiondo_safe_eval(..., permissive=True)— low-level evalInterpolableNode.evaluate(..., permissive=True)— node-levelLazyInterpolable(value, permissive=True)— lazy wrapperresolve_all_lazy(obj, permissive=True)— bulk resolutionDracontainer.resolve_all_lazy(permissive=True)— container method
Permissive vs. !deferred¶
Both handle "not yet available" values, but they solve different problems:
| Permissive eval | !deferred |
|
|---|---|---|
| Scope | Individual ${...} expressions |
Entire YAML subtrees |
| Result | String with unresolved ${...} markers |
DeferredNode object |
| Use case | Template strings with mixed known/unknown vars | Complex objects that need runtime construction |
| Resolution | Call evaluate_expression again with full context |
Call construct(node, context={...}) |
Use permissive eval when you have simple template strings where some variables arrive later. Use !deferred when entire object branches need runtime construction with Python objects (models, connections, etc.).
Context and Symbol Availability¶
Expressions have access to:
- Variables provided via
DraconLoader(context=...). - Variables defined via
!define/!set_default. - Dracon's default context functions:
getenv,getcwd,listdir,join,basename,dirname,expanduser,now,construct. - File-specific context variables:
DIR,FILE,FILE_PATH,FILE_STEM,FILE_EXT,FILE_LOAD_TIME,FILE_SIZE. numpyasnp(when installed).- Python built-ins allowed by the engine (
astevalhas a curated list;evalhas all). - Special symbols for references (
@,&handled internally before evaluation). - Helper functions or classes added to the context.