Python course Β· Module 5: Flask and Web Apps

Jinja2 Templates - dynamic interfaces

12 min read
In this lesson11

Welcome back! Darwin here with a new tool - Jinja2 templates!

So far you've been writing HTML as hardcoded strings in Python functions. That works, but it's unreadable, hard to maintain, and impractical for larger applications!

Safari Analogy: Hardcoded HTML is like drawing every safari map by hand from scratch - tiring and time-consuming! Jinja2 templates are like map templates - you prepare the structure once, then just fill in the data!

The problem with hardcoded HTML

Remember the code from the previous lesson:

1@app.route('/species')
2def list_species():
3    species_html = ""
4    for spec in species_db.values():
5        species_html += f"""
6        <div style="background: white; padding: 15px;">
7            <h3>{spec['name']}</h3>
8            <p><strong>Population:</strong> {spec['population']}</p>
9        </div>
10        """
11
12    return f"""
13    <!DOCTYPE html>
14    <html>
15    <head><title>Species list</title></head>
16    <body>
17        <h1>Species list</h1>
18        {species_html}
19    </body>
20    </html>
21    """

Problems: HTML mixed with Python logic Hard to read and edit No HTML syntax highlighting Code duplication (navigation, footer) Difficult collaboration with designers No reusability

Solution - Jinja2 Templates

Jinja2 is a template engine - a templating engine that allows separating HTML from Python logic.

How it works?

  1. You create HTML files in the templates/ folder
  2. You use {{ variable }} to display data
  3. You use {% if %}, {% for %} for logic
  4. Flask renders template + data β†’ finished HTML

Analogy: Jinja2 is like a fill-in form - you have a ready structure (HTML), you just insert data (Python variables)!

Project structure with templates

1safari_web/
2β”‚
3β”œβ”€β”€ app.py
4β”‚
5β”œβ”€β”€ templates/          ← Folder for HTML templates
6β”‚   β”œβ”€β”€ base.html
7β”‚   β”œβ”€β”€ home.html
8β”‚   β”œβ”€β”€ species_list.html
9β”‚   └── species_detail.html
10β”‚
11└── static/             ← Folder for static files
12    β”œβ”€β”€ css/
13    β”‚   └── style.css
14    β”œβ”€β”€ js/
15    β”‚   └── script.js
16    └── images/
17        └── logo.png

Flask automatically looks for files in the templates/ folder!

Jinja2 basics - syntax

1. Variables - {{ }}

Display the value of a variable:

1<h1>{{ species_name }}</h1>
2<p>Population: {{ population }}</p>

2. Expressions

1<p>{{ 2 + 3 }}</p>                    <!-- 5 -->
2<p>{{ "Hello " + name }}</p>          <!-- Hello Darwin -->
3<p>{{ species.name }}</p>             <!-- Attribute access -->
4<p>{{ species['name'] }}</p>          <!-- Dict access -->
5<p>{{ species_list[0] }}</p>          <!-- List element access -->

3. Control structures - {% %}

If statement:

1{% if population > 1000 %}
2    <span style="color: green;">Safe</span>
3{% elif population > 100 %}
4    <span style="color: orange;">Endangered</span>
5{% else %}
6    <span style="color: red;">Critically endangered</span>
7{% endif %}

For loop:

1<ul>
2{% for species in species_list %}
3    <li>{{ species.name }} - {{ species.population }} individuals</li>
4{% endfor %}
5</ul>

For loop with index:

1{% for species in species_list %}
2    <p>{{ loop.index }}. {{ species.name }}</p>
3{% endfor %}

4. Filters - |

Filters modify variables:

1<p>{{ name | upper }}</p>                    <!-- LION -->
2<p>{{ name | lower }}</p>                    <!-- lion -->
3<p>{{ name | title }}</p>                    <!-- Lion -->
4<p>{{ species_list | length }}</p>           <!-- 3 -->
5<p>{{ price | round(2) }}</p>                <!-- 19.99 -->
6<p>{{ text | truncate(50) }}</p>             <!-- Truncate to 50 chars -->
7<p>{{ published_date | default('None') }}</p> <!-- Default value -->

