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.
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 insteadreverse(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 namespace — blog: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 belowsuccess_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()?