To wrap a block in Jinja2, override the block in a child template and call {{ super() }} inside your wrapper markup, or define a macro that takes a body through {% call %} and renders {{ caller() }} where the wrapped content should go. Both approaches let you put a container, a card, a conditional div or a filter around existing content without copying that content. Which one you pick depends on whether the thing you are wrapping is a template inheritance block, a reusable snippet, or a piece of text that just needs transforming.
{{ super() }}: {% block content %}<div class="card">{{ super() }}</div>{% endblock %}. For reusable wrappers, write a macro and invoke it with {% call wrapper() %} ... {% endcall %}, rendering the body with {{ caller() }}. For text transforms, use {% filter upper %} ... {% endfilter %}. For an optional wrapper, capture the body once with {% set body %} ... {% endset %} and print it inside or outside the wrapper.The rest of this guide walks through each pattern with working templates you can drop into Flask, Django (with the Jinja2 backend), Ansible or any other project that renders Jinja2. It also covers include with context, how wrapping interacts with multiple levels of inheritance, and a troubleshooting section for the errors people hit most often, like super() being undefined or caller missing.
Wrapping an inherited block with super()
Template inheritance is the most common place people need a wrapper. You have a base template with a content block, and a child that fills it. Later you want a specific page to put the whole content area inside a container, but you do not want to duplicate what the parent already renders. That is exactly what super() is for. When you call it inside an overriding block, Jinja2 renders the parent’s version of that block and returns it as markup.
{# base.html #}
<main>
{% block content %}
<p>Default content from the base template.</p>
{% endblock %}
</main>
{# page.html #}
{% extends "base.html" %}
{% block content %}
<div class="card">
<div class="card-body">
{{ super() }}
</div>
</div>
{% endblock %}The output contains the card markup with the parent’s paragraph inside it. You can wrap in either direction: put your own markup before and after super(), or call super() first and append. The parent block never changes, and other children that extend the same base are unaffected.
This works across more than one level. If page.html extends section.html, which extends base.html, then super() in page.html renders the block as defined in section.html. If section.html itself calls super(), you get all three layers nested. Jinja2 also supports super.super() when you need to skip one level, though that usually signals the hierarchy could be simpler.
content block that also contains the sidebar is hard to wrap cleanly. Split it into content and sidebar blocks and children can wrap either one independently.Wrapping arbitrary content with a macro and call blocks
When the wrapper is reusable and not tied to inheritance, a macro is the right tool. A regular macro takes arguments and returns markup. A macro invoked with {% call %} also receives a block of template content, which it can render anywhere by calling caller(). This is how you build a card, modal, alert or panel component that accepts any body.
{# macros.html #}
{% macro panel(title, kind="info") -%}
<section class="panel panel-{{ kind }}">
<h3>{{ title }}</h3>
<div class="panel-body">
{{ caller() }}
</div>
</section>
{%- endmacro %}
{# page.html #}
{% from "macros.html" import panel %}
{% call panel("Deployment status", kind="warning") %}
<p>The last deploy finished with {{ warnings|length }} warnings.</p>
<ul>
{% for w in warnings %}
<li>{{ w }}</li>
{% endfor %}
</ul>
{% endcall %}The body between {% call %} and {% endcall %} is compiled as a nested macro and passed into panel as caller. Everything inside the body has access to the calling template’s variables, so warnings resolves normally. The macro can render caller() once, several times, or not at all, which is useful for things like tabs or collapsible sections.
The macro can also hand values back to the body. Declare parameters on the call block with {% call(item) render_list(items) %} and invoke caller(item) inside the macro. The official Jinja2 template documentation on call blocks shows this pattern for rendering a dialog and a list where the macro owns the loop and the caller owns the per item markup.
{% macro render_list(items) -%}
<ul class="list">
{% for item in items %}
<li>{{ caller(item) }}</li>
{% endfor %}
</ul>
{%- endmacro %}
{% call(user) render_list(users) %}
<strong>{{ user.name }}</strong> ({{ user.email }})
{% endcall %}Conditional wrappers without duplicating content
A very common request is “wrap this in a link only if a URL exists” or “wrap this in a div only on mobile”. The naive approach writes the body twice, once inside the wrapper and once outside, and the two copies drift apart. The clean approach captures the body into a variable once using a block set, then decides where to print it.
{% set body %}
<img src="{{ product.image }}" alt="{{ product.name }}">
<span class="name">{{ product.name }}</span>
{% endset %}
{% if product.url %}
<a href="{{ product.url }}" class="product">{{ body }}</a>
{% else %}
<div class="product">{{ body }}</div>
{% endif %}Block assignments were added in Jinja 2.8, so this works on any modern install. With autoescaping on, the captured value is already marked safe because the template engine rendered it, so printing {{ body }} does not double escape it.
The same idea works with the super() pattern from earlier. If a page should wrap the inherited content only when a flag is set, capture super() into a variable and branch on the flag, rather than writing two overriding blocks.
{% block content %}
{% set inner = super() %}
{% if boxed %}
<div class="box">{{ inner }}</div>
{% else %}
{{ inner }}
{% endif %}
{% endblock %}Before you branch on a variable like boxed, make sure it is actually defined in the rendering context. Our guide on checking whether a variable exists in Jinja2 covers is defined and the difference between undefined and falsy values.
Filter blocks for text transforms
Sometimes the wrapper is not markup at all. You want everything inside a region uppercased, stripped of whitespace, indented, or passed through a custom filter. Jinja2’s {% filter %} tag applies a filter to the rendered output of everything between the opening and closing tags, and you can chain filters the same way you would on an expression.
{% filter upper %}
Deployment {{ env }} finished at {{ finished_at }}
{% endfilter %}
{% filter indent(width=4) | trim %}
{% include "snippets/config.yaml" %}
{% endfilter %}Filter blocks are a good fit for generated configuration files, email bodies and anything where indentation or casing has to be exact. If you need to match or replace text inside the wrapped region, see how to use regular expressions in Jinja2, which covers the regex_replace and regex_search filters available in Ansible and how to add equivalents in plain Jinja2.
Include with context inside a wrapper
You can wrap an included template exactly like any other content, because {% include %} just renders another template in place. The question people usually have is about variables. By default an included template sees the current context, including variables you set with {% set %} in the including template. The with context and without context modifiers make that explicit, and they matter most for imports, where the default is the opposite.
{% set panel_title = "Recent orders" %}
<div class="wrapper">
{% include "partials/orders_table.html" with context %}
</div>
{# The partial can read panel_title and any loop or block variables #}
{# Imports are cached without context by default; opt in when the macro needs request data #}
{% from "macros.html" import panel with context %}One more include option is useful for wrappers: ignore missing skips silently if the file does not exist, which gives you optional per page overrides inside a fixed wrapper without any Python code.
Choosing the right wrapping technique
Each pattern solves a slightly different problem, and mixing them up leads to templates that are harder to maintain than they need to be. This table summarizes when to use which.
| Technique | Use when | Key syntax | Limits |
|---|---|---|---|
| Block override with super() | Wrapping content a parent template already renders | {{ super() }} inside {% block %} | Only works in a child template with extends |
| Macro with call block | Reusable component that accepts any body | {% call m() %} and {{ caller() }} | Macro must be imported; context is not shared unless with context |
| Block set capture | Conditional or repeated wrapper around one body | {% set x %} ... {% endset %} | Captured value is a string, not a live block |
| Filter block | Transforming text: casing, indent, trim, custom filters | {% filter f %} ... {% endfilter %} | Applies to text output, not structure |
| Include with context | Wrapping a partial that needs page variables | {% include "x.html" with context %} | Included files cannot override the includer’s blocks |
A useful rule of thumb: if the wrapper belongs to a layout, use inheritance; if it belongs to a component, use a macro; if it belongs to a single decision on a single page, use a block set. Macros that need to unpack several values from the caller are covered in how to unpack more than one variable in Jinja2.
Troubleshooting
UndefinedError: ‘super’ is undefined
super() is only available inside a block in a template that extends another template, and only when the parent defines a block with the same name. If the child is rendered directly without {% extends %}, or the block name has a typo, Jinja2 raises this error. Check the block names match exactly, including case, and that the extends tag is the first tag in the child template.
UndefinedError: ‘caller’ is undefined
You invoked a macro that uses {{ caller() }} with a plain {{ panel("Title") }} expression instead of a {% call %} block. Either switch to {% call panel("Title") %} ... {% endcall %}, or guard the macro with {% if caller %}{{ caller() }}{% endif %} so it works both ways.
The wrapped content is escaped and shows raw HTML
This happens when the body was built as a Python string and passed in with autoescaping on. Content rendered by the template engine, including super(), caller() and block set captures, is already marked safe. Content assembled in Python needs to be wrapped in markupsafe.Markup or printed with the safe filter, and only if you trust it.
Macro cannot see request, session or loop variables
Imported macros do not receive the calling context by default, for caching reasons. Add with context to the import line, or pass what the macro needs as explicit arguments. Explicit arguments are the more maintainable choice because the macro’s dependencies are visible at the call site.
Extra blank lines around the wrapper
Block tags leave newlines behind. Use the whitespace control dash on tags ({%- and -%}) at the edges of your macro, or enable trim_blocks and lstrip_blocks on the environment. For generated config files, pair that with a {% filter trim %} block around the whole output.
Frequently asked questions
Can I wrap a block from a parent template without overriding it?
No. The only way to change how an inherited block renders is to override it in a child template. Wrapping is done by overriding the block and calling super() inside your wrapper markup. If you want the wrapper applied everywhere, put it in the base template instead and keep the inner block for children.
Does super() work across multiple levels of inheritance?
Yes. super() renders the block as defined in the immediate parent. If that parent also calls super(), the grandparent’s version is included too, so wrappers nest naturally. Jinja2 also allows super.super() to reach the grandparent directly, but a simpler hierarchy is usually the better fix.
Can a call block pass variables back to the caller?
Yes. Declare parameters on the call tag, for example {% call(item, index) render(items) %}, and inside the macro invoke caller(item, loop.index). The body then uses those names. This lets the macro own the loop or structure while the calling template controls the markup for each item.
What is the difference between include and import for wrapping?
include renders another template in place and shares the current context by default. import loads macros and variables from a template without rendering it, and does not share context by default. Use include to drop a partial inside a wrapper, and import to bring in a reusable wrapper macro.
Can I wrap content in a filter and a macro at the same time?
Yes. Filter blocks and call blocks nest freely. You can place a {% filter indent(4) %} block inside a {% call %} body, or wrap an entire call block in a filter. The filter applies to the final rendered text of everything inside it, including whatever the macro produced.
The bottom line
Jinja2 gives you four solid ways to wrap content: super() for inherited blocks, {% call %} with caller() for reusable components, block set for conditional wrappers, and {% filter %} for text transforms. Each one avoids duplicating the wrapped content, which is the whole point.
Pick the technique that matches where the wrapper lives: layout, component or a single page decision. Keep block names unique and descriptive, pass macros explicit arguments instead of leaning on with context, and use whitespace control at the edges so your wrappers do not leak blank lines into the output.
About this article: GeekBlog covers U.S. technology news, AI, phones, smartwatches and gaming. Every story is written and checked under our Editorial Policy. Spotted a mistake or have a story tip? Contact our editors.

