skip to content

In a Django URLconf listing path('events/<slug:slug>/') before path('events/new/'), which view serves /events/new/, and why?

level: juniorimportance: must knowfreq 55%

answer

  1. list order, not specificity
  2. does 'new' fit the slug regex
  3. first full match wins
  4. static routes go above converters

basics

~20 s

The slug route serves it: Django tries urlpatterns in list order and calls the first pattern that matches, and 'new' is a valid slug. Listing the fixed events/new/ route above the converter route fixes it.

solid answer

~40 s

Django's resolver walks `urlpatterns` top to bottom and calls the view of the first entry that matches the whole path; there is no ranking by specificity, so a literal route listed below a converter route that accepts the same text is unreachable. `new` fits the slug regex `[-a-zA-Z0-9_]+`, so `event_detail(request, slug='new')` runs and probably returns a 404 for a missing event. The fix is to list literal routes first, or make the routes structurally distinct. A pattern declines only when its text does not fit: a literal part differs, a converter's regex rejects the segment, or a custom converter's `to_python()` raises `ValueError`. That is why the bug often appears after switching from `<int:pk>` to slugs: `int` never accepted `new`.

code

python · 9 lines
python
from django.urls import path

from . import views

urlpatterns = [
    path('events/new/', views.event_create, name='event-create'),
    path('events/<slug:slug>/', views.event_detail, name='event-detail'),
    path('events/<slug:slug>/edit/', views.event_edit, name='event-edit'),
]

go deeper

for a junior

Recall that Django tries urlpatterns in order and the first matching entry wins, so literal routes belong above converter routes that could accept the same text.

for a middle

Explain what makes a pattern decline (literal mismatch, converter regex, ValueError from to_python) and why switching from int keys to slugs can shadow an existing route.

for a senior

Show how you prevent shadowing in a growing URLconf: reserved-word validation, tests per route, catch-alls last, and grouping routes so ordering stays reviewable.

for a principal

Weigh URL designs that cannot collide, such as separate prefixes for actions, against flatter URLs that rely on list order and reserved words.

## How Django walks a URLconf For each request, Django takes `request.path_info` and hands it to the root resolver built from the `ROOT_URLCONF` module. The resolver goes through `urlpatterns` **in list order**. For each entry it asks the pattern whether it matches; the first `path()` or `re_path()` entry that matches the whole remaining path wins, and its view is called. Entries after it are not consulted for that request. Three consequences follow: - There is **no specificity ranking**. Django does not prefer literal routes over converter routes, longer routes over shorter ones, or named routes over unnamed ones. - A pattern **declines** only when its text does not fit: a literal part differs, a converter's regex rejects a segment, or a custom converter's `to_python()` raises `ValueError`. - An `include()` entry is tried in place: its prefix is matched, then its own list is walked in order, and if nothing inside matches, the outer list carries on with the next entry. Django's documentation shows ordering as a deliberate tool: a special-cased `articles/2003/` route placed above `articles/<int:year>/`. ## The shadowed route ```python from django.urls import path from . import views urlpatterns = [ path('events/<slug:slug>/', views.event_detail, name='event-detail'), path('events/new/', views.event_create, name='event-create'), # never reached ] ``` `new` consists of ASCII letters, so it satisfies the `slug` converter's regex. The first pattern matches `/events/new/`, and Django calls `event_detail(request, slug='new')`. The view looks for an event whose slug is `new`, finds none, and the organiser sees a 404 on the page that should create an event. Links to `event-create` still render, because building a URL from a route name does not consult resolution order; only following the link reveals the problem. ## Fixing it 1. **List literal routes first.** Put `events/new/` above `events/<slug:slug>/`. This is the idiomatic fix. 2. **Keep the words out of the data.** Once `new` is routed first, an event whose slug is `new` can never be reached. Validate event slugs against the reserved words (`new`, `edit`, `search`) when events are created. 3. **Or make the converter decline.** A custom converter whose `to_python()` raises `ValueError` for reserved words makes that pattern stop matching, so resolution falls through to the next entry regardless of order. 4. **Or separate the shapes.** Routes such as `events/<slug:slug>/` and `manage/events/new/` cannot collide at all. ## Why the bug appears after a refactor Which view answers `/events/new/` depends on what the earlier converter accepts: | Route listed first | Does `new` fit? | View that runs | |---|---|---| | `events/<int:pk>/` | no, digits only | `event_create` | | `events/<slug:slug>/` | yes | `event_detail` | | `events/<str:name>/` | yes | `event_detail` | | `events/<path:rest>` | yes, and deeper paths too | `event_detail` | Moving from integer keys to slugs silently widens what the converter accepts, and a route that worked for years goes dark without any code near it changing. ## Diagnosing and preventing it - Django's system checks do **not** report shadowed routes; `manage.py check` is silent about this. - A test that requests `/events/new/` and asserts the create form renders catches the regression immediately. - Resolving the path in a shell shows which named pattern won. - Keep catch-all routes, such as a `<path:...>` route for CMS pages, at the very end of the list. - Group routes per resource and keep each group's literal routes above its converter routes, so reviewers can see the order at a glance. ## Ordering across a whole project The same rule applies at every level of the URLconf tree, which is where it bites in larger projects: - **The root URLconf is ordered too.** An app's `include()` mounted at `''` near the top can capture paths meant for entries lower in the root list, if one of its converter routes fits them. - **Converter width matters more than route length.** `<str:...>` and `<path:...>` accept almost anything, so a route built on them belongs after every narrower sibling. - **Adding a route is an ordering decision.** Code review should ask where a new entry sits, not just whether it is correct in isolation. | Placement | Typical routes | |---|---| | Top of a group | literal action routes: `events/new/`, `events/search/` | | Middle | typed routes: `events/<slug:slug>/`, `events/<slug:slug>/edit/` | | Bottom of the list | catch-alls: `<path:page>/` for CMS or static pages | Following that layout, list order and intuition agree, and the first-match rule stops being a surprise.

  • How can a custom Django path converter keep reserved words such as new out of a slug route?
    Give it the slug regex and make `to_python()` raise `ValueError` when the value is in a reserved set. Django treats that as no match and tries the following patterns, so `events/new/` can sit anywhere in the list. The cost is a set lookup per resolution, and the reserved list must stay in step with the routes.
  • Why doesn't Django pick the most specific matching route automatically?
    `urlpatterns` is an ordinary Python list, and the documented contract is that patterns are tried in order, with the first match winning. That makes the outcome predictable and lets you place special cases deliberately. Ranking arbitrary `re_path()` regexes by specificity would have no clear definition.

A Django URLconf is like a mail sorter reading pigeonhole labels from the top row down and dropping each letter into the first slot whose label fits, even when a more exact label sits a few rows lower.

saying these in an interview costs you the question

  • Django picks the most specific route, so literal paths always beat converter routes.
  • manage.py check warns when one route shadows another.
  • Giving a route a name= raises its matching priority.
  • A converter route that wrongly matches raises an error instead of calling its view.
  • Order in urlpatterns is irrelevant because Django compiles all routes into one lookup table.