Introduction to Templates
Templates allow you to separate your application's logic from its presentation. Instead of building HTML strings in Python, you create template files with placeholders that Flask fills in with dynamic data.
What is Jinja2?
Jinja2 is a modern and designer-friendly templating language for Python. It is fast, widely used, and features powerful template inheritance. Flask uses Jinja2 as its default template engine, providing a clean separation between your Python code and HTML templates.
Why it matters: Templating makes your code more maintainable, allows designers to work on HTML independently, and provides automatic escaping to prevent XSS attacks.
Template Rendering Flow
The template engine combines your Python data with the template file, replacing placeholders with actual values to produce the final HTML sent to the browser.
Project Structure
Flask looks for templates in a templates folder by default. Create this folder at the same level as your main application file.
my_flask_app/
app.py # Main application
templates/ # Template folder
index.html
about.html
layout.html
static/ # Static files (CSS, JS, images)
css/
js/
images/
Keep your templates organized in the templates/ folder and static assets in the static/ folder. Flask automatically locates these directories.
Rendering Templates
The render_template() function loads a template file, fills in the provided variables, and returns the rendered HTML as a response.
Basic Template Rendering
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
@app.route('/user/')
def user(name):
return render_template('user.html', username=name)
The first argument is the template filename. Additional keyword arguments become variables available in the template.
Passing Multiple Variables
@app.route('/dashboard')
def dashboard():
user_data = {
'name': 'Alice',
'email': 'alice@example.com',
'posts': 42
}
notifications = ['New comment', 'New follower', 'Post liked']
return render_template('dashboard.html',
user=user_data,
notifications=notifications,
is_admin=True)
You can pass strings, numbers, lists, dictionaries, and even objects to templates. Each keyword argument becomes a variable in the template context.
Simple Template Example
<!-- templates/user.html -->
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Hello, {{ username }}!</h1>
<p>Welcome to our Flask application.</p>
</body>
</html>
The {{ username }} placeholder is replaced with the value passed from the Python view function.
Jinja2 Syntax
Jinja2 uses special delimiters to distinguish template code from regular HTML. Understanding these three main syntax types is essential for effective templating.
{{ }} Expressions
Output the result of an expression to the template.
<p>Hello, {{ name }}!</p>
<p>Total: {{ price * qty }}</p>
{% %} Statements
Execute control flow like loops and conditionals.
{% if user %}
<p>Welcome!</p>
{% endif %}
{# #} Comments
Add comments that are not rendered in output.
{# This comment will not
appear in the HTML #}
Accessing Variables
You can access dictionary keys, object attributes, and list indices using dot notation or bracket notation.
<!-- Dictionary access -->
<p>Name: {{ user.name }}</p>
<p>Email: {{ user['email'] }}</p>
<!-- List access -->
<p>First item: {{ items[0] }}</p>
<!-- Nested access -->
<p>City: {{ user.address.city }}</p>
Dot notation is preferred for readability. Both user.name and user['name'] work identically in Jinja2.
Safe HTML Output
By default, Jinja2 escapes HTML characters to prevent XSS attacks. Use the safe filter for trusted HTML content.
<!-- Escaped (safe) - default behavior -->
{{ user_input }}
<!-- Renders: <script>alert('xss')</script> -->
<!-- Unescaped (trusted content only) -->
{{ trusted_html | safe }}
safe filter with trusted content. Never use it with user-provided input.
Control Structures
Jinja2 provides powerful control structures for conditional rendering and iteration. These allow you to create dynamic content based on your data.
Conditional Statements
Use if, elif, and else to conditionally render content.
{% if user %}
<p>Hello, {{ user.name }}!</p>
{% else %}
<p>Please log in.</p>
{% endif %}
{% if score >= 90 %}
<span class="badge bg-success">A</span>
{% elif score >= 80 %}
<span class="badge bg-info">B</span>
{% elif score >= 70 %}
<span class="badge bg-warning">C</span>
{% else %}
<span class="badge bg-danger">F</span>
{% endif %}
Conditionals let you show different content based on values, user roles, or any other condition from your Python code.
For Loops
Iterate over lists, dictionaries, and other iterables to generate repeated content.
<!-- Simple list iteration -->
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<!-- Loop with index -->
{% for user in users %}
<p>{{ loop.index }}. {{ user.name }}</p>
{% endfor %}
The special loop variable provides useful properties like loop.index, loop.first, and loop.last.
Loop Variables
| Variable | Description |
|---|---|
loop.index |
Current iteration (1-indexed) |
loop.index0 |
Current iteration (0-indexed) |
loop.first |
True if first iteration |
loop.last |
True if last iteration |
loop.length |
Total number of items |
<!-- Practical example with loop variables -->
{% for product in products %}
<div class="{% if loop.first %}first{% endif %}">
<span>{{ loop.index }} of {{ loop.length }}</span>
<h3>{{ product.name }}</h3>
<p>{{ product.price | format_currency }}</p>
</div>
{% if not loop.last %}<hr>{% endif %}
{% endfor %}
Filters and Functions
Filters modify variables for display without changing the original data. They are applied using the pipe symbol (|) and can be chained together.
Common Built-in Filters
<!-- String filters -->
{{ name | upper }} {# ALICE #}
{{ name | lower }} {# alice #}
{{ name | capitalize }} {# Alice #}
{{ name | title }} {# Alice Smith #}
{{ text | truncate(50) }} {# First 50 chars... #}
<!-- Number filters -->
{{ price | round(2) }} {# 19.99 #}
{{ count | default(0) }} {# 0 if count is undefined #}
<!-- List filters -->
{{ items | length }} {# Number of items #}
{{ items | first }} {# First item #}
{{ items | last }} {# Last item #}
{{ items | join(', ') }} {# "a, b, c" #}
Filters transform data for display. Chain multiple filters with | - they execute left to right.
Chaining Filters
<!-- Multiple filters applied in sequence -->
{{ username | lower | replace(' ', '_') }}
{{ description | striptags | truncate(100) }}
{{ price | round(2) | string + ' USD' }}
Filters process data from left to right, so lower runs first, then replace.
url_for Function
Generate URLs dynamically using the endpoint name instead of hardcoding paths.
<!-- Link to routes -->
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('user', username='alice') }}">Profile</a>
<!-- Link to static files -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<img src="{{ url_for('static', filename='images/logo.png') }}">
url_for() for links and static files. It handles URL changes automatically and works across different deployment environments.
Template Inheritance
Template inheritance allows you to create a base layout with common elements (header, footer, navigation) and extend it in child templates. This eliminates repetition and ensures consistency.
Base Template (Layout)
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>Copyright 2026</footer>
</body>
</html>
The base template defines block placeholders that child templates can override. Common elements like nav and footer appear once.
Child Template
<!-- templates/home.html -->
{% extends 'base.html' %}
{% block title %}Home - My Site{% endblock %}
{% block content %}
<h1>Welcome to My Site</h1>
<p>This content replaces the content block.</p>
{% endblock %}
Child templates use extends to inherit from a base and block to provide content for specific sections.
Using super()
Include the parent block's content using super().
{% block content %}
{{ super() }} {# Include parent's content #}
<p>Additional content after parent.</p>
{% endblock %}
Practice Exercises
Apply your Jinja2 knowledge with these hands-on exercises covering templates, variables, loops, and inheritance.
Jinja Templates Practice
Task: Create a route that passes a name variable and a template that displays "Hello, [name]!"
Show Solution
# app.py
@app.route('/hello/')
def hello(name):
return render_template('hello.html', name=name)
<!-- templates/hello.html -->
<h1>Hello, {{ name }}!</h1>
Task: Pass a user dictionary with name, email, and age. Display all fields in the template.
Show Solution
# app.py
@app.route('/profile')
def profile():
user = {'name': 'Alice', 'email': 'alice@test.com', 'age': 28}
return render_template('profile.html', user=user)
<!-- templates/profile.html -->
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<p>Age: {{ user.age }}</p>
Task: Pass a list of products and display them in an unordered list with name and price.
Show Solution
# app.py
@app.route('/products')
def products():
items = [
{'name': 'Laptop', 'price': 999},
{'name': 'Mouse', 'price': 29},
{'name': 'Keyboard', 'price': 79}
]
return render_template('products.html', products=items)
<!-- templates/products.html -->
<ul>
{% for product in products %}
<li>{{ product.name }} - ${{ product.price }}</li>
{% endfor %}
</ul>
Task: Show different content based on whether user.is_admin is True or False.
Show Solution
# app.py
@app.route('/dashboard')
def dashboard():
user = {'name': 'Admin', 'is_admin': True}
return render_template('dashboard.html', user=user)
<!-- templates/dashboard.html -->
{% if user.is_admin %}
<div class="admin-panel">
<h2>Admin Dashboard</h2>
<a href="/admin/settings">Settings</a>
</div>
{% else %}
<div class="user-panel">
<h2>User Dashboard</h2>
</div>
{% endif %}
Task: Display a user's name in uppercase, truncate a long description, and format a price.
Show Solution
<!-- templates/item.html -->
<h1>{{ product.name | upper }}</h1>
<p>{{ product.description | truncate(100) }}</p>
<p>Price: ${{ product.price | round(2) }}</p>
<p>Tags: {{ product.tags | join(', ') }}</p>
Task: Create a base.html with nav, content block, and footer. Create home.html and about.html that extend it.
Show Solution
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
</head>
<body>
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
<main>{% block content %}{% endblock %}</main>
<footer>2026 My Site</footer>
</body>
</html>
<!-- templates/home.html -->
{% extends 'base.html' %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome Home</h1>
{% endblock %}
Task: Create a table of users with row numbers and alternating background colors using loop variables.
Show Solution
<!-- templates/users.html -->
<table>
<tr><th>#</th><th>Name</th><th>Email</th></tr>
{% for user in users %}
<tr class="{% if loop.index is odd %}odd{% else %}even{% endif %}">
<td>{{ loop.index }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
{% endfor %}
</table>
Task: Add a CSS file and JavaScript file from the static folder to your base template.
Show Solution
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Site{% endblock %}</title>
<link rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
{% block content %}{% endblock %}
<script src="{{ url_for('static', filename='js/main.js') }}">
</script>
</body>
</html>
Task: Create a macro that generates form input fields with label and use it multiple times.
Show Solution
<!-- templates/forms.html -->
{% macro input_field(name, label, type='text') %}
<div class="form-group">
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type }}" id="{{ name }}" name="{{ name }}">
</div>
{% endmacro %}
<!-- Usage -->
<form>
{{ input_field('username', 'Username') }}
{{ input_field('email', 'Email', 'email') }}
{{ input_field('password', 'Password', 'password') }}
</form>
Key Takeaways
Separation of Concerns
Templates separate HTML presentation from Python logic, making code maintainable and designer-friendly
Jinja2 Syntax
Use {{ }} for output, {% %} for control flow, and {# #} for comments in your templates
Loops and Conditionals
for loops iterate over data, if statements conditionally render content, loop variables track iteration
Filters Transform Data
Use filters like upper, lower, truncate, and join to format data for display without modifying originals
Template Inheritance
Create base layouts with blocks that child templates extend for consistent, DRY design
url_for Function
Generate URLs dynamically for routes and static files - never hardcode paths in templates
Knowledge Check
Quick Quiz
Test what you've learned about Jinja2 templates