Hint 1: The name is data, not code
A value should never be concatenated into template source. Put a {{ name }} placeholder in a fixed template and pass the name to render() as a variable.
Hint 1 unlocks after your first failed run.
Hint 2: Shrink the context
Autoescaping escapes rendered variables, not pasted text, so building the template from the name defeats it. Render only the variables you mean to show, and keep the account object and its reset token out of the context.
Hint 2 unlocks after 2 failed runs.
Reveal the reference patch and explanation
from jinja2 import Environment
def render_welcome(display_name, account):
env = Environment(autoescape=True)
template = env.from_string("<p>Welcome back, {{ name }}! You are on the {{ plan }} plan.</p>")
return template.render(name=display_name, plan=account["plan"])A fixed template `<p>Welcome back, {{ name }}! You are on the {{ plan }} plan.</p>` rendered with name=display_name and plan="Pro" shows every input as text: Ana and María López appear unchanged, <b>bob</b> becomes <b>bob</b>, O’Brien & Co becomes O'Brien & Co, and {{ 7*7 }} and {% for … %} stay literal. Because the account object is not in the context, {{ account.reset_token }} cannot read the token even though it is a legitimate-looking expression. SandboxedEnvironment exists for the rare case where a user must supply the template itself; it restricts attribute access and operators, but it does not stop a user-supplied template from reading context data or producing huge output, so it is not a substitute for keeping user input out of the template source.
Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.
The reference patch unlocks after you run code you have edited.
Inspect the actual test harness
The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.
Download the grader, python/cases.mjsHow the browser runs your Python
Your code runs in CPython 3.14.2 compiled to WebAssembly (Pyodide 314.0.7), in a Web Worker of its own. The test cases and the pass or fail verdict run in a second worker, which only sends your functions their inputs and reads back the values they return, so changing built-ins or the harness inside Python cannot change the result. All the cases together have a 8-second limit.
The first run downloads about 6.4 MB from this site, which your browser then keeps; later runs start in a second or two. Other pages never load it.
Available: the Python standard library, including sqlite3, ipaddress, pathlib, os.path and urllib.parse, and print(), whose output appears with your results. This lab also loads MarkupSafe 3.0.3 and Jinja2 3.1.6: the exact wheels pinned by Pyodide 314.0.7, served from this site and checked against the lock file’s SHA-256 digests.
Not available: network access, subprocesses and threads, and other packages from PyPI. The lab download runs the same cases with your local python3 (3.12 or newer), once Jinja2==3.1.6 is installed (its README shows how).
This lab renders one banner. SandboxedEnvironment is for genuinely user-authored templates and must be paired with resource limits (a template can still produce enormous output) and a minimal, side-effect-free context; the Jinja docs state plainly that the sandbox alone is not a complete security solution. Autoescaping covers HTML or XML output only — JavaScript, CSS, URLs and other sinks need their own encoding.
Your next task: Where in your app does a template, email or label get built by string concatenation from user or tenant data, and could that input reach the template engine as syntax rather than as a value?
Jinja sandbox and security considerations
Related guide: Keep untrusted values out of executable syntax