Use Templates to Render HTML
Use templates to render HTML pages — Python web development. This lesson explains the core concept, shows hands-on steps, compares options, and covers troubleshooting.
Focus: use templates to render html pages
If you’ve been building Python web applications, you know the pain: hardcoding HTML into your Python strings, concatenating user data into markup, and fixing broken pages every time a design changes. It’s fragile, hard to maintain, and an absolute nightmare when you need to display dynamic content. Fortunately, there’s a cleaner way: templates. In this lesson, you’ll learn how to use templates to render HTML pages — separating your logic from your presentation, keeping your code DRY, and making your web apps genuinely maintainable.
The problem this lesson solves
Returning plain text from a web route is fine for APIs and health checks, but real web pages need structure, styling, and dynamic data. If you’re building a user dashboard, a blog, or an e-commerce site, you need to send HTML to the browser — and that HTML must often reflect the current state of your application.
The naive approach is to embed HTML directly inside your Python code:
# bad_example.py — don't do this
def render_user(username, age):
return f"<h1>Welcome, {username}</h1><p>Age: {age}</p>"
That works for one tiny page, but imagine maintaining a full site with a dozen pages, shared headers, and changing designs. Every update requires editing Python, risking syntax errors and XSS vulnerabilities. You end up mixing business logic with presentation — a recipe for messy, unmaintainable code.
The solution is templating: you write HTML files with placeholders for dynamic data, and the framework fills those placeholders at runtime. This lesson shows you how to do that in Python — using the tools you already have in your web framework.
Core concept / mental model
Think of a template as a blueprint for a page. It’s an HTML file with special markers where dynamic content will be inserted. The web framework has a templating engine that reads that blueprint, combines it with your data, and produces the final HTML that gets sent to the client.
Here’s a word diagram of the flow:
Request → View function → Template + Data → Rendered HTML → Browser
- Template = static structure (HTML, CSS, placeholders)
- Data = Python variables (user object, list of items, etc.)
- Template engine = the component that merges them
This separation lets you build pages declaratively: templates define what the page looks like, and your Python code defines what data goes into it. It mirrors the pattern of separating HTML from logic in client-side frameworks, but it happens on the server, which means it’s fast, SEO-friendly, and works without JavaScript.
Templates also give you inheritance and partials — you can create a base layout with header and footer, then each page extends it. That’s a game-changer for consistency and maintainability.
How it works step by step
Let’s walk through the typical steps to render a template in a Python web app, using Flask as the example — but the same principles apply to Django, FastAPI with Jinja2, or any other framework.
-
Create a templates directory — most frameworks expect your
.htmlfiles in a folder namedtemplatesby default. -
Write an HTML template — with placeholders like
{{ variable }}for dynamic parts. -
In your route function, call
render_template— pass the template name and the data as keyword arguments. -
The engine processes the template — replaces placeholders, evaluates expressions, runs loops and conditionals (if your template syntax supports them).
-
The framework returns the rendered HTML — as a response to the client.
Here’s a minimal Flask app that does exactly that:
# app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html', name='Alex', items=['Python', 'Flask', 'Templates'])
if __name__ == '__main__':
app.run(debug=True)
And the template templates/index.html:
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head><title>Home</title></head>
<body>
<h1>Welcome, {{ name }}!</h1>
<p>Your skills:</p>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>
When you run the app and visit http://localhost:5000/, the browser receives fully rendered HTML with the loop expanded into list items. No string concatenation, no Python in the markup.
Hands-on walkthrough
Let’s build a slightly more realistic example: a multi-page site with a base layout, dynamic content, and a list of items.
First, create a base template that all pages will extend — this keeps your header/footer consistent:
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<nav>
<a href="/">Home</a> | <a href="/about">About</a>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer>© 2024 My App</footer>
</body>
</html>
Now create a child template that extends the base and overrides the content block:
<!-- templates/index.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome, {{ user.username }}!</h1>
<p>You have {{ user.posts|length }} posts.</p>
<ul>
{% for post in user.posts %}
<li><b>{{ post.title }}</b> — {{ post.date }}</li>
{% else %}
<li>No posts yet.</li>
{% endfor %}
</ul>
{% endblock %}
Finally, update your Flask route to pass a user object and its posts:
# app.py (updated)
from flask import Flask, render_template
app = Flask(__name__)
class User:
def __init__(self, username, posts):
self.username = username
self.posts = posts
class Post:
def __init__(self, title, date):
self.title = title
self.date = date
@app.route('/')
def home():
user = User(
username='alice',
posts=[
Post('First post', '2024-05-01'),
Post('Second post', '2024-05-03')
]
)
return render_template('index.html', user=user)
if __name__ == '__main__':
app.run(debug=True)
Now start the server and view the page. Notice how the template accesses attributes like user.username and user.posts, and loops over them. The output will be a clean HTML page with a nav, footer, and the posts listed.
Expected output (rendered HTML in the browser):
<h1>Welcome, alice!</h1>
<p>You have 2 posts.</p>
<ul>
<li><b>First post</b> — 2024-05-01</li>
<li><b>Second post</b> — 2024-05-03</li>
</ul>
You can also use templates to render JSON inside the page — for example, embedding data for a JavaScript chart:
<!-- inside a template -->
<script>
const data = {{ chart_data | tojson }};
// now use `data` in JS
</script>
The |tojson filter safely converts Python objects to JSON strings, preventing XSS and syntax errors.
Compare options / when to choose what
You don’t have to use templates — sometimes you want to return plain JSON, or you might use a JavaScript front-end framework. Here’s how templates stack up against the alternatives:
| Approach | When to use | Pros | Cons |
|---|---|---|---|
| Server-side templates | Multi-page apps, SEO-critical content, little to no SPA needs | Fast initial load, SEO-friendly, simple logic | Harder to build highly interactive UIs |
| JSON API + front-end framework (React, Vue) | Complex interactive dashboards, real-time updates, separate front-end team | Rich UI, decoupled, scales with many clients | More build tooling, slower initial load, SEO requires SSR/static generation |
| Plain string concatenation | One-off scripts, tiny prototypes | No extra dependencies | Unmaintainable, security risk, mixes logic/presentation |
| Static HTML files | Truly static sites with no dynamic data | Zero processing, fastest | Can't personalize or show dynamic content |
When to choose templates — Use templates for most server-rendered web apps where you need to show data from a database, user sessions, or dynamic content. They're ideal for blogs, dashboards, admin panels, and landing pages with a bit of personalization.
When to choose a JSON API — If you're building a single-page app with heavy interactivity, or you need to serve multiple clients (mobile, web), a JSON API with a framework or even a static generator is usually better. Templates can still be used for the initial HTML shell, but your main data flows via JSON.
Variations — Popular Python template engines include Jinja2 (Flask, FastAPI, standalone), Django Templates (Django's built-in engine), and Mako. Jinja2 is the most common and flexible; Django templates are sandboxed and opinionated. FastAPI works best with Jinja2 if you need server-side rendering.
Troubleshooting & edge cases
Error: Template not found / TemplateNotFound — This usually means the file isn’t in the expected templates directory. Make sure the directory is in the same folder as your Flask app, and the file name matches exactly, including case and extension.
Error: Syntax error in template — Check your {% %} and {{ }} tags. Common issues: missing endfor, unbalanced parentheses, or using {{ }} for blocks (e.g., {% for %} instead of {{ for }}).
Error: Undefined variable — If you reference a variable that wasn't passed, the engine may silently ignore it or throw an error depending on config. To debug, temporarily print the data you're passing, or use {{ user | pprint }} in the template.
Wrong data type in loop — If you loop over a none, Jinja2 will silently do nothing. Instead, test with {% if items %}. Actually, in Jinja2, looping over None raises an error. You can avoid this by passing an empty list from your view.
HTML tags showing as text — If your template contains user-provided content, it might be automatically escaped for security. Use the |safe filter only when you're sure the content is trusted, otherwise you risk XSS.
Debug set to False — With debug=True, errors show detailed messages. For production, set debug=False and configure logging to avoid leaking code details.
Performance — Templates are compiled into Python code on first load, which is fast. But if you’re generating thousands of variants, consider caching the rendered output at the HTTP layer.
What you learned & what's next
You now know how to use templates to render HTML pages — separating structure from data, creating a base layout, extending it with child templates, and passing dynamic content from your Python routes. You’ve seen how to loop, use conditionals, filters, and inheritance, and you understand when templates are the right choice versus a JSON API.
You can complete a practical exercise: create a small blog app with a list of posts, a detail page, and a base template that includes a navigation bar — then pass the post data from a Python list.
To take care of edge cases: always escape user-generated content, pass all variables your template needs, and organize your templates into a proper directory structure.
Next up in this track: you'll learn about forms and user input handling — how to capture data from HTML forms, validate it, and process it with Python. That’s the natural next step after rendering dynamic pages.
Time to practice and make your apps shine!
Practice recap
Create a two-page website: a home page with a personalized greeting and an about page. Use a base template with a common nav, pass a user object to the home template, and use a loop to display a list of skills. Try adding a conditional to show a different message if the user has zero skills.
Common mistakes
- Forgetting the templates directory — Flask and Django expect templates in a
templates/folder; putting them elsewhere causes a TemplateNotFound error. - Using
{{ }}inside a{% %}tag or vice versa — for loops and conditionals must use{% %}, variable output uses{{ }}. - Passing a variable that doesn't exist — Jinja2 silently renders nothing for missing variables; double-check your route's data dict.
- Not escaping user input — automatically escaped content prevents XSS, but if you use
|safewithout trusting the source, you invite attacks. - Looping over
None— if your data isNone, Jinja2 throws an error; always pass an empty list from the view.
Variations
- Use Django Templates instead of Jinja2 — the syntax is similar but more restricted (e.g., no arbitrary Python calls).
- Use FastAPI with Jinja2 as an optional dependency — you get async request handling plus server-side rendering.
- Adopt Mako for better Python integration — but be aware it's less secure due to higher flexibility.
Real-world use cases
- A blog or news site where each article page is generated from a template with the article's title and body.
- A user profile dashboard that renders personalized data (username, recent orders, stats) using a shared base layout.
- An admin panel that lists records from a database, with templates for listing, creating, and editing entries.
Key takeaways
- Templates separate HTML structure from Python logic, making web apps easier to maintain and extend.
- The
render_templatefunction combines a template file with dynamic data, producing the final HTML response. - Template inheritance lets you define a base layout with common header/footer and override only the content.
- Jinja2 syntax uses
{{ }}for expressions,{% %}for logic like loops and conditionals, and{# #}for comments. - Always escape user-provided data unless you genuinely trust it as raw HTML.
- Choose templates for SEO-friendly, multi-page, server-rendered apps; choose JSON APIs when you need heavy client-side interactivity.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.