[Django]-How to use Python type hints with Django QuerySet?

78πŸ‘

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.

πŸ‘€A. J. Parr

45πŸ‘

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]
πŸ‘€sobolevn

26πŸ‘

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]).

πŸ‘€Or Duan

17πŸ‘

Update 2023

In django_hint 0.3.0 you can extend your model and the return type of the common functions of objects such as filter, get, etc. will be automatically detected.

from django_hint import StandardModelType

class SampleModel(models.Model, StandardModelType['SampleModel']):
    name: str = models.CharField(max_length=300)

Original Answer

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.

Disclaimer: I am the author of django_hint

πŸ‘€Ramtin

13πŸ‘

QuerySet is a good approach for function/method returning any queryset of any models. The Django queryset is iterable. But when the return type is very specific to one model, it may be better to use QuerySet[Model] over QuerySet.

Example: Filter all active users of a company

import datetime
from django.utils import timezone
from myapp.models import User
from collections.abc import Iterable

def get_active_users(company_id: int) -> QuerySet[User]:
    one_month_ago = (timezone.now() - datetime.timedelta(days=30)).timestamp()
    return User.objects.filter(company_id=company_id, is_active=True, 
                               last_seen__gte=one_month_ago)

The above function signature is more readable than def get_active_users(company_id: int) -> QuerySet:

Iterable[User] will also work the type checker will complain when the returned queryset is invoked on other methods.

def func() -> Iterable[User]:
    return User.objects.all()

users = func()
users.filter(email__startswith='support')

MyPy output

"Iterable[User]" has no attribute "filter"
πŸ‘€Kracekumar

8πŸ‘

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!

7πŸ‘

You actually can do what you want if you import the annotations module:

from __future__ import annotations
from django.db import models
from django.db.models.query import QuerySet

class MyModel(models.Model):
    pass

def my_function() -> QuerySet[MyModel]:
    return MyModel.objects.all()

Neither MyPy nor the Python interpreter will complain or raise exceptions on this (tested on python 3.7). MyPy will probably be unable to type-check it, but if all you want is to document your return type, this should be good enough.

πŸ‘€drakenation

3πŸ‘

from typing import Iterable

def func(queryset_or_list: Iterable[MyModel]): 
    pass

Both of queryset and list of model instance is iterable object.

πŸ‘€Shaffron

1πŸ‘

I’ve found myself that using typing.Sequence to solve a similar issue:

from typing import Sequence


def print_emails(users: Sequence[User]):
    for user in users:
        print(user.email)


users = User.objects.all()


print_emails(users=users)

As far as I know from docs:

A Sequence is anything that supports len() and .getitem(), independent of its actual type.

πŸ‘€dharmagetic

1πŸ‘

from typing import (TypeVar, Generic, Iterable, Optional)
from django.db.models import Model
from django.db.models import QuerySet
_T = TypeVar("_T", bound=Model)


class QuerySetType(Generic[_T], QuerySet):
    def __iter__(self) -> Iterable[_T]:
        pass

    def first(self) -> Optional[_T]:
        pass
πŸ‘€SIGKILL

Leave a comment