2025, Dec 25 19:00
Render a Jinja2 dictionary key with a dash (hyphen) when you can only edit the template
Learn how to render a Jinja2 dictionary key with a dash (like 'b-c') when only the template is editable. Workaround using Undefined, builtins and Context.
Rendering a dictionary key that contains a dash inside a Jinja2 template looks deceptively simple. But if you try to print something like b-c directly, Jinja2 treats it as a subtraction expression, not a variable. When you can’t change the context data or the rendering call and your only lever is the template itself, you need a different approach.
Problem
The goal is to render the value of the key b-c (456) by changing only the template string.
import jinja2
env = jinja2.Environment()
payload = {'a': 123, 'b-c': 456}
tpl = "hello {{ b-c }}"
rendered = env.from_string(tpl, globals=payload).render()
print('result:', rendered)
Expected output:
hello 456
Actual behavior:
jinja2.exceptions.UndefinedError: 'b' is undefined
Switching to env.from_string(tpl).render(payload) doesn’t change the outcome. The name b-c isn’t a valid identifier in an expression, so it’s always parsed as b minus c.
Why this fails
Jinja2 expressions follow Python-like rules. Identifiers can’t contain hyphens, so {{ b-c }} is parsed as a subtraction: look up name b, subtract name c. Since neither b nor c exists in the context, evaluation fails with UndefinedError. You can neither rename the key in the source data nor restructure the render call here, so you can’t fix it at the context boundary.
Solution
The workable path—when only the template can be edited—is to leverage Jinja2 internals to reach the underlying Context and perform a dictionary-style lookup using the exact key 'b-c'. The idea is to intentionally reference an undefined name (for example, _), obtain a jinja2.runtime.Undefined instance, and from one of its Python-level methods access __builtins__.locals() to get the current frame’s local namespace. From there, you can reach the jinja2.runtime.Context object via the name-mangled attribute _Context__self and then fetch the key using square brackets. This avoids the need for a syntactically valid identifier.
import jinja2
env = jinja2.Environment()
payload = {'a': 123, 'b-c': 456}
tpl = "hello {{ _.__init__.__builtins__.locals()._Context__self['b-c'] }}"
rendered = env.from_string(tpl, globals=payload).render()
print('result:', rendered)
Output:
result: hello 456
Why this works
When an undefined name is evaluated in a Jinja2 template, Jinja2 produces an instance of jinja2.runtime.Undefined. That object exposes a Python-implemented __init__ method. From a Python-implemented method you can access the method’s __builtins__, which includes the locals function. Calling locals() inside this evaluation frame yields a namespace dictionary that contains Jinja2’s internal Context object as _Context__self (the double underscore in the original attribute name triggers name mangling). Context supports lookups via __getitem__, so Context['b-c'] returns the value stored under the exact key 'b-c'.
Why you should care
If you handle templates with external or legacy data, keys may not be valid identifiers. When constraints prevent you from changing input keys or the rendering pipeline, understanding how Jinja2 represents undefined names and how its Context can be reached allows you to retrieve such values directly. Otherwise, as noted, the engine will parse b-c as subtraction and fail. In a setting where you can adjust inputs, the straightforward route would be to fix the producer of these keys or normalize them before rendering, but that’s outside the scope when only the template is editable.
Takeaways
Keys containing hyphens are not valid identifiers in Jinja2 expressions and will be parsed as arithmetic. If you’re restricted to modifying only the template, you can still fetch such values by navigating through jinja2.runtime.Undefined to builtins, retrieving the local namespace, obtaining the Context as _Context__self, and finally indexing the original key. This keeps the program logic intact while making the template resilient to non-identifier keys.