Filter concepts by levelShowing all levels.

Django · URL Routing

URL Routing

Regex routes, namespaces, reversing URLs by name, and versioning conventions

  • re_path() captures are always stringsunlike path()'s typed converters
  • app_name + namespace=disambiguate an app included more than once
  • reverse_lazy() for class attributesevaluated before urls.py finishes loading
  • Version by URL prefix/api/v1/ and /api/v2/, separate urls.py each

1.re_path() and regex patterns

re_path() matches a URL with a regular expression instead of path()'s converter syntax — named groups ((?P<name>...)) capture values, but every captured value arrives as a plain string, unlike path()'s automatic int/slug/uuid conversion.

Key features

  • Named groups use (?P<name>pattern) — django.urls.path() equivalents like <int:name> don't exist in regex syntax
  • Every captured value is a plain str — re_path() has no equivalent of path()'s type converters
  • Reach for re_path() only when path()'s five built-in converters (str, int, slug, uuid, path) can't express the pattern
  • A regex mistake (wrong anchor, missing escape) just fails to match — no error, the request 404s or falls through to the next pattern
  • path() is preferred by the docs for anything expressible with a converter — re_path() is the escape hatch, not the default

path() vs re_path() for the same route

path() vs re_path() for the same route
Aspectpath()re_path()
Syntax<int:year>(?P<year>[0-9]{4})
Captured typeint (converted)str (always)
Readable by non-regex usersyesno
Expresses arbitrary patternsonly via a custom converteryes, directly

Command

python
from django.urls import re_path

urlpatterns = [
    re_path(r"^articles/(?P<year>[0-9]{4})/$", views.year_archive, name="year-archive"),
]

re_path() matches with a regex and captures everything as a string — reach for it only when path()'s five converters genuinely can't express the pattern.

2.URL namespaces

app_name in an app's urls.py declares its application namespace; include()'s namespace= argument sets an instance namespace when the same app is included more than once. Together they let "polls:index" resolve unambiguously even if two different "polls" URLconfs are both installed.

Key features

  • app_name is set once, inside the included app's own urls.py module
  • namespace= is set per-include(), in the including (usually root) urls.py — defaults to app_name if omitted
  • reverse("polls:index") and {% url "polls:index" %} both use the colon syntax to target a namespace
  • Two include()s of the same app.urls need different namespace= values, or reverse() can't tell them apart
  • Namespaces can nest: "parent:child:view-name" for an app included inside another app's own included URLs

app_name vs. include()'s namespace=

app_name vs. include()'s namespace=
PieceSet inPurpose
app_namethe app's own urls.pydeclares the app's default namespace
namespace=include() in the including urls.pyoverrides the instance namespace for THIS include
"app_name:view_name"reverse()/{% url %}targets the single/default instance
"instance_ns:view_name"reverse()/{% url %}targets a specific instance when included more than once
The same app, mounted twice, disambiguated by namespace

polls/urls.py

app_name = "polls"

declared once, in the app itself

author-polls/

namespace="author-polls"

reverse("author-polls:index")

publisher-polls/

namespace="publisher-polls"

reverse("publisher-polls:index")

  • polls/urls.py
    • app_name = "polls" — declared once, in the app itself
  • author-polls/
    • namespace="author-polls" — reverse("author-polls:index")
  • publisher-polls/
    • namespace="publisher-polls" — reverse("publisher-polls:index")

Command

python
reverse("polls:detail", kwargs={"pk": 5})
# in a template:
# {% url 'polls:detail' pk=poll.id %}

app_name declares an app's namespace once; include()'s namespace= sets a specific instance's namespace — required whenever the same app is included more than once.

3.reverse() and reverse_lazy()

reverse("name", kwargs={...}) turns a URL name back into a real path string, immediately. reverse_lazy() does the same but defers resolution until the value is actually accessed — required in places evaluated at import/class-definition time, like a class-based view's success_url.

Key features

  • reverse(viewname, args=None, kwargs=None) resolves immediately and returns a plain string
  • reverse_lazy() returns a lazy proxy object — looks and behaves like a string once actually used, but resolution is deferred
  • Use reverse_lazy() for a class attribute (success_url on an UpdateView), a decorator argument, or a settings.py value
  • Use reverse() everywhere else — inside a function body, a view, anywhere already running at request time
  • Both accept args (positional) or kwargs (named) — never mix the two for the same call

