If you've written a Django URL like this:
path('documents/<int:pk>/', views.document_detail, name='document_detail'),
...and then used {{ document.id }} inside your HTML template, you might have wondered: how does the template even know what document is?
The answer is not magic. It's a clean three-step handoff — URL → View → Template. Let's walk through each step.
Step 1: The URL Captures the ID
When a user visits /documents/5/, Django looks through your urls.py to find a matching pattern.
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('documents/<int:pk>/', views.document_detail, name='document_detail'),
]
The <int:pk> part is a URL parameter. It tells Django:
- Capture whatever integer is in that position of the URL
- Give it the name
pk - Pass it to the view function automatically
So when someone visits /documents/5/, Django extracts pk = 5 and hands it to your view.
Step 2: The View Receives the ID and Fetches the Object
Your view function gets called with the captured value as an argument:
# views.py
from django.shortcuts import render, get_object_or_404
from .models import Document
def document_detail(request, pk):
document = get_object_or_404(Document, pk=pk)
return render(request, 'documents/detail.html', {'document': document})
Here's what happens line by line:
def document_detail(request, pk)— Django passes the capturedpk=5directly here as an argument.get_object_or_404(Document, pk=pk)— This queries the database for theDocumentwithid=5. If it doesn't exist, it returns a 404 error.render(request, 'documents/detail.html', {'document': document})— This is the key part. The context dictionary{'document': document}is what makes the object available inside the template. The string'document'becomes the variable name you use in the template.
Think of the context dictionary as the bridge between Python and HTML. Whatever you put in it, the template can access.
Step 3: The Template Uses the Object
Now the template has access to the full Document object under the name document:
<!-- documents/detail.html -->
<h1>{{ document.title }}</h1>
<p>Document ID: {{ document.id }}</p>
<p>Status: {{ document.status }}</p>
<p>Created by: {{ document.author.username }}</p>
When Django renders this template, {{ document.id }} becomes 5 — because document is the Python object you fetched, and .id is just accessing its id attribute.
The Full Picture
Here's the entire flow in one view:
User visits: /documents/5/
↓
urls.py captures: pk = 5
↓
view receives: document_detail(request, pk=5)
↓
view queries DB: Document.objects.get(pk=5) → returns document object
↓
view sends context: {'document': <Document: id=5>}
↓
template renders: {{ document.id }} → "5"
A Common Mistake to Avoid
The variable name in your context dictionary must match what you use in the template.
# ✅ Correct — 'document' in context, {{ document.id }} in template
return render(request, 'detail.html', {'document': document})
# ❌ Wrong — 'doc' in context, but template uses {{ document.id }}
return render(request, 'detail.html', {'doc': document})
# This will render as empty string — no error, just blank output
Django won't throw an error if the variable name doesn't match. The template will just silently render nothing. This is a subtle bug that can be confusing at first.
Quick Recap
| Layer | What it does |
|---|---|
| urls.py | Captures the integer from the URL and names it pk |
| views.py | Receives pk, queries the database, builds context dict |
| template.html | Accesses the object using the key from the context dict |
The template itself has no idea what the URL looks like. It only knows what the view passed into the context. The view is the translator between the URL world and the template world.
{% url 'document_detail' document.id %}
Read it like this:
{% url → "go to urls.py and find the pattern named..."
'document_detail' → "...document_detail"
document.id %} → "fill in <int:id> with this id from the database"
So Django does this behind the scenes:
urls.py has → path('documents/<int:id>/', ...)
document.id = 5
result → /documents/5/
- Looks up the URL pattern by name
- Takes the id from the database object
- Builds the final URL string
That's the complete pipeline. Once you see it this way — URL captures → view fetches → context bridges → template renders — it becomes a natural mental model for every Django view you write going forward.