How to use Python type hints with Django QuerySet?

40,286

Solution 1

One solution may be using Union typing class.

from typing import Union, List
from django.db.models import QuerySet
from my_app.models import MyModel

def somefunc(row: Union[QuerySet, List[MyModel]]):
    pass

Now when you slice the row argument it will know that the returned type is either another list of MyModel or an instance of MyModel, whilst also hinting that the methods of the QuerySet class are available on the row argument too.

Solution 2

There's a special package called django-stubs (the name follows PEP561) to type your django code.

That's how it works:

# server/apps/main/views.py
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render

def index(request: HttpRequest) -> HttpResponse:
    reveal_type(request.is_ajax)
    reveal_type(request.user)
    return render(request, 'main/index.html')

Output:

» PYTHONPATH="$PYTHONPATH:$PWD" mypy server
server/apps/main/views.py:14: note: Revealed type is 'def () -> builtins.bool'
server/apps/main/views.py:15: note: Revealed type is 'django.contrib.auth.models.User'

And with models and QuerySets:

# server/apps/main/logic/repo.py
from django.db.models.query import QuerySet

from server.apps.main.models import BlogPost

def published_posts() -> 'QuerySet[BlogPost]':  # works fine!
    return BlogPost.objects.filter(
        is_published=True,
    )

Output:

reveal_type(published_posts().first())
# => Union[server.apps.main.models.BlogPost*, None]

Solution 3

I made this helper class to get a generic type hint:

from django.db.models import QuerySet
from typing import Iterator, Union, TypeVar, Generic

T = TypeVar("T")

class ModelType(Generic[T]):
    def __iter__(self) -> Iterator[Union[T, QuerySet]]:
        pass

Then use it like this:

def somefunc(row: ModelType[SomeModel]):
    pass

This reduces the noise everytime I use this type and it make it usable between models (like ModelType[DifferentModel]).

Solution 4

This is an improved helper class of Or Duan.

from django.db.models import QuerySet
from typing import Iterator, TypeVar, Generic

_Z = TypeVar("_Z")  

class QueryType(Generic[_Z], QuerySet):
    def __iter__(self) -> Iterator[_Z]: ...

This class is used specifically for QuerySet object such as when you use filter in a query.
Sample:

from some_file import QueryType

sample_query: QueryType[SampleClass] = SampleClass.objects.filter(name=name)

Now the interpreter recognizes the sample_query as a QuerySet object and you will get suggestions such as count() and while looping through the objects, you will get suggestions for the SampleClass

Note
This format of type hinting is available from python3.6 onwards.


You can also use django_hint which has hinting classes specifically for Django.

Solution 5

IMHO, the proper way to do it is to define a type that inherits QuerySet and specify a generic return type for the iterator.

    from django.db.models import QuerySet
    from typing import Iterator, TypeVar, Generic, Optional

    T = TypeVar("T")


    class QuerySetType(Generic[T], QuerySet):  # QuerySet + Iterator

        def __iter__(self) -> Iterator[T]:
            pass

        def first(self) -> Optional[T]:
            pass

        # ... add more refinements


Then you can use it like this:

users: QuerySetType[User] = User.objects.all()
for user in users:
   print(user.email)  # typing OK!
user = users.first()  # typing OK!

Share:
40,286

Related videos on Youtube

Алексей Голобурдин
Author by

Алексей Голобурдин

Updated on April 19, 2021

Comments

  • Алексей Голобурдин
    Алексей Голобурдин about 3 years

    Is it possible to specify type of records in Django QuerySet with Python type hints? Something like QuerySet[SomeModel]?

    For example, we have model:

    class SomeModel(models.Model):
        smth = models.IntegerField()
    

    And we want to pass QuerySet of that model as param in func:

    def somefunc(rows: QuerySet):
        pass
    

    But how to specify type of records in QuerySet, like with List[SomeModel]:

    def somefunc(rows: List[SomeModel]):
        pass
    

    but with QuerySet?

  • sobolevn
    sobolevn almost 5 years
    There are stubs for django with typed QuerySet and models: github.com/typeddjango/django-stubs Tutorial: sobolevn.me/2019/08/typechecking-django-and-drf
  • Manu Artero
    Manu Artero over 3 years
    quick note if someone is using VSCode; this solution works perfectly if response type is surronded by single quotes (don't know the reason); def my_method(self) -> 'QueryType[MyModel]': ...