Django template views: turning a URL into rendered HTML
A Django view's simplest job is turning a URL into an HTML page: take a request, render a template, return it as a response. This covers that whole path — a function-based view first, since it's what render() actually does under the hood, then TemplateView, the class-based shortcut for exactly this pattern, plus the template syntax and inheritance every real page ends up using.
This assumes a Django project and app already exist (django-admin startproject mysite && cd mysite && python manage.py startapp pages), with pages added to INSTALLED_APPS in settings.py. Everything below builds inside that pages app.
A minimal function-based view
# pages/views.py
from django.shortcuts import render
def home(request):
return render(request, "pages/home.html")render() is doing three things in one call: loading the template file, rendering it (with no context data yet), and wrapping the result in an HttpResponse. Every other pattern in this tutorial — including TemplateView — is a variation on exactly this.
Wiring it up: urls.py
# pages/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.home, name="home"),
]# mysite/urls.py
from django.urls import path, include
urlpatterns = [
path("", include("pages.urls")),
]include("pages.urls") is what makes the app's own urlpatterns reachable from the project's root URLconf — without it, Django never sees the routes defined inside pages/urls.py at all. The name="home" on the route lets templates and other views reference this URL by name ({% url "home" %}) instead of hardcoding the path, so renaming the route later doesn't mean hunting down every hardcoded link to it.
Passing data to the template
def home(request):
context = {
"page_title": "Welcome",
"articles": ["First post", "Second post", "Third post"],
}
return render(request, "pages/home.html", context){# pages/templates/pages/home.html #}
<h1>{{ page_title }}</h1>
<ul>
{% for article in articles %}
<li>{{ article }}</li>
{% endfor %}
</ul>context is a plain dict — every key becomes a variable available inside the template with {{ }}. {% for %} / {% endfor %} is a template tag, not Python; template tags handle control flow (loops, conditionals, includes) while {{ }} only ever outputs a value. Django looks for pages/home.html inside a templates/ directory on each app's own path by default (pages/templates/pages/home.html here) — the repeated pages/ in that path is a namespacing convention, not a typo, and it's what stops two apps that both have a home.html from silently shadowing each other.
Conditionals and filters
<p>
{% if articles %}
{{ articles|length }} article{{ articles|length|pluralize }}
{% else %}
No articles yet.
{% endif %}
</p>{% if %} works exactly like a Python conditional — truthy/falsy, no == needed for a plain existence check. The | syntax is a filter: articles|length is roughly len(articles), and filters chain left to right, so articles|length|pluralize feeds the count straight into pluralize, which prints "s" when the count isn't 1 and nothing when it is.
Writing template logic that belongs in the view instead — nested filter chains standing in for what should be a computed value passed through context. If a template line is hard to read, that's usually a sign the calculation belongs in Python, not in the template.
Template inheritance
{# pages/templates/pages/base.html #}
<!DOCTYPE html>
<html>
<head><title>{% block title %}My Site{% endblock %}</title></head>
<body>
<nav>...</nav>
{% block content %}{% endblock %}
</body>
</html>{# pages/templates/pages/home.html #}
{% extends "pages/base.html" %}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>{{ page_title }}</h1>
{% endblock %}{% extends %} must be the first tag in the file — Django rejects the template otherwise. base.html defines named {% block %} regions; every other template {% extends %} it and fills in only the blocks it cares about, inheriting everything else (the <nav>, the <!DOCTYPE>, the overall page shell) unchanged. This is what stops every page in a real project from repeating the same HTML skeleton.
Switching to TemplateView
# pages/views.py
from django.views.generic import TemplateView
class HomeView(TemplateView):
template_name = "pages/home.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Welcome"
context["articles"] = ["First post", "Second post", "Third post"]
return context# pages/urls.py
urlpatterns = [
path("", views.HomeView.as_view(), name="home"),
]TemplateView is the class-based shortcut for the exact function from the top of this tutorial: set template_name, and it handles the render() call for you. Context data goes in get_context_data() instead of a plain dict — and super().get_context_data(**kwargs) has to run first and get merged in, not skipped, since that's what carries through anything Django or a parent class already put in context (URL kwargs among them). .as_view() in the URLconf is what turns the class into something Django can actually route a request to; a bare views.HomeView (without the call) is a routing error, not a working view.
When TemplateView earns its keep
A function-based render() call and a TemplateView do the same thing here — the choice is about what the view needs to do beyond rendering. TemplateView is worth reaching for when a page's entire job is "render this template with some context," since it removes the boilerplate around that one job. The moment a view needs to branch on HTTP method (handle a POST differently from a GET), that's a sign to reach for a different generic view — or drop back to a function view, which stays more directly readable once there's real logic involved rather than pure templating.