reverse() vs reverse_lazy()

reverse() vs reverse_lazy()
Aspectreverse()reverse_lazy()
Resolvesimmediately, when calledlazily, on first actual use
Returnsa plain stra lazy string-like proxy object
Use insidea view, a function bodya class attribute, a decorator, settings.py
Fails if urlconf not loaded yetyes, immediatelyno — deferred past that point
When each one is safe to use

reverse()

  • Resolves immediately, when called
  • Safe inside a view or function body — urls.py is already loaded
  • Fails immediately if urlconf isn't loaded yet

reverse_lazy()

  • Returns a lazy proxy — resolves on first actual use
  • Required for a class attribute (success_url), a decorator, settings.py
  • Safe even before urls.py has finished loading
  • reverse()
    • Resolves immediately, when called
    • Safe inside a view or function body — urls.py is already loaded
    • Fails immediately if urlconf isn't loaded yet
  • reverse_lazy()
    • Returns a lazy proxy — resolves on first actual use
    • Required for a class attribute (success_url), a decorator, settings.py
    • Safe even before urls.py has finished loading

Command

python
reverse("articles:year-archive", kwargs={"year": 2012})
# -> "/articles/2012/"
reverse("articles:year-archive", args=(2012,))
# -> "/articles/2012/" — same result, positional

reverse() resolves immediately — use it in views and functions. reverse_lazy() defers resolution — use it anywhere evaluated at import/class-definition time (success_url, settings.py, decorators).

4.Versioned URLs and naming conventions

URL-based API versioning puts the version in the path itself (/api/v1/orders/), included via a separate urls.py per version; naming conventions (prefixing a URL name with the app, e.g. "polls-detail" instead of "detail") avoid collisions when many apps define similarly-named views.

Key features

  • URL-path versioning (/api/v1/...) is the most visible, cache-friendly form — the version is explicit in every request
  • Each version usually gets its own urls.py, included under its own prefix, so v1 and v2 can diverge freely
  • App namespaces (see URL namespaces) solve most naming collisions — a naming convention matters most for un-namespaced, project-level names
  • A URL name should describe the resource and action ("order-detail", "order-cancel"), not the HTTP method — the method is already in the request
  • Consistent past-tense-free, hyphenated names ("order-detail" not "OrderDetail" or "get_order") make reverse() calls predictable across a codebase

Versioning approaches, in order of visibility

Versioning approaches, in order of visibility
ApproachExampleTrade-off
URL path/api/v1/orders/most visible, cacheable, but couples the version to routing
Query parameter/api/orders/?version=1easy to default, easy to forget to pass
Custom headerAccept: application/vnd.myapi.v1+jsoninvisible in URLs/logs, harder to test by hand

Command

python
path("orders/<int:pk>/", views.OrderDetailView.as_view(), name="order-detail")
path("orders/<int:pk>/cancel/", views.OrderCancelView.as_view(), name="order-cancel")

Version an API by path prefix, backed by separate urls.py per version; name a URL after the resource and action ("order-detail"), never the HTTP method.

Remember

  • re_path() matches with a regex and captures everything as a string — reach for it only when path() genuinely can't express the pattern.
  • app_name declares an app's namespace; include()'s namespace= overrides it per-instance.
  • reverse() resolves immediately; reverse_lazy() defers — required for class attributes like success_url.
  • Name a URL after the resource and action ("order-detail"), never the HTTP method.

Pro tip

  • Two include()s of the same app.urls need different namespace= values, or reverse() can't tell them apart.
  • Version an API by URL path prefix with a fully separate urls.py per version, so a breaking change in v2 never touches v1.

Next up

  • Function-Based Views
  • Class-Based Views
  • Templates

Sources

  • URL dispatcher — docs.djangoproject.com/en/5.2/topics/http/urls/
  • Every claim checked against Django 5.2 LTS docs · checked 2026-08-21

Spotted a mistake on this page? Tell us.