django-stubs: Cannot mix URL patterns and included urlconfs

Bug report

What’s wrong

I cannot find a way to get the typing in urls.py right. It seems that a path that points to a view returns a URLPattern but a path that points to an included urlconf returns a URLResolver. So mixing both in a single list results in a List[object], for which mypy complains if you try to concatenate it with another list of url patterns.

For example the following:

urlpatterns = [
    path("admin/", admin.site.urls),
]

if settings.DEBUG:
    urlpatterns = (
        [
            path(
                "media/<path:path>/",
                django.views.static.serve,
                {"document_root": settings.MEDIA_ROOT, "show_indexes": True},
            ),
            path("__debug__/", include(debug_toolbar.urls)),
        ]
        + staticfiles_urlpatterns()
        + urlpatterns
    )

Results in:

myproject/config/urls.py: error: Unsupported operand types for + ("List[object]" and "List[URLPattern]")
myproject/config/urls.py: error: Incompatible types in assignment (expression has type "List[object]", variable has type "List[URLResolver]")
myproject/config/urls.py: error: Unsupported operand types for + ("List[object]" and "List[URLResolver]")

How is that should be

The code above shouldn’t trigger any errors, or maybe a section could be added to the FAQ explaining how to get typing right in urlconfs.

System information

  • OS: NixOS 20.09
  • python version: 3.7.9
  • django version: 3.1
  • mypy version: 0.790
  • django-stubs version: 1.7.0

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 8
  • Comments: 15 (5 by maintainers)

Most upvoted comments

I used this snippet, to bypass the problem:

from django.urls import URLResolver, URLPattern


URL = typing.Union[URLPattern, URLResolver]
URLList = typing.List[URL]

#<...>
urlpatterns: URLList = [
    path('', include(router.urls)),
]

I think you need to explicitly include the type for project_urls because of the way Variance and Union interact with inference in mypy:

project_urls: Sequence[Union[URLPattern, URLResolver]]] = [...

I use * unpacking

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path, URLPattern, URLResolver
from django.views import defaults as default_views

urlpatterns: list[URLPattern, URLResolver] = [
    path("health/", include("health_check.urls")),
    path(settings.ADMIN_URL, admin.site.urls),
    path("captcha/", include("captcha.urls")),
    path("", include("hodovi_ch.web.urls")),
    # Your stuff: custom urls includes go here
    *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
]

if settings.DEBUG:
    # This allows the error pages to be debugged during development, just visit
    # these url in browser to see how these error pages look like.
    urlpatterns = [
        *urlpatterns,
        path(
            "400/",
            default_views.bad_request,
            kwargs={"exception": Exception("Bad Request!")},
        ),
        path(
            "403/",
            default_views.permission_denied,
            kwargs={"exception": Exception("Permission Denied")},
        ),
        path(
            "404/",
            default_views.page_not_found,
            kwargs={"exception": Exception("Page not Found")},
        ),
        path("500/", default_views.server_error),
    ]
    if "debug_toolbar" in settings.INSTALLED_APPS:
        import debug_toolbar

        urlpatterns = [path("__debug__/", include(debug_toolbar.urls)), *urlpatterns]

Here you go:

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.views import defaults as default_views

urlpatterns = [
    path("health/", include("health_check.urls")),
    path(settings.ADMIN_URL, admin.site.urls),
    path("captcha/", include("captcha.urls")),
    path("", include("hodovi_ch.web.urls")),
    # Your stuff: custom urls includes go here
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

if settings.DEBUG:
    # This allows the error pages to be debugged during development, just visit
    # these url in browser to see how these error pages look like.
    urlpatterns += [
        path(
            "400/",
            default_views.bad_request,
            kwargs={"exception": Exception("Bad Request!")},
        ),
        path(
            "403/",
            default_views.permission_denied,
            kwargs={"exception": Exception("Permission Denied")},
        ),
        path(
            "404/",
            default_views.page_not_found,
            kwargs={"exception": Exception("Page not Found")},
        ),
        path("500/", default_views.server_error),
    ]
    if "debug_toolbar" in settings.INSTALLED_APPS:
        import debug_toolbar

        urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns

$ poetry show mypy
name         : mypy
version      : 0.910
description  : Optional static typing for Python

dependencies
 - mypy-extensions >=0.4.3,<0.5.0
 - toml *
 - typed-ast >=1.4.0,<1.5.0
 - typing-extensions >=3.7.4
$ poetry show django-stubs
name         : django-stubs
version      : 1.9.0
description  : Mypy stubs for Django

dependencies
 - django *
 - django-stubs-ext >=0.3.0
 - mypy >=0.910
 - toml *
 - types-pytz *
 - types-PyYAML *
 - typing-extensions *

Was it fixed though? I still experience the same error with django-stubs 1.9.0 installed. Also, I couldn’t find any related commit that would fix it.

The workaround from micheller worked for me for django-stubs 1.8.0, but it appears that this is no longer necessary in the newly-released django-stubs 1.9.0.