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.

ConverterMatchesExample captured value
str (default)Any non-empty string, excluding /'weekly-report'
intOne or more digits42 (a Python int)
slugLetters, numbers, hyphens, and underscores'client-onboarding'
uuidA formatted UUID stringUUID('075194d3-...')
pathAny 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 keyHuman-readable slug
Setup effortNone — every model already has an idNeeds a dedicated field, kept unique
ReadabilityMeaningless to a human (/tasks/42/)Descriptive (/projects/client-onboarding/)
Best fit in TaskFlowTask — internal, rarely shared as a linkProject — 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 no Task with that primary key exists, Task.objects.get(pk=task_id) raises an exception rather than returning anything.
Note: That second case is common enough that Django ships a shortcut for it, 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.
Next up: Now that a view can receive captured values, it's time to look at what a view function actually does with them — head to Function-Based Views.