Search code examples
pythondjangodjango-modelsmypypython-typing

How to use T=TypeVar('T', bound=...) with Type[T] correctly?


I have the following type-annotated Django code:

from typing import Optional, Type, TypeVar

from django.db import models

T = TypeVar('T', bound=models.Model)


def get_obj_or_none(model: Type[T], obj_id: int) -> Optional[T]:
    try:
        return model.objects.get(pk=obj_id)
    except model.DoesNotExist:
        return None

The function expects a class derived from django.db.models.Model as the first parameter and an int id as the second parameter:

# obj is now the Person with id 123, or None if that didn't exist.
obj = get_obj_or_none(Person, 123)

But when I run mypy on the code, I get the error:

error: "Type[T]" has no attribute "objects"
error: "Type[T]" has no attribute "DoesNotExist"

But if change my code to this and run mypy again, I get no errors:

from typing import Optional, Type

from django.db import models


def get_obj_or_none(model: Type[models.Model], obj_id: int) -> Optional[models.Model]:
    try:
        return model.objects.get(pk=obj_id)
    except model.DoesNotExist:
        return None

Why doesn't the first example work? I would really prefer to use it since the second example doesn't tie the return value in any way to the model parameter, so the function could be returning an instance, which is completely unrelated to the class given as the first parameter.

I'm using Python 3.8.1 with mypy 0.761.


Edit:

Here's a self contained example, which can be tested as is:

from typing import Dict, Optional, Type, TypeVar


class Model:
    objects: Dict[int, 'Model'] = {}


T = TypeVar('T', bound=Model)


def get_obj_or_none(model: Type[T], obj_id: int) -> Optional[T]:
    try:
        return model.objects[obj_id]
    except KeyError:
        return None

Running mypy on this gives (to my surprise) a completely different error:

type_example.py:17: error: Incompatible return value type (got "Model", expected "Optional[T]")
Found 1 error in 1 file (checked 1 source file)

Why does mypy behave differently on these two examples? Can I fix both of the cases somehow?


Solution

  • The first example works correctly after setting up django-stubs for my project. Good instructions can be found from this SO answer.

    The last example gives correctly an error from mypy, since it's not allowed by Python. To quote from a GitHub issue I opened:

    The objects attribute can contain an instance of an arbitrary Model subclass, but the return type of get_obj_or_none contains a specific subtype T of Model. Type[T] has no effect in this example, since the type of the objects attribute is the same in subclasses (it doesn't use a "self" type). I don't think that there's a way to use a self type for a class variable.