Маршруты в Django: как URLconf находит нужную view
Разбираем, как Django сопоставляет входящий путь с вашими маршрутами, почему порядок в urlpatterns решает всё и где ловушки с path, re_path и include.

Что происходит между запросом и view
Пользователь открывает /blog/42/, а в ответ прилетает 404 — хотя view для статьи явно написана. Причина почти всегда в маршрутизации: Django не нашёл совпадения либо нашёл не то, что вы ожидали. Разберём механизм на конкретных примерах для Django 5.x (для 4.2 всё описанное тоже верно) и покажем, где именно ломается сопоставление.
URL-маршрутизация в Django — это не магия, а последовательный перебор. Есть корневой модуль, указанный в настройке ROOT_URLCONF, в нём список urlpatterns, и Django идёт по этому списку сверху вниз, беря первое совпадение. Понимание этого перебора снимает большую часть вопросов «почему открывается не та страница».
Как читается urlpatterns
Django берёт путь из запроса — уже без домена и без query-строки. Для https://example.com/blog/42/?ref=vk в маршрутизацию попадёт строка blog/42/ (ведущий слэш отбрасывается). Дальше движок идёт по urlpatterns и на каждом элементе проверяет, совпадает ли начало пути с шаблоном.
# myproject/urls.py — Django 5.x
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
path('', include('pages.urls')),
]Порядок здесь не косметика. Django останавливается на первом совпадении, поэтому более общие шаблоны нельзя ставить выше специфичных — иначе они перехватят запрос.
path и конвертеры
Функция path() использует конвертеры пути — куски в угловых скобках вида <тип:имя>. В файле blog/urls.py:
# blog/urls.py — Django 5.x
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='list'),
path('<int:post_id>/', views.post_detail, name='detail'),
path('<slug:slug>/', views.post_by_slug, name='by_slug'),
]Конвертер int совпадает только с цифрами и передаёт во view уже число, а не строку. Конвертер slug ловит буквы, цифры, дефисы и подчёркивания. Встроенные конвертеры: str (по умолчанию, любой текст без слэша), int, slug, uuid, path (включая слэши).
Тонкость с порядком видна прямо тут. Запрос /blog/42/ совпадёт с <int:post_id>/, потому что 42 — цифры. А /blog/django-5/ дойдёт до <slug:slug>/. Но если поменять эти две строки местами, slug перехватит и 42 тоже — цифры это валидный slug. View по id перестанет вызываться, хотя ошибки не будет никакой.
Django не выбирает «лучшее» совпадение. Он берёт первое сверху. Более специфичный маршрут всегда должен стоять выше более общего.
Когда нужен re_path
Если шаблона path не хватает — например, нужен год из четырёх цифр строго — берут re_path с регулярным выражением:
from django.urls import re_path
from . import views
urlpatterns = [
re_path(r'^archive/(?P<year>[0-9]{4})/$', views.archive),
]Именованная группа (?P<year>...) передаётся во view как аргумент year. Частая ошибка новичков — забыть ^ и $: без них выражение совпадёт с частью пути, и /archive/2024/extra/ тоже пройдёт.
include и вложенность
Когда движок доходит до path('blog/', include('blog.urls')) и запрос начинается с blog/, Django откусывает совпавший префикс и передаёт остаток во вложенный urlpatterns. Для /blog/42/ в blog/urls.py уйдёт уже 42/. Поэтому в дочернем файле маршруты пишут без повторения blog/ — это одна из самых частых ошибок при разбиении проекта на приложения.
Как имена превращаются в ссылки
Атрибут app_name = 'blog' и name='detail' дают возможность строить URL по имени, а не хардкодить строки. В шаблоне:
<a href="{% url 'blog:detail' post_id=post.id %}">{{ post.title }}</a>В коде — через reverse:
from django.urls import reverse
url = reverse('blog:detail', kwargs={'post_id': 42})
# '/blog/42/'Смысл в том, что при смене структуры URL менять придётся один файл маршрутов, а не все шаблоны и вьюхи. Хардкод путей в href — это отложенная боль.
Где сопоставление ломается на практике
Разберём частые ситуации, когда открывается не то, что ждали.
- Слэш в конце. При
APPEND_SLASH = True(значение по умолчанию) запрос/blog/42без совпадения перенаправится на/blog/42/редиректом 301. Но редирект превращает POST в GET и теряет тело запроса — форма, отправленная на URL без слэша, сломается тихо. - Общий маршрут выше специфичного.
path('<slug:slug>/', ...)надpath('about/', ...)перехватит/about/, потому чтоabout— валидный slug. - Забытый include в корне. Приложение написано, но
path('blog/', include('blog.urls'))не добавлен в корневой файл — все URL приложения дают 404. - path вместо path в конвертере. Конвертер
<path:rest>ловит слэши и съедает то, что должно достаться следующим маршрутам. Ставьте его максимально низко в списке.
Как отладить маршрутизацию
Когда непонятно, какой маршрут сработал, включённая страница отладки помогает напрямую. При DEBUG = True и 404 Django показывает весь список urlpatterns в том порядке, в котором он их проверял, и подсвечивает, до какого дошёл. Это самый быстрый способ увидеть, что общий шаблон стоит выше нужного.
Для проверки из shell:
from django.urls import resolve
match = resolve('/blog/42/')
print(match.view_name) # 'blog:detail'
print(match.kwargs) # {'post_id': 42}Функция resolve делает ровно то, что делает Django на входящем запросе — сопоставляет путь с urlpatterns и возвращает найденную view. Обратная к ней — reverse. Пара resolve/reverse — основной инструмент для проверки, что маршруты собраны так, как задумано.
Порядок конвертеров: короткая таблица
| Конвертер | Совпадает с | Тип во view |
|---|---|---|
str | любой текст без / | str |
int | ноль и положительные цифры | int |
slug | буквы, цифры, -, _ | str |
uuid | формат UUID с дефисами | UUID |
path | любой текст, включая / | str |
Отсюда правило порядка: чем уже конвертер ловит, тем выше он стоит. int выше slug, slug выше str, path — в самом низу.
Что делать дальше
Соберите проверку в привычку: после каждой правки urls.py прогоняйте resolve на паре характерных путей в shell — это ловит перехваченные маршруты до того, как их найдёт пользователь. Для крупных проектов посмотрите пользовательские конвертеры пути (register_converter) — они позволяют вынести повторяющиеся регулярки из re_path в переиспользуемый класс. Инвалидация и кеширование reverse — отдельная тема, здесь мы разбирали только само сопоставление.
Турагент по РФ: Маршруты по Алтаю, Кавказу, Карелии и расчёт бюджета.
Спросить за 12 ₽Источники: Django documentation — URL dispatcher, Django documentation — django.urls functions and path converters
Частые вопросы
У меня Django 4.2, а не 5.x — примеры сработают?
Открывается не та view, хотя маршрут написан правильно. Что смотреть первым?
В чём разница между path и re_path?
Почему форма ломается при отправке, хотя страница открывается?
Как проверить все маршруты проекта, не открывая каждую страницу?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.