Render Dynamic Pages with Django Templates
Learn how to render dynamic pages with Django templates in this hands-on tutorial. Understand the core concepts, apply them in a practical exercise, and discover what to study next in the Django Web Development track.
Focus: render dynamic pages with django templates
You've connected URLs to views with django.urls and django.shortcuts, and now your app can respond to a request — but every response looks exactly the same. A plain JSON blob or a static string is fine for an API, but users came to a website, and they expect a page. That's where Django's template engine becomes the missing link between your Python logic and the HTML your readers actually see. Without templates, every small change to your site's layout means editing Python code by hand — an unholy mix of markup and logic that breaks fast and scales worse. In this lesson, you'll learn how to render dynamic pages with Django templates — turning view functions into rich, maintainable HTML pages that show data straight from your Python code.
The problem this lesson solves
So far in this track, your Django views have done one of two things: returned an HttpResponse with a plain string, or returned a JSON object with JsonResponse. That works when you're building an API, but it fails the moment you want to show a user-facing interface. You need:
- A way to generate real HTML that a browser understands.
- A way to inject dynamic content (variable values, lists, user names, database records) into that HTML — not hard-coded text.
- A way to keep presentation separate from logic, so you can update your design without touching Python.
If you skip templates and put HTML directly in your views, you'll end up with return HttpResponse('<html><body><h1>' + name + '</h1></body></html>') — a string-manipulation nightmare that becomes a security risk the moment a user enters a value with a <script> tag. Templates solve this by giving you a declarative, safe, and reusable grammar to render dynamic pages with Django templates.
By the end of this lesson, you'll be able to convert any view into a templated response, pass data from your Python code into the page, and write template logic that stays simple and testable.
Core concept / mental model
Think of a Django template as a blueprint or fill-in-the-blank form. The HTML skeleton is written once, with placeholder spots marked by double curly braces {{ ... }}. Your view function is the supplier of data — it prepares a dictionary of values (called the context) and hands it to Django's renderer. Django then fills every placeholder with the matching value and produces a complete HTML string that gets sent to the browser.
Here's the mental model in one line:
View prepares the data → Template defines the layout → Django mix them → User sees a page.
This separation is the core of Model-View-Template (MVT) — Django's take on the classic MVC pattern. Instead of a controller, Django's URL resolver calls a view, which pulls data (often from a database via models, or just from user input) and passes it to a template. The template is pure presentation: it knows where to show things, but never how to compute them.
This separation gives you three immediate superpowers:
- Reusability — One template can be used by many views. A user profile card template can render for any user ID.
- Maintainability — Your HTML lives in
.htmlfiles, not inside Python functions. Designers can edit it without touching your code. - Security — Django auto-escapes every variable by default, so user-supplied content won't break your page or inject scripts.
How it works step by step
Rendering a dynamic page with Django templates is a four-step pipeline, and each piece is small on its own. Once you see the sequence, you'll be able to debug any template issue by walking through the same checks.
Step 1: Create a template file
Django expects templates to live in a templates/ directory inside your app (or in a project-level folder if you've configured DIRS in settings.py). For a new app named blog, you'd create:
blog/
templates/
blog/
post_list.html
post_detail.html
The extra blog/ folder inside templates/ is a best practice — it avoids name collisions when two apps both have a post_list.html.
Step 2: Write template markup
Inside post_list.html, you write standard HTML with Django's template tags and variables. For example:
<!DOCTYPE html>
<html>
<head>
<title>{{ page_title }}</title>
</head>
<body>
<h1>{{ page_title }}</h1>
<ul>
{% for post in posts %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
</body>
</html>
Here {{ page_title }} is a template variable, and {% for %} is a template tag that loops over a list you'll pass from the view.
Step 3: Pass data from the view
In your views.py, you build a dictionary with the values that match the variables in the template, then call Django's render helper:
from django.shortcuts import render
def post_list(request):
posts = [
{'title': 'First post', 'id': 1},
{'title': 'Second post', 'id': 2},
]
context = {
'page_title': 'Blog Posts',
'posts': posts,
}
return render(request, 'blog/post_list.html', context)
Step 4: Let Django render and respond
The render() function does three things for you:
1. Loads the template from disk.
2. Fills in every {{ variable }} and executes every {% tag %} using the context dictionary.
3. Returns an HttpResponse with the final HTML.
That's it — the four steps form a complete loop. The URL dispatcher calls the view, the view prepares the context, the template renders, and the user sees a dynamic page.
Hands-on walkthrough
Now let's apply the mental model in a concrete project. I'll assume you already have a Django project with an app named blog. If not, you can follow along with any app you've built in previous lessons.
1. Set up your template directory
Make sure your app has a templates/ folder and that Django knows about it. For app-level templates, no extra configuration is needed — Django looks for templates/ inside each installed app by default. (If you're using a project-level templates/ directory, add it to DIRS in settings.py as shown later.)
Create the file blog/templates/blog/post_detail.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ post.title }}</title>
</head>
<body>
<h1>{{ post.title }}</h1>
<p>Written by <strong>{{ post.author }}</strong></p>
<div class="content">
{{ post.content }}
</div>
</body>
</html>
2. Write a view that passes dynamic data
In blog/views.py, add a view that provides a single post's data. For now we'll use a hardcoded dictionary — in a later lesson on models, you'll pull this from the database.
from django.shortcuts import render
def post_detail(request, post_id):
# In a real app, you'd fetch this from the database using the post_id.
post = {
'title': f'Post number {post_id}',
'author': 'Ada Lovelace',
'content': 'This is a dynamically rendered page from Django templates.',
}
context = {'post': post}
return render(request, 'blog/post_detail.html', context)
Note how we pass a dictionary post — the template accesses its keys with the dot notation {{ post.title }}.
3. Wire up a URL
In blog/urls.py (or your project's urls.py), map a URL pattern to this view:
path('posts/<int:post_id>/', views.post_detail, name='post_detail')
4. Test it
Start the development server and visit http://127.0.0.1:8000/posts/7/. You'll see a page that says Post number 7 — the data comes from your view, not the HTML file. Change the post_id to 8, and you get a different title. That's dynamic rendering in action.
Expected output in the browser: the heading shows Post number 7, the author's name is bold, and the content paragraph appears below.
Now, expand that to a list page. Create blog/templates/blog/post_list.html:
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
</head>
<body>
<h1>All Posts</h1>
{% for post in posts %}
<article>
<h2><a href="/posts/{{ post.id }}/">{{ post.title }}</a></h2>
<p>By {{ post.author }}</p>
</article>
{% empty %}
<p>No posts yet. Check back soon!</p>
{% endfor %}
</body>
</html>
And a view that sends a list:
def post_list(request):
posts = [
{'id': 1, 'title': 'Getting Started', 'author': 'Ada'},
{'id': 2, 'title': 'Advanced Tricks', 'author': 'Alan'},
]
return render(request, 'blog/post_list.html', {'posts': posts})
Visit /posts/ and you'll see two article titles, each linking to its detail page. The {% empty %} tag is a pro tip — it shows a fallback when the list is empty, so your page never looks broken.
Compare options / when to choose what
You now have three main ways to return a response from a view. Choosing the right one keeps your app clean and your performance on target.
| Approach | Best for | Example | Downsides |
|---|---|---|---|
Static HttpResponse |
Quick tests, simple text responses | return HttpResponse("Hello") |
No formatting, hard-coded content |
JsonResponse |
APIs, AJAX endpoints, mobile backends | return JsonResponse({'ok': True}) |
Not human-readable as a webpage |
Django templates with render() |
Dynamic user-facing web pages | return render(request, 'template.html', context) |
Requires template files & context setup |
Inside templates themselves, you have further choices. For basic variable substitution and for loops, built-in tags are enough. If you need more logic, you can create custom template filters. For highly modular pages, you'll use template inheritance (covered in the next lesson) with {% extends %} and {% block %} — that's the recommended approach for larger sites.
When to choose templates over JsonResponse: if your audience is a browser, not a script. Even if you're building a single-page app with a JavaScript front end, Django templates are still great for the initial HTML shell. If your API is purely data-driven — say, a mobile app consuming JSON — skip templates and use JsonResponse instead.
Troubleshooting & edge cases
Even the cleanest template pipeline can trip you up. Here are the most common failure modes and their fixes.
The page shows a blank or raw HTML
If you see the literal text {{ post.title }} on the page, the template wasn't processed. Common causes:
- You used HttpResponse instead of render().
- You passed the context as a second positional argument incorrectly — render(request, template, context) is the order.
- Your template file isn't being found (see next item).
TemplateDoesNotExist at /posts/7/
This means Django can't locate your template. Check:
- Is the file at blog/templates/blog/post_detail.html? (Note the double blog/ folder.)
- Is your app listed in INSTALLED_APPS in settings.py?
- If you're using a project-level template directory, did you add it to DIRS? Example for settings.py:
# settings.py
import os
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')], # project-level templates
'APP_DIRS': True, # looks inside each app's templates/ folder
# ... other settings
},
]
Context variable doesn't show up
If you pass {'post': post} but your template uses {{ posts.name }}, you'll get nothing (or an error). Django's dot notation does dict lookup, attribute lookup, and index lookup in order, but if the key doesn't exist, it silently renders an empty string. Use {% if %} tags to conditionally display optional fields:
{% if post.optional_field %}
<p>{{ post.optional_field }}</p>
{% endif %}
Variable contains HTML that renders as text
Django auto-escapes all variable output by default. This prevents XSS attacks but can be surprising when you want to render rich text. If you trust the content, you can turn off auto-escaping for that part with the safe filter:
{{ post.content|safe }}
But never use |safe on user-submitted data without sanitizing it first — that's an open door for cross-site scripting.
What you learned & what's next
You've just unlocked the full power of rendering dynamic pages with Django templates. You now understand:
- Why templates are the professional way to generate HTML, keeping presentation separate from Python logic.
- How the view-context-template pipeline works: your view prepares a dictionary of data, Django merges it with an HTML blueprint, and a complete page is returned.
- How to use
{{ variable }}for substitution and{% for %}/{% if %}tags for logic in your templates. - How to pass both a single object (like a post) and a list of objects (like a collection of posts) to a template.
- How to troubleshoot common errors:
TemplateDoesNotExist, missing context, and auto-escaping surprises.
You've completed a hands-on walkthrough where you created a post detail page and a list page with dynamic links. This is the foundation for every user-facing Django feature you'll build from here on.
In the next lesson, you'll take templates to the next level with template inheritance — building a base layout for your whole site, removing duplication, and letting individual pages fill in blocks. You'll never write the same navigation bar twice again. Get ready to make your pages clean, consistent, and maintainable.
One more thing to remember: the more logic you put in your templates, the harder they are to test. Keep loops and conditionals simple; if you need heavy computation, do it in the view. Your templates should read like a blueprint, not a Python script — and Django gives you all the tools to keep it that way.
Practice recap
Create a new view and template that lists items from a hardcoded list (like your favorite movies) and link each item to a detail page. Pass the item ID via the URL and render its name in a detail template. Try adding an {% empty %} block and test with an empty list. Then try switching to a project-level template directory to see how the DIRS setting changes where Django looks.
Common mistakes
- Forgetting to pass the context dictionary — you call
render(request, 'template.html')without the third argument, so variables like{{ title }}render as empty strings. - Misplacing template files — not creating the
templates/folder inside your app, or putting the file intemplates/instead oftemplates/blog/, which makes Django raiseTemplateDoesNotExist. - Using
HttpResponsewith a template string instead ofrender()— you end up with raw variable tags printed on the page. - Overusing
|safeon user-generated content — you disable Django's auto-escaping and open your site to XSS attacks. - Putting heavy logic in templates — you try
{% with %}and complex conditionals instead of computing values in the view, making templates hard to debug and test.
Variations
- Use
render_to_string()when you need the rendered HTML as a string for emails or AJAX responses, instead of directly returning anHttpResponsefromrender(). - Adopt a project-wide
templates/directory (configured viaDIRSinsettings.py) instead of app-level templates — ideal when you share a common base layout across multiple apps. - Consider optional third-party template engines like Jinja2, which Django supports natively as an alternative backend with a different syntax and features.
Real-world use cases
- Rendering a blog post page where the title, author, and content come from a database record, with URLs like
/posts/3/. - Building a dashboard that shows a user's account information in a HTML template after they log in.
- Displaying a product list in an e-commerce store with each item's name, price, and link, generated in a for loop.
Key takeaways
- Templates separate presentation from logic: HTML lives in .html files, data lives in the view's context.
render(request, 'template.html', context)is the go-to shortcut — it loads, fills, and returns anHttpResponsein one step.- Use
{{ variable }}for substitution,{% for %}/{% if %}for logic, and{% empty %}to handle empty lists gracefully. - Django auto-escapes all template variables for XSS protection — use
|safeonly on trusted content. - Keep template logic simple; do heavy computation in the view so templates stay easy to read and test.
- Always include the app name in your template filename (e.g.,
templates/blog/post_list.html) to avoid naming conflicts between apps.
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.