Type hints for the generic utility in the Django

Question:

def get_or_create_object(self, model : ?(what will be the type?), data: dict) -> ? (what will be the return type):
        try:
            obj, _ = model.objects.get_or_create(**data)
        except model.MultipleObjectsReturned:
            obj = model.objects.filter(**data).first()
        return obj

get_or_create_object(ModelName, data)

What will be the type hint here – as function will get the Model instances dynamically.

Asked By: Iceman

||

Answers:

You can use models.Model or Type[models.Model] All Django models inherit from models.Model and the model that is sent to the function is of the same type, of course, if you want to say that it is a class and is a subset of this class, you can use Type[models.Model]

from typing import Type
from django.db import models

def get_or_create_object(model: Type[models.Model], data: dict) -> models.Model:
    try:
        obj, _ = model.objects.get_or_create(**data)
    except model.MultipleObjectsReturned:
        obj = model.objects.filter(**data).first()
    return obj

note:
get_or_create is exist in Django build-in:

exp = Example.objects.get_or_create(**data)
Answered By: PersianMan

Since you want your function to be generic in terms of the specific model class you pass to it, the solution is what @SUTerliakov said in his comment. You need to define a typing.TypeVar and ideally give it an upper bound of Model to narrow the types it can represent. See the relevant PEP 484 section for details.

Here is how you do it:

from typing import Any, TypeVar, cast

from django.db.models import Model


M = TypeVar("M", bound=Model)


def get_or_create_object(model: type[M], data: dict[str, Any]) -> M:
    try:
        obj, _ = model.objects.get_or_create(**data)
    except model.MultipleObjectsReturned:
        obj = cast(M, model.objects.filter(**data).first())
    return obj

Note that we need to use typing.cast because the QuerySet.first method can return an instance of the model or None and type checkers are not smart enough to know that in this specific situation it can never return None (due to the previously caught MultipleObjectsReturned exception). So we need to make it clear to the type checker that the value is indeed an instance of our model.

To demonstrate:

class SpecificModel(Model):
    def foo(self) -> int:
        return 1


instance = get_or_create_object(SpecificModel, {})
reveal_type(instance)
reveal_type(instance.foo())

Running mypy over this gives the following output:

note: Revealed type is "SpecificModel"
note: Revealed type is "builtins.int"

Thus the type checker correctly infers the type of the instance returned by the function.


As a side note, if you wanted you could actually broaden the type of data because it can be literally any Mapping, not necessarily a dictionary. For that you just do from collections.abc import Mapping and annotate it with data: Mapping[str, Any] instead.

Answered By: Daniil Fajnberg
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.