How to use Python type hints with Django QuerySet?
Asked Answered
D

10

143

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?

Diarrhea answered 22/2, 2017 at 16:48 Comment(2)
Check out sobolevn.me/2019/08/typechecking-django-and-drfBuntline
You might be looking for SomeModelQuerySet, import it from the same path you import SomeModel fromCoauthor
T
80

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.

Thirion answered 2/6, 2017 at 4:50 Comment(2)
There are stubs for django with typed QuerySet and models: github.com/typeddjango/django-stubs Tutorial: sobolevn.me/2019/08/typechecking-django-and-drfBuntline
In Python3.10+ (PEP585 & PEP604) it's possible to type hint row: QuerySet | list[MyModel]Heighho
B
49

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]
Buntline answered 27/8, 2019 at 10:39 Comment(0)
I
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]).

Iglesias answered 21/2, 2019 at 0:25 Comment(0)
T
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

Twittery answered 4/7, 2019 at 4:9 Comment(1)
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]': ...Gluttony
T
14

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"
Twelvemonth answered 17/4, 2021 at 12:39 Comment(0)
L
9

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!

Lenette answered 7/10, 2019 at 13:54 Comment(0)
B
8

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.

Baalbek answered 4/10, 2019 at 5:8 Comment(1)
This works well for Python 3.9 (then even without from __future__ import annotations)Bieber
H
4
from typing import Iterable

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

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

Heppman answered 9/6, 2020 at 7:27 Comment(0)
U
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.

Ultranationalism answered 16/2, 2020 at 17:43 Comment(0)
F
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
Floorer answered 16/1, 2021 at 7:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.