Dynamic URLs & Parameters
A fixed route like tasks/ is fine for a list, but the moment a URL needs to point at
one specific thing — a single task, a single project — a hardcoded path stops working.
Django solves this with path converters: placeholders inside a URL pattern that
capture a piece of the URL and hand it to your view as an ordinary Python argument. This lesson
covers how those captures work, how to accept more than one value in the same URL, and how to
build a URL back out from its name instead of typing the string by hand every time.
Why URLs Need to Capture Values
Imagine TaskFlow has a hundred tasks. You would never write a separate path() entry
for tasks/1/, tasks/2/, tasks/3/, and so on. Instead, you
write one pattern with a variable segment, and Django matches any value that fits
the shape you describe, passing that value straight into your view function.
# taskflow/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('tasks/', views.task_list, name='task-list'),
path('tasks/<int:task_id>/', views.task_detail, name='task-detail'),
]
The second pattern matches tasks/1/, tasks/42/, tasks/907/ —
and any other tasks/<number>/ URL — with a single line.
Path Converters: The Built-In Types
The part inside the angle brackets is written as <converter:name>. The converter
decides which characters are allowed to match, and what Python type the captured value is converted
to before it reaches your view.
| Converter | Matches | Example captured value |
|---|---|---|
str (default) | Any non-empty string, excluding / | 'weekly-report' |
int | One or more digits | 42 (a Python int) |
slug | Letters, numbers, hyphens, and underscores | 'client-onboarding' |
uuid | A formatted UUID string | UUID('075194d3-...') |
path | Any string, including forward slashes | 'archive/2025/q3' |
If you leave the converter off entirely — just <task_id> — Django assumes
str. Being explicit with <int:task_id> is almost always better: it
documents intent and rejects non-numeric values before your view code ever runs.
Capturing a Single Value: A Task Detail URL
Here is the full picture for a page that shows one TaskFlow task. The URL pattern captures the task's primary key, and the captured value arrives as a parameter with the exact same name used inside the angle brackets.
# taskflow/urls.py
path('tasks/<int:task_id>/', views.task_detail, name='task-detail')
# taskflow/views.py
def task_detail(request, task_id):
# task_id arrives here as a Python int, e.g. 42 — not the string "42"
task = Task.objects.get(pk=task_id)
return render(request, 'taskflow/task_detail.html', {'task': task})
Notice that task_id is a required positional parameter of the view function, right
after request. Every named converter in the URL pattern becomes one parameter, in the
same name, on the view.
Capturing Multiple Values in One URL
A URL pattern isn't limited to one converter. TaskFlow could nest a task's detail page under its project, so the URL itself reads as "this task, inside this project":
# taskflow/urls.py
path(
'projects/<slug:project_slug>/tasks/<int:task_id>/',
views.project_task_detail,
name='project-task-detail',
)
# taskflow/views.py
def project_task_detail(request, project_slug, task_id):
task = Task.objects.get(pk=task_id, project__slug=project_slug)
return render(request, 'taskflow/task_detail.html', {'task': task})
Both captured values are matched by name, not by position — project_slug and
task_id land on whichever parameters share those names, regardless of the order you
list them in the function signature. (This example assumes a slug field has been
added to the Project model for exactly this purpose — a short, URL-friendly stand-in
for the project's full name.)
Reversing URLs by Name
Typing out '/tasks/' + str(task.id) + '/' by hand works today and quietly breaks the
moment someone edits the URL pattern. Django's reverse() function builds the URL
from the pattern's name and its arguments, so it always matches whatever urls.py
currently says.
from django.urls import reverse
# Single captured value, positional
url = reverse('task-detail', args=[task.id])
# -> '/tasks/42/'
# Multiple captured values, by keyword
url = reverse('project-task-detail', kwargs={'project_slug': project.slug, 'task_id': task.id})
# -> '/projects/client-onboarding/tasks/42/'
Inside templates, the equivalent is the {% url %} template tag — same idea, same
underlying lookup by name, just usable directly in HTML. It gets a full treatment a few lessons
ahead in Django Template Language, but as a preview:
{% url 'task-detail' task.id %} produces the same string as the reverse()
call above.
Slugs vs IDs: Which to Use
Both converters solve the same problem — identifying one row — but they trade off differently, and TaskFlow actually uses both:
Numeric int primary key | Human-readable slug | |
|---|---|---|
| Setup effort | None — every model already has an id | Needs a dedicated field, kept unique |
| Readability | Meaningless to a human (/tasks/42/) | Descriptive (/projects/client-onboarding/) |
| Best fit in TaskFlow | Task — internal, rarely shared as a link | Project — user-facing, worth a clean URL |
When Django Raises a 404 Automatically
There are actually two different "not found" situations here, and it's worth telling them apart:
- The URL itself doesn't match anything. If a converter's rules reject the
value — say, a letter shows up where
<int:task_id>expects digits — that pattern simply doesn't match, and Django moves on to check the remaining patterns. If none of them match, the visitor gets Django's standard 404 page. - The URL matches, but the row doesn't exist.
tasks/9999/matches the pattern perfectly — 9999 is a valid integer — but if noTaskwith that primary key exists,Task.objects.get(pk=task_id)raises an exception rather than returning anything.
get_object_or_404(), which looks up a row and automatically turns a missing result into a
proper 404 response instead of an unhandled error. It's covered fully, with a full worked example, in
the next lesson.