How to use Python type hints with Django QuerySet?

PythonDjangoType Hinting

Python Problem Overview


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?

Python Solutions


Solution 1 - Python

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 - Python

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 - Python

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 - Python

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 - Python

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!

Solution 6 - Python

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"

Solution 7 - Python

from typing import Iterable

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

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

Solution 8 - Python

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.

Solution 9 - Python

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.

Solution 10 - Python

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionАлексей ГолобурдинView Question on Stackoverflow
Solution 1 - PythonA. J. ParrView Answer on Stackoverflow
Solution 2 - PythonsobolevnView Answer on Stackoverflow
Solution 3 - PythonOr DuanView Answer on Stackoverflow
Solution 4 - PythonRamtinView Answer on Stackoverflow
Solution 5 - PythonDominique PERETTIView Answer on Stackoverflow
Solution 6 - PythonKracekumarView Answer on Stackoverflow
Solution 7 - PythonShaffronView Answer on Stackoverflow
Solution 8 - PythondrakenationView Answer on Stackoverflow
Solution 9 - PythondharmageticView Answer on Stackoverflow
Solution 10 - PythonSIGKILLView Answer on Stackoverflow