Is it possible to specify type of records in Django QuerySet with Python type hints? Something like QuerySet[SomeModel]
?
For example, we have model:
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]
).
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.
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.
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!
from typing import Iterable
def func(queryset_or_list: Iterable[MyModel]):
pass
Both of queryset and list of model instance is iterable object.
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