Skip to content

What's the best way to write a type hint for any kind of model object? #1068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
YPCrumble opened this issue Jul 21, 2022 · 8 comments
Open
Labels
question Further information is requested

Comments

@YPCrumble
Copy link
Contributor

Thanks for maintaining this repo! I have an issue that's likely a documentation issue in that I can't figure out how to create a type hint for a django model object that supports more than one model class.

I'm writing a type hint for a function that takes a model object of any type and performs an action on it, something like:

def transform_model(some_object: models.Model) -> models.Model:
    LOGGER.info("Transforming model with id %s", model.id)  # example of an error MyPy would raise
    return some_object

The issue I'm facing is that if I try to use models.Model as my type hint, I get errors like:

"Model" has no attribute "id"

as well as an error for any model methods I'm trying to use in the transformation function.

If I use a particular model class as the type hint it works fine, for instance some_object: SomeParticularModel. The issue is that I want to use this function for any type of model, not just SomeParticularModel.

Is there an appropriate way to be able to type hint a generic Django model?

@sobolevn
Copy link
Member

sobolevn commented Jul 21, 2022

Try Protocol with just id: int field. It should be enough for this case.

@YPCrumble
Copy link
Contributor Author

Thanks @sobolevn !

Do you mean something like this:

class DjangoModelProtocol(Protocol):
    id: int

Let's say my class looks like this:

class SomeModel(models.Model):
    def say_something(self):
        print(self.id)

If my function is like this one, the issue is that the DjangoModelProtocol protocol won't show an error for a missing model method. For instance, I would expect this to raise an error:

def dont_speak(my_model: DjangoModelProtocol):
    LOGGER.info("Calling dont_speak on model %s", my_model.id)  # The DjangoModelProtocol handles this, yay!
    my_model.say_something()  # OK
    my_model.say_nothing()  # I would expect this to raise a MyPy error

Thanks again for your help and for any suggestions on the above use case!

@sobolevn
Copy link
Member

my_model.say_nothing() must raise an error. Proof: https://mypy-play.net/?mypy=latest&python=3.10&gist=728d3a85fa2a39bcfb911174f4a12b75

Can you please attach a reproducable example where it does not?

@YPCrumble
Copy link
Contributor Author

@sobolevn thank you for your suggestion, and thank you for your patience!

Here's the code you provided:

from typing_extensions import Protocol

class DjangoModelProtocol(Protocol):
    id: int
    def say_something(self) -> None: ...

def dont_speak(my_model: DjangoModelProtocol):
    my_model.id               # ok
    my_model.say_something()  # ok
    my_model.say_nothing()    # not ok

What I was hoping for was a way to type hint a "generic" django model that would automatically include whichever model that is being used's methods automatically, without me having to define the model methods in two places. At a minimum I would want it to include the fields that are present on models.Model, for instance _meta.

For instance, I would hope this would work:

from typing_extensions import Protocol

class DjangoModelProtocol(Protocol):
    id: int

def dont_speak(my_model: DjangoModelProtocol):
    my_model.id               # ok
    my_model._meta       # I would hope this is OK, but it breaks
    my_model.say_something()  # This is probably impossible, but ideally I would want this to pass depending on the model instance passed to `dont_speak`.

I've tried for instance this code but it doesn't seem to work:

class DjangoModelProtocol(Protocol, models.Model):
    id: int

Again, apologies if I'm being slow here and thank you very much for your suggestions!

@christianbundy
Copy link
Contributor

Have you tried TypeVar?

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

def dont_speak(my_model:  TModel): ...

@YPCrumble
Copy link
Contributor Author

@christianbundy thank you for your suggestion! How would I add the id attribute to TModel in your suggestion? When I try this I get "TModel" has no attribute "id" - but overall it seems pretty close to what I'm looking for!

@christianbundy
Copy link
Contributor

Does it work if you use pk instead of id? I thought .id would always work, but maybe I'm not understanding... https://docs.djangoproject.com/en/dev/topics/db/models/#automatic-primary-key-fields

@UnknownPlatypus
Copy link
Contributor

If Django sees you’ve explicitly set Field.primary_key, it won’t add the automatic id column.

Because of that, django-stubs only declare the pk attribute on models.Model and not the id one because he might not exist. The presence of an id field will be correctly inferred if you give a more specific model.

@YPCrumble I think in your case you probably want to reference the pk because if one of your models were to set the primary_key on another field, you would have a runtime error:

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

class SomeModel(models.Model):
    name = models.Charfield(primary_key=True)

def transform_model(some_object: TModel) -> TModel:
    LOGGER.info("Transforming model with id %s", model.id)
    return some_object

transform_model(SomeModel.objects.first()) # AttributeError: 'SomeModel' object has no attribute 'id'

If nonetheless, you know for sure that every models will have an id, I'm not sure how you can declare that. I guess the pk tip should do the trick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Development

No branches or pull requests

5 participants