Jinja2 Deep Dive: Filters, Macros & Inheritance
Reviewed & published by Brayan K
Jinja2 is Flask's template engine that turns HTML files into dynamic pages using variables, filters, loops, and reusable layouts.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll go beyond basic output and master filters, control flow, template inheritance, includes, and macros — the tools that keep large template sets clean.
What You'll Learn in This Lesson
- Output variables and expressions in templates
- Use built-in filters: default, length, join, upper
- Write a custom filter with @app.template_filter
- Loop and branch with for and if blocks
- Share a layout with extends and block
- Reuse markup with include and macros
1 Variables, Expressions & Filters
Inside a template, prints a value and runs a statement. A filter transforms the value to its left using the pipe character, like .
Filters chain and accept arguments: default('Guest') supplies a fallback for missing values, length counts items, and join(', ') stitches a list into a string. They keep formatting logic in the template instead of cluttering your Python.
We use render_template_string here so every example runs with no template files — but the exact same syntax works in .html files rendered with render_template.
# app.py — built-in Jinja filters
from flask import Flask, render_template_string
app = Flask(__name__)
# render_template_string runs a template from a string (no files needed)
with app.app_context():
out = render_template_string(
"{{ name|default('Guest') }} has {{ items|length }} items: {{ items|join(', ') }}",
name="Ada",
items=["pen", "map", "key"],
)
print(out) # Ada has 3 items: pen, map, key
# default() fills in when a variable is missing/empty
out2 = render_template_string(
"Welcome, {{ name|default('Guest') }}!",
name="",
)
print(out2) # Welcome, Guest!
# ✅ Expected output:
# Ada has 3 items: pen, map, key
# Welcome, !2 Custom Filters, Loops & Conditionals
When a built-in filter doesn't exist, register your own with @app.template_filter. The decorated function receives the piped value (plus any arguments) and returns the transformed result.
Templates also have full control flow: iterates and branches. Together they let you build tables, lists, and conditional sections directly in markup.
# app.py — a custom filter plus for/if control flow
from flask import Flask, render_template_string
app = Flask(__name__)
# Register a reusable custom filter
@app.template_filter("shout")
def shout(value):
return value.upper() + "!"
with app.app_context():
print(render_template_string("{{ 'hi'|shout }}")) # HI!
# Loop with a conditional inside it
template = "{% for n in nums %}{% if n % 2 == 0 %}{{ n }} {% endif %}{% endfor %}"
even = render_template_string(template, nums=[1, 2, 3, 4, 5, 6])
print(even.strip()) # 2 4 6
# loop.index gives the 1-based position
rows = "{% for name in names %}{{ loop.index }}. {{ name }} {% endfor %}"
print(render_template_string(rows, names=["Ada", "Grace"]).strip()) # 1. Ada 2. Grace
# ✅ Expected output:
# HI!
# 2 4 6
# 1. Ada 2. GraceNotice loop.index — Jinja exposes a special loop object inside every for block with handy helpers like loop.index, loop.first, and loop.last.
3 Inheritance, Includes & Macros
Template inheritance is Jinja's superpower. A base layout declares placeholders; a child template uses and overrides only the blocks it cares about. Your header, nav, and footer live in one place.
drops another template's contents inline — perfect for a reusable card or navbar. A macro is like a template function: define it once, call it many times to render repeated markup such as form inputs.
Because extends and include need to look templates up by name, the example registers them with a DictLoader instead of files — the behavior is identical to a real templates/ folder.
# app.py — inheritance, include, and macros
from flask import Flask, render_template, render_template_string
from jinja2 import DictLoader
app = Flask(__name__)
# Register named templates so {% extends %} / {% include %} can find them
app.jinja_loader = DictLoader({
"base.html": (
"<html><head><title>"
"{% block title %}Site{% endblock %}"
"</title></head><body>"
"{% block content %}{% endblock %}"
"</body></html>"
),
"home.html": (
"{% extends 'base.html' %}"
"{% block title %}Home{% endblock %}"
"{% block content %}<h1>Welcome!</h1>{% endblock %}"
),
"_badge.html": "<span class='badge'>{{ label }}</span>",
})
with app.app_context():
# extends pulls the child into the base layout
print(render_template("home.html"))
# include drops a partial in place
print(render_template_string("{% include '_badge.html' %}", label="New"))
# a macro = a reusable template function
macro_tpl = (
"{% macro field(name, value='') %}"
"<input name='{{ name }}' value='{{ value }}'>"
"{% endmacro %}"
"{{ field('email') }} {{ field('user', 'ada') }}"
)
print(render_template_string(macro_tpl))
# ✅ Expected output:
# <html><head><title>Home</title></head><body><h1>Welcome!</h1></body></html>
# <span class='badge'>New</span>
# <input name='email' value=''> <input name='user' value='ada'>The first print shows the child filling the base layout's blocks; the macro prints two inputs from one definition. That is how you keep a 50-page site DRY.
🔬 Jinja2 Syntax Cheat Sheet
| You want to... | Use |
|---|---|
| Print a value | |
| Provide a fallback | |
| Loop over items | |
| Override a layout block | |
| Reuse a snippet |
🎯 Your Turn: Fill in the Blanks
Complete the template so it shows a default name, counts the tasks, and uppercases a status. Replace each ___.
# 🎯 YOUR TURN: replace each ___ to finish the template
from flask import Flask, render_template_string
app = Flask(__name__)
with app.app_context():
template = (
"User: {{ name|___('Guest') }} | " # fallback filter
"Tasks: {{ tasks|___ }} | " # count the list
"Status: {{ status|___ }}" # make it UPPERCASE
)
out = render_template_string(
template,
# note: 'name' is intentionally NOT passed, so default() kicks in
tasks=["a", "b", "c"],
status="active",
)
print(out) # User: Guest | Tasks: 3 | Status: ACTIVECommon Errors & Quick Reference
Jinja can't locate the parent. Make sure the name matches a file in templates/ (or a key in your loader) exactly, including the .html extension.
❌ Forgetting to close a block
Every needs and every needs . A missing end tag raises a TemplateSyntaxError.
| Task | Code |
|---|---|
| Custom filter | @app.template_filter("name") |
| Loop counter | |
| Define a macro | |
| Override a block |
🏆 Mini Challenge: A Reusable Card Macro
Build a macro that renders a product card, then call it for each item in a list.
- Define a card(title, price) macro
- Loop over a list of products and call the macro for each
- Format the price with the round filter
# 🏆 Mini Challenge: a card macro in a loop
from flask import Flask, render_template_string
app = Flask(__name__)
products = [
{"title": "Mug", "price": 8.5},
{"title": "Notebook", "price": 12.0},
]
with app.app_context():
template = (
"{% macro card(title, price) %}"
"[{{ title }}: USD {{ price|round(2) }}]"
"{% endmacro %}"
"{% for p in products %}{{ card(p.title, p.price) }} {% endfor %}"
)
print(render_template_string(template, products=products).strip())
# [Mug: USD 8.5] [Notebook: USD 12.0]❓ Frequently Asked Questions
Lesson complete — your templates are now DRY and powerful!
You can apply built-in and custom filters, loop and branch in markup, share a layout with extends and block, and factor out repetition with include and macros.
Practice quiz
In a Jinja2 template, what does {{ ... }} do?
- Defines a macro
- Imports a module
- Prints (outputs) a value or expression
- Starts a loop
Answer: Prints (outputs) a value or expression. {{ ... }} prints a value, while {% ... %} runs a statement.
What does {% ... %} mark in a Jinja2 template?
- A comment
- A raw string
- A printed value
- A statement or control structure
Answer: A statement or control structure. {% ... %} delimits statements like for, if, block, and extends.
Which character applies a filter to a value?
- The pipe |
- The ampersand &
- The at sign @
- The percent %
Answer: The pipe |. Filters use the pipe, as in {{ name|upper }}.
What does the default filter do, e.g. {{ name|default('Guest') }}?
- Counts characters
- Supplies a fallback when the value is undefined
- Uppercases the value
- Reverses the string
Answer: Supplies a fallback when the value is undefined. default('Guest') provides a fallback for a missing/undefined value.
Which filter counts the items in a list?
- count
- size
- length
- total
Answer: length. {{ items|length }} returns the number of items.
How do you register a custom Jinja filter in Flask?
- @app.filter
- @app.add_filter
- @jinja.filter
- @app.template_filter("name")
Answer: @app.template_filter("name"). @app.template_filter("name") registers a reusable custom filter.
Inside a {% for %} loop, what does loop.index give?
- The 1-based position of the current item
- The list length
- The loop variable name
- The last item
Answer: The 1-based position of the current item. loop.index is the 1-based iteration counter Jinja exposes.
Which tag makes a child template build on a base layout?
- {% include %}
- {% extends 'base.html' %}
- {% import %}
- {% macro %}
Answer: {% extends 'base.html' %}. {% extends 'base.html' %} lets a child override the base layout's blocks.
What does {% include '_partial.html' %} do?
- Defines a new block
- Loops over a partial
- Drops another template's contents inline
- Deletes a template
Answer: Drops another template's contents inline. include inserts the rendered contents of another template in place.
What is a Jinja macro best described as?
- A reusable template function you define once and call many times
- A database query
- A Python decorator
- A CSS class
Answer: A reusable template function you define once and call many times. A macro is like a template function for rendering repeated markup.
Continue this course
- Previous: Deploying Flask
- Next: URL Building & Redirects with url_for — Generate URLs the safe way with url_for and redirect between views
- Quick reference: Flask cheat sheet › Templates (Jinja2)
Frequently asked questions
What is a Jinja2 filter?
A filter transforms a value inside a template using the pipe syntax, like {{ name|upper }}. Jinja ships with dozens of built-in filters such as default, length, join, capitalize, and round, and you can write your own with @app.template_filter to reuse formatting logic across templates.
How does template inheritance work in Flask?
You define a base template with {% block %} placeholders, then a child template starts with {% extends 'base.html' %} and overrides those blocks. The child only fills in the parts that change, so the shared layout (header, nav, footer) lives in one file and the whole site stays consistent.
What is the difference between include and extends?
extends pulls your child template INTO a parent layout and overrides its blocks — it is for the overall page skeleton. include drops the full contents of another template at a specific spot, like reusing a card or a navbar partial. Use extends for layout and include for reusable chunks.
What is a Jinja macro?
A macro is a reusable template function defined with {% macro name(args) %}...{% endmacro %}. Call it like a function to render repeated markup — for example an input() macro that outputs a form field. Macros keep templates DRY the way functions keep Python code DRY.