Learn Flask templates using Jinja2 templating engine. Master template inheritance, variables, control flow, filters, macros, and dynamic HTML rendering in Python Flask applications.
Flask uses Jinja2 as its default templating engine. Jinja2 allows you to create dynamic HTML pages by embedding Python-like expressions and logic directly in your HTML templates.
Setting Up Templates
Flask looks for templates in a templates/ folder relative to your application:
my-flask-app/
├── app.py
├── templates/
│ ├── base.html
│ ├── index.html
│ ├── about.html
│ └── user/
│ ├── profile.html
│ └── settings.html
└── static/
├── css/
└── js/
Rendering Templates
Use render_template() to render a template file:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user/profile.html', username=name, age=25)
@app.route('/products')
def products():
items = [
{'id': 1, 'name': 'Laptop', 'price': 999.99, 'in_stock': True},
{'id': 2, 'name': 'Mouse', 'price': 29.99, 'in_stock': False},
{'id': 3, 'name': 'Keyboard', 'price': 79.99, 'in_stock': True},
]
return render_template('products.html', products=items, title='Our Products')
Jinja2 Syntax Overview
Jinja2 uses three types of delimiters:
| Delimiter | Purpose | Example |
|---|
{{ }} | Output expressions | {{ username }} |
{% %} | Control flow statements | {% if user %} |
{# #} | Comments (not rendered) | {# This is a comment #} |
Variables
<!-- templates/user/profile.html -->
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<h1>Hello, {{ username }}!</h1>
<p>Age: {{ age }}</p>
<!-- Accessing object attributes -->
<p>Email: {{ user.email }}</p>
<!-- Accessing dictionary keys -->
<p>City: {{ user['city'] }}</p>
<!-- Default value if variable is undefined -->
<p>Bio: {{ bio | default('No bio provided') }}</p>
<!-- Arithmetic in templates -->
<p>Next year age: {{ age + 1 }}</p>
</body>
</html>
Control Flow — If/Elif/Else
<!-- templates/status.html -->
{% if user %}
<h2>Welcome back, {{ user.name }}!</h2>
{% if user.is_admin %}
<span class="badge">Administrator</span>
{% elif user.is_moderator %}
<span class="badge">Moderator</span>
{% else %}
<span class="badge">Member</span>
{% endif %}
{% else %}
<h2>Welcome, Guest!</h2>
<a href="/login">Please Login</a>
{% endif %}
<!-- Checking variables and values -->
{% if score >= 90 %}
<p>Grade: A</p>
{% elif score >= 80 %}
<p>Grade: B</p>
{% elif score >= 70 %}
<p>Grade: C</p>
{% else %}
<p>Grade: F</p>
{% endif %}
<!-- Inline conditional (ternary) -->
<p class="{{ 'active' if page == 'home' else 'inactive' }}">Home</p>
Control Flow — For Loops
<!-- templates/products.html -->
<h1>{{ title }}</h1>
{% if products %}
<ul class="product-list">
{% for product in products %}
<li class="product-item {% if loop.index is odd %}odd{% else %}even{% endif %}">
<h3>{{ loop.index }}. {{ product.name }}</h3>
<p>Price: ${{ product.price }}</p>
{% if product.in_stock %}
<span class="in-stock">In Stock</span>
{% else %}
<span class="out-of-stock">Out of Stock</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>No products available.</p>
{% endif %}
Loop Variables
Jinja2 provides special variables inside for loops:
{% for item in items %}
<tr class="{{ 'odd' if loop.odd else 'even' }}">
<td>{{ loop.index }}</td> <!-- 1-indexed position -->
<td>{{ loop.index0 }}</td> <!-- 0-indexed position -->
<td>{{ loop.revindex }}</td> <!-- reverse index (from end) -->
<td>{{ loop.first }}</td> <!-- True if first item -->
<td>{{ loop.last }}</td> <!-- True if last item -->
<td>{{ loop.length }}</td> <!-- Total number of items -->
<td>{{ item.name }}</td>
</tr>
{% endfor %}
<!-- Loop with else (when list is empty) -->
{% for user in users %}
<li>{{ user.name }}</li>
{% else %}
<li>No users found.</li>
{% endfor %}
<!-- Iterating over dictionary -->
{% for key, value in config.items() %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}
Template Filters
Filters transform variables. Use the pipe | character:
<!-- String filters -->
<p>{{ name | upper }}</p> <!-- JOHN -->
<p>{{ name | lower }}</p> <!-- john -->
<p>{{ name | title }}</p> <!-- John Doe -->
<p>{{ name | capitalize }}</p> <!-- John -->
<p>{{ text | truncate(50) }}</p> <!-- Truncate to 50 chars -->
<p>{{ text | truncate(50, True, '...') }}</p>
<p>{{ name | replace('o', '0') }}</p> <!-- Replace characters -->
<p>{{ name | reverse }}</p> <!-- Reverse string -->
<p>{{ ' spaces ' | trim }}</p> <!-- Remove leading/trailing spaces -->
<p>{{ name | length }}</p> <!-- String length -->
<!-- HTML/Safe filters -->
<p>{{ html_content | safe }}</p> <!-- Render HTML (careful with user input!) -->
<p>{{ user_input | escape }}</p> <!-- Escape HTML characters -->
<p>{{ user_input | e }}</p> <!-- Shorthand for escape -->
<!-- Number filters -->
<p>{{ price | round(2) }}</p> <!-- Round to 2 decimal places -->
<p>{{ number | int }}</p> <!-- Convert to integer -->
<p>{{ number | float }}</p> <!-- Convert to float -->
<p>{{ number | abs }}</p> <!-- Absolute value -->
<!-- List/Array filters -->
<p>{{ items | length }}</p> <!-- Count items -->
<p>{{ items | join(', ') }}</p> <!-- Join list to string -->
<p>{{ items | first }}</p> <!-- First item -->
<p>{{ items | last }}</p> <!-- Last item -->
<p>{{ items | reverse | list }}</p> <!-- Reverse list -->
<p>{{ items | sort }}</p> <!-- Sort list -->
<p>{{ items | unique | list }}</p> <!-- Unique items -->
<!-- URL filter -->
<p>{{ text | urlencode }}</p> <!-- URL encode -->
Template Inheritance
Template inheritance lets you define a base layout and extend it in child templates:
<!-- templates/base.html — The parent template -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My Flask App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navigation -->
<nav>
<div class="nav-brand">
<a href="{{ url_for('index') }}">MyApp</a>
</div>
<ul class="nav-links">
<li><a href="{{ url_for('index') }}" class="{{ 'active' if request.endpoint == 'index' }}">Home</a></li>
<li><a href="{{ url_for('about') }}">About</a></li>
<li><a href="{{ url_for('products') }}">Products</a></li>
{% if current_user %}
<li><a href="{{ url_for('profile') }}">{{ current_user.name }}</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li>
{% else %}
<li><a href="{{ url_for('login') }}">Login</a></li>
{% endif %}
</ul>
</nav>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Main Content Block -->
<main>
{% block content %}{% endblock %}
</main>
<!-- Footer -->
<footer>
<p>© 2026 My Flask App</p>
{% block footer_extra %}{% endblock %}
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
<!-- templates/index.html — Child template -->
{% extends "base.html" %}
{% block title %}Home - My Flask App{% endblock %}
{% block content %}
<div class="hero">
<h1>Welcome to My Flask App</h1>
<p>Build amazing web applications with Python and Flask.</p>
<a href="/get-started" class="btn btn-primary">Get Started</a>
</div>
<section class="features">
<h2>Features</h2>
<div class="feature-grid">
{% for feature in features %}
<div class="feature-card">
<h3>{{ feature.title }}</h3>
<p>{{ feature.description }}</p>
</div>
{% endfor %}
</div>
</section>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/home.js') }}"></script>
{% endblock %}
<!-- templates/products.html — Another child template -->
{% extends "base.html" %}
{% block title %}Products - My Flask App{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}">
{% endblock %}
{% block content %}
<h1>Products</h1>
<div class="product-grid">
{% for product in products %}
<div class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price | round(2) }}</p>
<p class="stock {{ 'in-stock' if product.in_stock else 'out-of-stock' }}">
{{ 'In Stock' if product.in_stock else 'Out of Stock' }}
</p>
<a href="{{ url_for('product_detail', product_id=product.id) }}" class="btn">
View Details
</a>
</div>
{% endfor %}
</div>
{% endblock %}
Including Templates
Use {% include %} to include a template inside another:
<!-- templates/partials/_navbar.html -->
<nav class="navbar">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<!-- templates/partials/_footer.html -->
<footer>
<p>© 2026 WoHoTech</p>
</footer>
<!-- templates/index.html — Using include -->
{% include 'partials/_navbar.html' %}
<main>
<h1>Welcome!</h1>
</main>
{% include 'partials/_footer.html' %}
Jinja2 Macros
Macros are reusable template functions:
<!-- templates/macros/forms.html -->
{% macro input(name, label, type='text', value='', required=False) %}
<div class="form-group">
<label for="{{ name }}">{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
<input
type="{{ type }}"
id="{{ name }}"
name="{{ name }}"
value="{{ value }}"
{% if required %}required{% endif %}
class="form-control"
>
</div>
{% endmacro %}
{% macro button(text, type='submit', class='btn-primary') %}
<button type="{{ type }}" class="btn {{ class }}">{{ text }}</button>
{% endmacro %}
{% macro card(title, body, footer='') %}
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
{{ body }}
</div>
{% if footer %}
<div class="card-footer">{{ footer }}</div>
{% endif %}
</div>
{% endmacro %}
<!-- templates/register.html — Using macros -->
{% from 'macros/forms.html' import input, button %}
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form method="POST" action="/register">
{{ input('username', 'Username', required=True) }}
{{ input('email', 'Email', type='email', required=True) }}
{{ input('password', 'Password', type='password', required=True) }}
{{ input('confirm_password', 'Confirm Password', type='password', required=True) }}
{{ button('Create Account') }}
</form>
{% endblock %}
Custom Filters
Register custom Jinja2 filters in Flask:
from flask import Flask
from datetime import datetime
app = Flask(__name__)
# Custom filter — format currency
@app.template_filter('currency')
def currency_filter(value, symbol='$', decimal=2):
return f"{symbol}{value:,.{decimal}f}"
# Custom filter — format datetime
@app.template_filter('datetimeformat')
def datetimeformat_filter(value, format='%B %d, %Y'):
if isinstance(value, str):
value = datetime.strptime(value, '%Y-%m-%d')
return value.strftime(format)
# Custom filter — pluralize
@app.template_filter('pluralize')
def pluralize_filter(count, singular='', plural='s'):
if count == 1:
return singular
return plural
# Custom filter — excerpt
@app.template_filter('excerpt')
def excerpt_filter(text, length=150):
if len(text) <= length:
return text
return text[:length].rsplit(' ', 1)[0] + '...'
@app.route('/demo')
def demo():
return render_template('demo.html',
price=1234.567,
date='2026-06-12',
count=5,
text='Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.'
)
<!-- Using custom filters in template -->
<p>Price: {{ price | currency }}</p> <!-- $1,234.57 -->
<p>Price (EUR): {{ price | currency('€') }}</p> <!-- €1,234.57 -->
<p>Date: {{ date | datetimeformat }}</p> <!-- June 12, 2026 -->
<p>Short date: {{ date | datetimeformat('%m/%d/%Y') }}</p> <!-- 06/12/2026 -->
<p>{{ count }} item{{ count | pluralize }}</p> <!-- 5 items -->
<p>{{ text | excerpt(100) }}</p>
Context Processors
Inject variables into all templates automatically:
from flask import Flask, g
app = Flask(__name__)
# Make variables available in ALL templates
@app.context_processor
def inject_globals():
return {
'app_name': 'My Flask App',
'app_version': '2.0',
'current_year': 2026,
'nav_links': [
{'name': 'Home', 'url': '/'},
{'name': 'About', 'url': '/about'},
{'name': 'Blog', 'url': '/blog'},
]
}
@app.context_processor
def inject_user():
# Inject current user (from session, for example)
user = g.get('user', None)
return {'current_user': user}
<!-- Available in ALL templates without passing explicitly -->
<title>{{ app_name }} v{{ app_version }}</title>
<nav>
{% for link in nav_links %}
<a href="{{ link.url }}">{{ link.name }}</a>
{% endfor %}
</nav>
<footer>
<p>© {{ current_year }} {{ app_name }}</p>
</footer>
Flash Messages
from flask import Flask, flash, redirect, url_for, render_template
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
if username == 'admin':
flash('Welcome back, Admin!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid username or password.', 'danger')
flash('Please try again.', 'warning')
return redirect(url_for('login_page'))
<!-- In base.html — Display flash messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-container">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible">
{{ message }}
<button onclick="this.parentElement.remove()">×</button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
Static Files
Serve CSS, JavaScript, and images:
# Flask automatically serves files from static/ directory
# Access via url_for('static', filename='...')
<!-- In templates -->
<!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
<!-- Images -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
Complete Working Example
# app.py — Complete Flask app with templates
from flask import Flask, render_template, request, flash, redirect, url_for
from datetime import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret-key-change-this'
# Sample data
BLOG_POSTS = [
{'id': 1, 'title': 'Getting Started with Flask', 'author': 'Alice',
'date': '2026-06-01', 'excerpt': 'Learn the basics of Flask web framework.',
'tags': ['flask', 'python', 'web']},
{'id': 2, 'title': 'Jinja2 Templating Guide', 'author': 'Bob',
'date': '2026-06-05', 'excerpt': 'Master Jinja2 for dynamic HTML pages.',
'tags': ['jinja2', 'templates', 'html']},
{'id': 3, 'title': 'Flask REST APIs', 'author': 'Carol',
'date': '2026-06-10', 'excerpt': 'Build powerful REST APIs with Flask.',
'tags': ['api', 'rest', 'flask']},
]
@app.context_processor
def inject_vars():
return {'current_year': datetime.now().year, 'site_name': 'WoHo Blog'}
@app.route('/')
def index():
return render_template('blog/index.html', posts=BLOG_POSTS)
@app.route('/post/<int:post_id>')
def post_detail(post_id):
post = next((p for p in BLOG_POSTS if p['id'] == post_id), None)
if not post:
flash('Post not found!', 'danger')
return redirect(url_for('index'))
return render_template('blog/post.html', post=post)
@app.template_filter('format_date')
def format_date(date_str):
date = datetime.strptime(date_str, '%Y-%m-%d')
return date.strftime('%B %d, %Y')
if __name__ == '__main__':
app.run(debug=True)
Summary
In this lesson, you learned:
- ✅ Setting up and rendering Jinja2 templates
- ✅ Using variables, filters, and control flow in templates
- ✅ Template inheritance with
extends and block - ✅ Including partials with
include - ✅ Creating reusable macros
- ✅ Building custom Jinja2 filters
- ✅ Using context processors for global variables
- ✅ Flash messages for user notifications
- ✅ Serving static files (CSS, JS, images)
*Next Lesson: Django Introduction →*