Django

URL namespacing and reverse() — never hardcode a URL

A hardcoded URL string breaks the instant a route changes; reverse() and {% url %} build the URL FROM the name Django already has, so renaming a path is a one-line change instead of a project-wide find-and-replace.

Beginner

3 min read

The problem with a hardcoded path

# a redirect with the URL written out literally
return redirect("/articles/create/")
<a href="/articles/create/">New article</a>

Both of these work — until path("articles/create/", ...) in urls.py changes to something else, at which point every hardcoded copy of that string, scattered across views and templates, silently points at the wrong place. Nothing catches this at test time or even at request time for the template case — a stale link just quietly 404s.

Naming a URL pattern

# urls.py
from django.urls import path
from . import views
 
urlpatterns = [
    path("articles/create/", views.article_create, name="article-create"),
    path("articles/<int:pk>/", views.article_detail, name="article-detail"),
]

The name= argument on path() gives that route a stable identifier, independent of the actual URL string — this name is what reverse() and {% url %} look up. Renaming the path itself (articles/create/ to articles/new/) requires changing exactly one line, here, and every reverse("article-create") or {% url "article-create" %} elsewhere in the codebase keeps working without modification.

reverse(): building a URL from its name, in Python

from django.urls import reverse
 
reverse("article-create")            # '/articles/create/'
reverse("article-detail", args=[42])   # '/articles/42/'
reverse("article-detail", kwargs={"pk": 42})   # '/articles/42/' — same result, by keyword instead

reverse(name, ...) looks up the named pattern and builds the actual URL string, filling in any required path parameters (<int:pk>) via args (positional) or kwargs (by name). This is the standard way to build a URL anywhere in Python code — a redirect target, an email link, an API response — instead of writing the path out as a literal string.

{% url %}: the same thing, inside a template

<a href="{% url 'article-detail' article.pk %}">{{ article.title }}</a>
<form action="{% url 'article-create' %}" method="post">

{% url %} is reverse() for templates — same lookup by name, same parameter-filling, and the same benefit: the template never contains a literal path, so a route change never requires editing a template. This is the default, expected way to link to any internal page in a Django template; a hardcoded href="/articles/..." in a template is a near-guaranteed spot for a stale link once the project has been around for a while.

App namespacing: the same name, used by more than one app

# blog/urls.py
app_name = "blog"
urlpatterns = [
    path("", views.post_list, name="list"),
]
 
# shop/urls.py
app_name = "shop"
urlpatterns = [
    path("", views.product_list, name="list"),
]
 
# project urls.py
urlpatterns = [
    path("blog/", include("blog.urls")),
    path("shop/", include("shop.urls")),
]
{% url 'blog:list' %}     {# resolves to blog's post_list, not shop's #}
{% url 'shop:list' %}      {# resolves to shop's product_list #}

Without app_name, two apps both naming a pattern "list" collide — Django can't tell which one reverse("list") should mean, and whichever was registered last effectively wins, silently breaking the other. app_name = "blog" in an app's urls.py, combined with include("blog.urls") in the project's root urls.py, creates a namespaceblog:list and shop:list are then unambiguous, letting every reusable app define its own URL names freely without coordinating names against every other app in the project.

reverse_lazy: for places evaluated before URLs are loaded

from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
 
class ArticleCreateView(CreateView):
    model = Article
    fields = ["title", "body"]
    success_url = reverse_lazy("article-list")   # NOT reverse() — see why below

success_url = reverse("article-list") would fail at class-definition time — class attributes are evaluated when the module is first imported, which can happen before Django has finished loading all URL patterns. reverse_lazy() returns a lazy object that only actually resolves the URL the first time it's used (when the view actually runs), sidestepping that ordering problem entirely — the rule of thumb is reverse_lazy for anything evaluated at class-definition or module-import time, plain reverse() everywhere else (inside a function body, which only runs later, after everything is loaded).

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does a hardcoded href="/articles/create/" in a template break easily?

2. What does reverse('article-detail', args=[42]) do?

3. Why does app_name = 'blog' in an app's urls.py matter when two apps both name a pattern 'list'?

4. Why does success_url on a CreateView need reverse_lazy() instead of reverse()?