Goal: Build a small website that uses HTML templates with Jinja2 to show dynamic content
1) Welcome and What You'll Learn
Have you ever wished your website could change based on data, like showing a list of movies or scores without copy-pasting HTML over and over? That's what templates are for!
In this lesson, you will:
- Understand what a template is
- Learn how Jinja2 fills templates with data
- Use Flask + Jinja2 to build a simple website
- Pass data from Python to HTML templates
- Use variables, loops, and if-statements inside templates
- Create a base layout so pages share the same header and footer
- Link pages with url_for and serve a CSS file
2) Key Vocabulary (Simple Definitions)
- Template: An HTML file with placeholders (like blanks) that get filled in with data.
- Placeholder: A spot in the template where a variable will go, written like {{ name }}.
- Render: Turning a template file into a final HTML page with real data.
- Jinja2: A template language used by Flask to fill in placeholders and run simple logic (loops, if statements).
- Route: A URL path in your web app (like "/" or "/about") handled by a Python function.
- Static file: Files like images, CSS, or JavaScript that don't change each time you load the page.
3) What We're Building: "Movie Night Planner"
We'll create a tiny website that shows a list of movies with ratings. You'll:
- Share the same header and footer across pages using a base template
- Display a list of movies using a for-loop
- Highlight "NEW" movies and editor's picks with if-statements
- Count how many movies are listed
- Add a little CSS for style
4) Setup Steps
You need: Python 3.10+ and an internet connection
A) Create a project folder
- Create a new folder named
movie-night
B) (Optional) Create a virtual environment
- Windows:
python -m venv venv - macOS/Linux:
python3 -m venv venv - Activate it:
- Windows:
venv\Scripts\activate - macOS/Linux:
source venv/bin/activate
- Windows:
C) Install Flask
pip install Flask
D) Create the project structure
Inside movie-night, create these folders and files:
movie-night/
app.py
templates/
base.html
home.html
about.html
static/
styles.css5) Write the Python Code (Flask App)
File: app.py
from flask import Flask, render_template
app = Flask(__name__)
# Sample data for the page
movies = [
{"title": "Spider-Man", "rating": 9.1, "year": 2021, "new": False},
{"title": "Inside Out 2", "rating": 8.7, "year": 2024, "new": True, "note": "Family-friendly <em>favorite</em>!"},
{"title": "The Iron Giant", "rating": 8.4, "year": 1999, "new": False}
]
@app.route("/")
def home():
# Pass data to the template as keyword arguments
return render_template(
"home.html",
page_title="Movie Night Planner",
movies=movies
)
@app.route("/about")
def about():
return render_template("about.html", page_title="About")
if __name__ == "__main__":
app.run(debug=True)6) Create the Base Layout
This template holds the shared head, header, navigation, and footer. Other pages will "extend" it.
File: templates/base.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ page_title }} - PyVerse</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<header>
<h1>PyVerse Movie Night</h1>
<nav>
<a href="{{ url_for('home') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2025 PyVerse</p>
</footer>
</body>
</html>What's new here:
{{ page_title }}is a variable placeholder.{{ url_for('static', filename='styles.css') }}helps Flask find your CSS file.{% block content %}{% endblock %}creates a section that child pages can fill in.
7) Create the Home Page Template
This page extends base.html and fills in the content block. It shows variables, loops, conditionals, and a filter.
File: templates/home.html
{% extends "base.html" %}
{% block content %}
<h2>{{ page_title }}</h2>
<p>We have {{ movies | length }} movies to pick from.</p>
<ul class="movie-list">
{% for movie in movies %}
<li class="movie-card">
<strong>{{ movie.title }}</strong>
<span class="year">({{ movie.year }})</span>
{% if movie.new %}
<span class="badge">NEW</span>
{% endif %}
<div>Rating: {{ movie.rating }}</div>
{% if movie.rating >= 9 %}
<div class="recommend">Editor's pick!</div>
{% endif %}
{% if movie.note %}
<!-- safe is OK here because we wrote the note ourselves (not user input) -->
<p class="note">{{ movie.note | safe }}</p>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}What's happening:
{{ movies | length }}uses a filter to count items in the list.{% for %}loops through the movie list and shows each movie.{% if %}conditionally shows badges or messages.| safetells Jinja2 not to escape HTML in movie.note. Only use safe when you trust the content (like your own text), never on user input.
8) Create the About Page Template
File: templates/about.html
{% extends "base.html" %}
{% block content %}
<h2>{{ page_title }}</h2>
<p>This tiny app shows how Jinja2 templates work with Flask. You can pass data from Python into HTML and use loops and if-statements right in the template.</p>
{% endblock %}9) Add a Simple Stylesheet
File: static/styles.css
body { font-family: system-ui, Arial, sans-serif; margin: 0; }
header { background: #222; color: #fff; padding: 1rem; }
nav a { color: #fff; margin-right: 1rem; text-decoration: none; }
main { padding: 1rem; }
.movie-list { list-style: none; padding-left: 0; }
.movie-card { background: #f4f6ff; border: 1px solid #dbe1ff; padding: .75rem; margin: .5rem 0; border-radius: 8px; }
.badge { background: #00c853; color: #fff; padding: 0 .4rem; border-radius: 4px; font-size: .8rem; margin-left: .5rem; }
.recommend { color: #1565c0; font-weight: bold; }
.note { color: #444; font-style: italic; }10) Run Your App
- In your terminal, make sure you're in the
movie-nightfolder. - Start the server:
- Windows:
python app.py - macOS/Linux:
python3 app.py
- Windows:
- Open your browser and go to:
http://127.0.0.1:5000/ - Click "About" in the top navigation to see the second page.
11) How Jinja2 Works (Quick Guide)
- Variables:
{{ name }}shows a value you pass from Python. Example:{{ page_title }} - Filters: Change how variables look. Examples:
{{ text | upper }}→ ALL CAPS{{ movies | length }}→ number of items{{ text | title }}→ Title Case
- Loops: Repeat HTML for each item.
{% for movie in movies %} ... {% endfor %}
- Conditionals: Show something only if a condition is true.
{% if movie.rating >= 9 %} Editor's pick! {% endif %}
- Inheritance: Share layout and styles across pages.
{% extends "base.html" %}and{% block content %}{% endblock %}
- url_for: Generates correct links to routes and static files.
{{ url_for('home') }},{{ url_for('static', filename='styles.css') }}
- Auto-escaping: Jinja2 automatically escapes HTML to keep you safe from harmful code. Only use
| safeon content you trust.
12) Mini-Exercises
Try these to practice. Test after each change.
A) Add a "Contact" page
- Create
templates/contact.htmlthat extendsbase.html. - Add a route in
app.py:@app.route("/contact") def contact(): return render_template("contact.html", page_title="Contact") - Add a link in
base.htmlnav:<a href="{{ url_for('contact') }}">Contact</a>
B) Show genres
- Add a "genre" key to your movies in
app.py(like "Action" or "Animation"). - Display it under the title in
home.html.
C) Color-code ratings
- If rating >= 9, add a class "top-rated".
- If rating < 7, add a class "low-rated".
- Style those classes in CSS.
D) Count new movies
- Show a line: "New this year: X"
- Hint: You can count inside Python (calculate before render_template) or use a Jinja2 sum filter like:
{{ movies | selectattr('new') | list | length }}(Select movies where new is true, turn into a list, count it.)
E) Format titles
- Try filters like title or upper:
{{ movie.title | title }}
13) Bonus: Reusable "Include"
If you like, move the movie card HTML into a small partial template and include it.
File: templates/_movie.html
<li class="movie-card">
<strong>{{ movie.title }}</strong>
<span class="year">({{ movie.year }})</span>
{% if movie.new %}<span class="badge">NEW</span>{% endif %}
<div>Rating: {{ movie.rating }}</div>
{% if movie.rating >= 9 %}<div class="recommend">Editor's pick!</div>{% endif %}
{% if movie.note %}<p class="note">{{ movie.note | safe }}</p>{% endif %}
</li>Update templates/home.html loop:
<ul class="movie-list">
{% for movie in movies %}
{% include "_movie.html" %}
{% endfor %}
</ul>14) Common Errors and Fixes
- TemplateNotFound: Make sure the folder is named
templates(exactly) and the file name matches. - Static files not loading: Is your CSS in
static/styles.css? Is the link usingurl_for('static', filename='styles.css')? - Jinja2 syntax error (unexpected 'endfor'): Check that every
{% for %}has an{% endfor %}, and every{% if %}has an{% endif %}. - Variable not showing: Did you pass it in
render_template? Example:render_template("home.html", movies=movies) - Server not restarting: If
debug=Trueis on, Flask auto-reloads. If not, stop and re-runpython app.py. - Port already in use: Stop other servers or change the port:
app.run(debug=True, port=5001)
16) Wrap-Up
You now know how to:
- Set up a Flask app that uses Jinja2 templates
- Share a common layout with template inheritance
- Pass data from Python to HTML and display it with variables
- Use loops and if-statements to make pages dynamic
- Link to routes and static files with url_for
This is the foundation for building real, data-driven websites. Great work!
17) Next Steps
- Add a "Top 10" page that shows only the highest-rated movies.
- Add a search box (no database needed yet—just filter your Python list).
- Learn about forms to let users add movies (Flask + POST requests).
- Explore more Jinja2 filters and macros for cleaner templates.