We use cookies to enhance your experience on the site
CodeWorlds

Jinja2 Templates - dynamic interfaces

Welcome back, @name! 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! πŸ“βœ…

Go to CodeWorlds→