Should Neapolitan support URL Namespaces?
2026-05-10Yes, but the first designs reached for in #16 would introduce Neapolitan-machinery that duplicates Django primitives. Ouch. There’s a smaller change that avoids it…
Many people shy away from utilising URL namespaces, because they oftentimes don’t add much benefit for the additional effort or complexity. Furthermore, I can think of multiple ways to support namespaces in Neapolitan—all of which have some downside. As URL namespaces are a stable documented feature of the Django URL dispatcher, I strongly believe that Neapolitan should support them.
The following is an exploration of how to support URL namespaces in Neapolitan.
The Breakage
Consider the following code based on Neapolitan’s quickstart from the project’s README. We define a Bookmark model,
create a CRUDView and register the resulting URLs, which results in a functional application. Most notably we’ll see
a list view linking to all CRUD actions.
from django.db import models
from neapolitan.views import CRUDView
class Bookmark(models.Model):
url = models.URLField(unique=True)
title = models.CharField(max_length=255)
note = models.TextField(blank=True)
class BookmarkCRUDView(CRUDView):
model = Bookmark
fields = ["url", "title", "note"]
urlpatterns = [
*BookmarkCRUDView.get_urls(),
]
This covers the common case. If you want to include those URLs from another app or put them in a namespace though,
Neapolitan’s url reversal will break. Setting app_name in the included module creates an implicit namespace of the
same name.
# bookmarks/urls.py
app_name = "bookmarks"
urlpatterns = [
*BookmarkCRUDView.get_urls(),
]
# urls.py
urlpatterns = [
path("", include("bookmarks.urls")), # implicit namespace 'bookmarks'
path("public/", include("bookmarks.urls", namespace="public")), # explicit namespace 'public'
]
Neapolitan isn’t aware of the namespaces and tries to reverse urls as bookmark-list, bookmark-detail etc. although
we would need to resolve them like bookmarks:bookmark-list or public:bookmark-list. The resulting list view won’t
contain links to CRUD actions as maybe_reverse simply returns None for urls that don’t have a match.
The Ask
Why do we even care about namespaces, if many people plain ignore them? I think it boils down to three use cases:
- Having names like
bookmarks:index - Including the same view multiple times
- Including the same app multiple times
I feel that people drawn to (1) tend to change their urls to bookmarks-index when encountering any namespacing
hardships. While I like to construct urls following (2), I find myself simply ditching namespaces and adding what would
have been the namespace as a prefix to the url name, which makes this use case equivalent to (1). Only (3) actually
requires using namespaces—if you want to stay sane, and in the scope of Django provided solutions.
Even if (1) and (2) have workarounds, Neapolitan composing cleanly with Django primitives is the real value.
The Primitives
Reversing namespaced URLs summarises how Django is actually reversing URLs. Let’s look at four view registration scenarios to grasp how we actually want to resolve URLs.
# bookmarks/urls.py
app_name = "bookmarks"
urlpatterns = [
path("bookmark/", BookmarkListView.as_view(), name="bookmark-list"),
path("bookmark/<int:pk>/", BookmarkDetailView.as_view(), name="bookmark-detail"),
]
# (1) registering a view
path("bookmark/", BookmarkListView.as_view(), name="bookmark-list")
path("bookmark/<int:pk>/", BookmarkDetailView.as_view(), name="bookmark-detail")
# (2) registering app URLs (implicit namespace)
path("", include("bookmarks.urls"))
# (3) registering app URLs (explicit namespace)
path("", include("bookmarks.urls", namespace="public"))
# (4) registering app URLs multiple times (different explicit namespaces)
path("", include("bookmarks.urls", namespace="internal"))
path("public/", include("bookmarks.urls", namespace="public"))
All four scenarios are valid Django. They diverge in what URL name you have to feed into reverse() or {% url %} and
that’s where Neapolitan’s current behaviour stops keeping up. The following examples are reversals from the template
rendering the bookmarks list:
- The common case you probably use every day:
{% url "bookmark-detail" bookmark.pk %}resulting inbookmark/13/. - The same thing you would do when linking to a Django Admin URL:
{% url "bookmarks:bookmark-detail" bookmark.pk %}resulting inbookmark/13/. {% url "public:bookmark-detail" bookmark.pk %}resulting inbookmark/13/again, as we replaced the implicit namespace with the explicitpublic.{% url "bookmarks:bookmark-detail" bookmark.pk %}resulting inbookmark/13/if reversed during a request onbookmark/andpublic/bookmark/13/if reversed duringpublic/bookmark/. What?!
Scenario 4 is the interesting one as we specified the app_name, which enables the reversal logic to resolve the
URL based on the matched URLResolver for the current request. Again the full logic is documented
here.
The Solution
Neapolitan’s URL namespace implementation should be coherent with Django primitives. Simply adding the following four
lines to Role.reverse makes Neapolitan behave just like Django outlined above.
def reverse(self, view, object=None):
url_name = f"{view.url_base}-{self.url_name_component}"
url_kwarg = view.lookup_url_kwarg or view.lookup_field
with suppress(AttributeError):
url_namespace = view.request.resolver_match.namespace
if url_namespace:
url_name = f"{url_namespace}:{url_name}"
match self:
case Role.LIST | Role.CREATE:
return reverse(url_name)
case _:
return reverse(
url_name,
kwargs={url_kwarg: getattr(object, view.lookup_field)},
)
If reverse was called with a dispatched view instance, we get a resolver_match that we can read the current namespace
from. That’s awesome as no Neapolitan-specific machinery is required. It just works the Django way.
Writing this post led to Neapolitan PR #94. Let’s see if the argument survives contact with the real world…
© 2026 Dennis Stritzke
Code samples are public domain unless otherwise noted.