Popular filters:

  • upper, lower, title - case change
  • length - list/string length
  • default(value) - default value if None
  • round(n) - round to n places
  • truncate(n) - truncate text
  • join(separator) - join list
  • safe - disable HTML escaping (use carefully!)

5. Comments - {# #}

1{# This is a comment - won't be visible in HTML #}
2<h1>{{ species_name }}</h1>

Rendering templates in Flask

Use the render_template() function:

1from flask import Flask, render_template
2
3app = Flask(__name__)
4
5@app.route('/')
6def home():
7    # Render template + pass variables
8    return render_template('home.html',
9                         title='Safari Database',
10                         species_count=3)
11
12@app.route('/species/<int:species_id>')
13def show_species(species_id):
14    species = species_db.get(species_id)
15
16    return render_template('species_detail.html',
17                         species=species)

Flask:

  1. Looks for the file templates/home.html
  2. Renders the template with passed variables
  3. Returns finished HTML

First template - home.html

Create a templates/ folder and a home.html file:

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>{{ title }}</title>
7    <style>
8        body {
9            font-family: Arial, sans-serif;
10            max-width: 800px;
11            margin: 50px auto;
12            padding: 20px;
13            background-color: #f4f1de;
14        }
15        h1 { color: #2c5f2d; }
16    </style>
17</head>
18<body>
19    <h1>{{ title }}</h1>
20    <p>Welcome to Safari Database!</p>
21    <p>The database contains <strong>{{ species_count }} species</strong>.</p>
22</body>
23</html>

app.py:

1from flask import Flask, render_template
2
3app = Flask(__name__)
4
5@app.route('/')
6def home():
7    return render_template('home.html',
8                         title='Safari Database',
9                         species_count=3)
10
11if __name__ == '__main__':
12    app.run(debug=True)

Run: http://localhost:5000/ - you'll see the rendered template!

Template inheritance

The most important Jinja2 feature! It allows creating a base template (layout) and extending it in other templates.

Analogy: Template inheritance is like DNA inheritance - a descendant species (child template) inherits traits from its parent (base template), but can have its own unique characteristics!

base.html - base template

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>{% block title %}Safari Database{% endblock %}</title>
7    <style>
8        body {
9            font-family: Arial, sans-serif;
10            max-width: 900px;
11            margin: 0 auto;
12            padding: 20px;
13            background-color: #f4f1de;
14        }
15        header {
16            background-color: #2c5f2d;
17            color: white;
18            padding: 20px;
19            border-radius: 8px;
20            margin-bottom: 20px;
21        }
22        nav {
23            margin: 20px 0;
24            padding: 15px;
25            background: white;
26            border-radius: 8px;
27        }
28        nav a {
29            margin-right: 15px;
30            color: #2c5f2d;
31            text-decoration: none;
32            font-weight: bold;
33        }
34        nav a:hover {
35            text-decoration: underline;
36        }
37        main {
38            background: white;
39            padding: 30px;
40            border-radius: 12px;
41        }
42        footer {
43            margin-top: 30px;
44            text-align: center;
45            color: #666;
46        }
47    </style>
48    {% block extra_css %}{% endblock %}
49</head>
50<body>
51    <header>
52        <h1>Safari Database</h1>
53        <p>African species data management system</p>
54    </header>
55
56    <nav>
57        <a href="{{ url_for('home') }}">Home</a>
58        <a href="{{ url_for('list_species') }}">Species list</a>
59        <a href="{{ url_for('search') }}">Search</a>
60    </nav>
61
62    <main>
63        {% block content %}
64        <!-- Content from child templates goes here -->
65        {% endblock %}
66    </main>
67
68    <footer>
69        <p>&copy; 2024 Safari Database by Darwin</p>
70    </footer>
71
72    {% block extra_js %}{% endblock %}
73</body>
74</html>

Blocks ({% block %}): Places that can be overridden in child templates.

home.html - extending base.html

1{% extends 'base.html' %}
2
3{% block title %}Home - Safari Database{% endblock %}
4
5{% block content %}
6    <h2>Welcome to Safari Database!</h2>
7    <p>The database contains <strong>{{ species_count }} species</strong>.</p>
8
9    <div style="margin-top: 30px;">
10        <h3>System features:</h3>
11        <ul>
12            <li>Browse species list</li>
13            <li>Search species</li>
14            <li>Population statistics</li>
15            <li>Add new observations</li>
16        </ul>
17    </div>
18
19    <p style="margin-top: 30px;">
20        <a href="{{ url_for('list_species') }}" style="background: #2c5f2d; color: white; padding: 10px 20px; border-radius: 4px; text-decoration: none;">
21            See all species β†’
22        </a>
23    </p>
24{% endblock %}

{% extends 'base.html' %} - inherits from base.html {% block content %} - overrides the content block

species_list.html - species list

1{% extends 'base.html' %}
2
3{% block title %}Species list - Safari Database{% endblock %}
4
5{% block content %}
6    <h2>All species ({{ species | length }})</h2>
7
8    {% if species %}
9        {% for spec in species %}
10            <div style="background: #f9f9f9; padding: 20px; margin: 15px 0; border-radius: 8px; border-left: 4px solid #2c5f2d;">
11                <h3 style="margin-top: 0;">{{ spec.name }}</h3>
12                <p><strong>Scientific name:</strong> <em>{{ spec.scientific_name }}</em></p>
13                <p><strong>Population:</strong> {{ spec.population }} individuals</p>
14
15                {% if spec.population > 1000 %}
16                    <span style="color: green;">Safe</span>
17                {% elif spec.population > 100 %}
18                    <span style="color: orange;">Endangered</span>
19                {% else %}
20                    <span style="color: red;">Critically endangered</span>
21                {% endif %}
22
23                <p style="margin-top: 15px;">
24                    <a href="{{ url_for('show_species', species_id=spec.id) }}" style="color: #2c5f2d; font-weight: bold;">
25                        See details β†’
26                    </a>
27                </p>
28            </div>
29        {% endfor %}
30    {% else %}
31        <p style="color: #666;">No species in the database.</p>
32    {% endif %}
33{% endblock %}

species_detail.html - species details

1{% extends 'base.html' %}
2
3{% block title %}{{ species.name }} - Safari Database{% endblock %}
4
5{% block content %}
6    {% if species %}
7        <h2>{{ species.name }}</h2>
8
9        <div style="background: #f9f9f9; padding: 30px; border-radius: 12px; border: 3px solid #2c5f2d;">
10            <table style="width: 100%; border-collapse: collapse;">
11                <tr>
12                    <td style="padding: 10px; font-weight: bold;">Common name:</td>
13                    <td style="padding: 10px;">{{ species.name }}</td>
14                </tr>
15                <tr>
16                    <td style="padding: 10px; font-weight: bold;">Scientific name:</td>
17                    <td style="padding: 10px;"><em>{{ species.scientific_name }}</em></td>
18                </tr>
19                <tr>
20                    <td style="padding: 10px; font-weight: bold;">Population:</td>
21                    <td style="padding: 10px;">{{ species.population }} individuals</td>
22                </tr>
23                <tr>
24                    <td style="padding: 10px; font-weight: bold;">Status:</td>
25                    <td style="padding: 10px;">
26                        {% if species.population > 1000 %}
27                            <span style="color: green;">Safe</span>
28                        {% elif species.population > 100 %}
29                            <span style="color: orange;">Endangered</span>
30                        {% else %}
31                            <span style="color: red;">Critically endangered</span>
32                        {% endif %}
33                    </td>
34                </tr>
35                <tr>
36                    <td style="padding: 10px; font-weight: bold;">System ID:</td>
37                    <td style="padding: 10px;">{{ species.id }}</td>
38                </tr>
39            </table>
40        </div>
41
42        <p style="margin-top: 30px;">
43            <a href="{{ url_for('list_species') }}" style="color: #2c5f2d;">← Back to list</a> |
44            <a href="{{ url_for('home') }}" style="color: #2c5f2d;">← Home page</a>
45        </p>
46    {% else %}
47        <h2>Species not found</h2>
48        <p>The species with the given ID does not exist in the database.</p>
49        <p><a href="{{ url_for('list_species') }}">← Back to list</a></p>
50    {% endif %}
51{% endblock %}

Static files

CSS, JavaScript, images files are stored in the static/ folder.

Structure:

1static/
2β”œβ”€β”€ css/
3β”‚   └── style.css
4β”œβ”€β”€ js/
5β”‚   └── script.js
6└── images/
7    └── logo.png

static/css/style.css

1/* Safari Database Styles */
2body {
3    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
4    max-width: 900px;
5    margin: 0 auto;
6    padding: 20px;
7    background-color: #f4f1de;
8}
9
10header {
11    background: linear-gradient(135deg, #2c5f2d 0%, #45a049 100%);
12    color: white;
13    padding: 30px;
14    border-radius: 12px;
15    margin-bottom: 20px;
16    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
17}
18
19.species-card {
20    background: white;
21    padding: 20px;
22    margin: 15px 0;
23    border-radius: 8px;
24    border-left: 4px solid #2c5f2d;
25    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
26    transition: transform 0.2s;
27}
28
29.species-card:hover {
30    transform: translateY(-3px);
31    box-shadow: 0 4px 8px rgba(0,0,0,0.1);
32}
33
34.badge {
35    display: inline-block;
36    padding: 4px 12px;
37    border-radius: 4px;
38    font-size: 13px;
39    font-weight: bold;
40}
41
42.badge-safe { background: #4caf50; color: white; }
43.badge-endangered { background: #ff9800; color: white; }
44.badge-critical { background: #f44336; color: white; }

Using static files in templates

1<!DOCTYPE html>
2<html>
3<head>
4    <title>Safari Database</title>
5    <!-- Link to CSS -->
6    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
7</head>
8<body>
9    <!-- Logo -->
10    <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
11
12    <!-- Page content -->
13
14    <!-- Link to JS -->
15    <script src="{{ url_for('static', filename='js/script.js') }}"></script>
16</body>
17</html>

url_for('static', filename='...') generates the URL to a static file.

Complete application with templates

app.py

1# app.py
2from flask import Flask, render_template, request, redirect, url_for
3
4app = Flask(__name__)
5
6# Test data
7species_db = {
8    1: {"id": 1, "name": "Lion", "scientific_name": "Panthera leo", "population": 120},
9    2: {"id": 2, "name": "African Elephant", "scientific_name": "Loxodonta africana", "population": 450},
10    3: {"id": 3, "name": "Cheetah", "scientific_name": "Acinonyx jubatus", "population": 7100},
11    4: {"id": 4, "name": "Giraffe", "scientific_name": "Giraffa camelopardalis", "population": 850},
12}
13next_id = 5
14
15@app.route('/')
16def home():
17    return render_template('home.html',
18                         species_count=len(species_db))
19
20@app.route('/species')
21def list_species():
22    return render_template('species_list.html',
23                         species=list(species_db.values()))
24
25@app.route('/species/<int:species_id>')
26def show_species(species_id):
27    species = species_db.get(species_id)
28    return render_template('species_detail.html',
29                         species=species)
30
31@app.route('/search')
32def search():
33    query = request.args.get('q', '').lower()
34
35    if query:
36        results = [s for s in species_db.values()
37                  if query in s['name'].lower() or query in s['scientific_name'].lower()]
38    else:
39        results = []
40
41    return render_template('search.html',
42                         query=query,
43                         results=results)
44
45if __name__ == '__main__':
46    print("Safari Database starting...")
47    app.run(debug=True)

templates/base.html

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>{% block title %}Safari Database{% endblock %}</title>
7    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8    {% block extra_css %}{% endblock %}
9</head>
10<body>
11    <header>
12        <h1>Safari Database</h1>
13        <p>African species data management system</p>
14    </header>
15
16    <nav>
17        <a href="{{ url_for('home') }}">Home</a>
18        <a href="{{ url_for('list_species') }}">Species list</a>
19        <a href="{{ url_for('search') }}">Search</a>
20    </nav>
21
22    <main>
23        {% block content %}{% endblock %}
24    </main>
25
26    <footer>
27        <p>&copy; 2024 Safari Database by Darwin | Powered by Flask & Jinja2</p>
28    </footer>
29
30    {% block extra_js %}{% endblock %}
31</body>
32</html>

templates/search.html

1{% extends 'base.html' %}
2
3{% block title %}Search - Safari Database{% endblock %}
4
5{% block content %}
6    <h2>Species search</h2>
7
8    <form method="GET" style="margin: 30px 0;">
9        <input type="text"
10               name="q"
11               value="{{ query }}"
12               placeholder="Enter species name..."
13               style="padding: 12px; width: 60%; border: 2px solid #2c5f2d; border-radius: 4px; font-size: 16px;">
14        <button type="submit"
15                style="padding: 12px 24px; background: #2c5f2d; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer;">
16            Search
17        </button>
18    </form>
19
20    {% if query %}
21        <h3>Results for: "{{ query }}" ({{ results | length }})</h3>
22
23        {% if results %}
24            {% for species in results %}
25                <div class="species-card">
26                    <h3 style="margin-top: 0;">{{ species.name }}</h3>
27                    <p><em>{{ species.scientific_name }}</em></p>
28                    <p>Population: {{ species.population }} individuals</p>
29                    <a href="{{ url_for('show_species', species_id=species.id) }}" style="color: #2c5f2d; font-weight: bold;">
30                        See details β†’
31                    </a>
32                </div>
33            {% endfor %}
34        {% else %}
35            <p style="color: #666; text-align: center; padding: 40px;">
36                No species found matching query "{{ query }}"
37            </p>
38        {% endif %}
39    {% else %}
40        <p style="color: #666; text-align: center; padding: 40px;">
41            Enter a species name to start searching.
42        </p>
43    {% endif %}
44{% endblock %}

Useful Jinja2 techniques

1. Passing multiple variables

1@app.route('/dashboard')
2def dashboard():
3    return render_template('dashboard.html',
4                         title='Dashboard',
5                         user='Darwin',
6                         species_count=10,
7                         observations_count=152,
8                         recent_activity=['Lion observation', 'Added cheetah'])

2. Passing a dictionary as **kwargs

1context = {
2    'title': 'Dashboard',
3    'user': 'Darwin',
4    'species_count': 10
5}
6
7return render_template('dashboard.html', **context)

3. Global variables (available in all templates)

1@app.context_processor
2def inject_globals():
3    return {
4        'app_name': 'Safari Database',
5        'version': '1.0.0',
6        'current_year': 2024
7    }
8
9# Now {{ app_name }}, {{ version }}, {{ current_year }} work everywhere!

4. Custom filters

1@app.template_filter('format_population')
2def format_population(value):
3    if value >= 1000:
4        return f"{value / 1000:.1f}k"
5    return str(value)
6
7# In template:
8# {{ species.population | format_population }}  β†’ "7.1k"

5. Macros (reusable components)

1{# macros.html #}
2{% macro render_species_card(species) %}
3    <div class="species-card">
4        <h3>{{ species.name }}</h3>
5        <p><em>{{ species.scientific_name }}</em></p>
6        <p>Population: {{ species.population }}</p>
7    </div>
8{% endmacro %}
9
10{# In other templates #}
11{% from 'macros.html' import render_species_card %}
12
13{% for species in species_list %}
14    {{ render_species_card(species) }}
15{% endfor %}

Summary

In this lesson you learned:

  • Why templates are better than hardcoded HTML
  • Jinja2 syntax ({{ }}, {% %}, {# #})
  • Variables and expressions
  • Control structures (if, for)
  • Filters (|upper, |length, |default)
  • Template inheritance (extends, blocks)
  • Static files (CSS, JS, images)
  • url_for() in templates
  • Complete Safari Database application with templates

Final Safari Analogy: Jinja2 templates are like genetic DNA templates - you have a base structure (base.html = ancestor DNA), which you extend in descendants (child templates = descendant DNA) with unique characteristics (blocks)! All species (templates) inherit common traits (navigation, footer), but have their own uniqueness (content)!

Next lesson: Darwin will show you Flask-WTF - forms and data validation the professional way!

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you render the template 'species.html' with an 'animals' variable?

  2. 2. What is Jinja2 in the context of Flask?

These are 2 of 4 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Horizontal ordering

    What does the syntax for displaying a species variable look like in Jinja2?

  • Horizontal ordering

    How do you start an if condition in Jinja2?

  • Click in order

    Arrange a for loop iterating over animals in Jinja2:

  • Vertical ordering

    Arrange the structure of a Jinja2 template with inheritance:

  • Code editor

    Write a template with a for loop displaying animal names in <li> elements.

Useful articles