4

I am checking with hasattr if an object has an attribute. If it exists, I assign it. However, mypy still complains has no attribute. How can I help mypy to remember that this attribute exists?

MVCE

Save as example.py:

from typing import Any


class MakeNoiseMixin:
    def make_noise(self):
        if isinstance(self, Cat) or hasattr(self, "cat"):
            cat = self if isinstance(self, Cat) else self.cat
            cat.meow()
        else:
            print("zZ")


class Cat(MakeNoiseMixin):
    def meow(self):
        print("meow!")


class Dog(MakeNoiseMixin):
    ...


class Human(MakeNoiseMixin):
    def __init__(self, cat):
        self.cat = cat


felix = Cat()
felix.make_noise()

tom = Dog()
tom.make_noise()

felix_owner = Human(felix)
felix_owner.make_noise()

Run:

$ python example.py 
meow!
zZ
meow!

$ example.py --check-untyped-defs
example.py:4: error: "MakeNoiseMixin" has no attribute "cat"
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • 1
    AFAIK mypy doesn't do type-narrowing based on `hasattr`, since `hasattr` doesn't in itself imply a type. Maybe you could define a `Protocol`? – Samwise May 19 '21 at 05:26
  • [Instance type hint](https://stackoverflow.com/questions/51191061/mypy-base-class-has-no-attribute-x-how-to-type-hint-in-base-class)? – fzzylogic May 19 '21 at 05:37
  • Works for me as is with python 3.7.9 and mypy-0.812. – fzzylogic May 19 '21 at 05:42
  • @fzzylogic In principle, it is possible to add the `cat` attribute as a class variable to the `MakeNoiseMixin`. However, as I'm dealing with Django models here I'm uncertain if that might cause other issues (e.g. the `Cat` table getting a `cat` column). – Martin Thoma May 19 '21 at 05:55
  • @MartinThoma Maybe this SO answer explaining [how to implement a mixin with a Protocol](https://stackoverflow.com/questions/51930339/how-do-i-correctly-add-type-hints-to-mixin-classes) is helpful (see second answer). – fzzylogic May 19 '21 at 06:24
  • @fzzylogic The protocol solution breaks with the usage of `super()`. If you want to, I can extend the MVCE to show this. – Martin Thoma May 19 '21 at 06:44

1 Answers1

1

This isn't the answer being sought, but putting it here as one approach. Look forward to seeing other answers.

Fwiw, defining a variable annotation won't result in a new table column, but even if it was a class level variable that wasn't an instance of a model field, Django would know better.

from typing import Any

class MakeNoiseMixin:
    cat: Any
    def make_noise(self):
        if isinstance(self, Cat) or hasattr(self, 'cat'):
            cat = self if isinstance(self, Cat) else self.cat
            cat.meow()
        else:
            print("zZ")


class Cat(MakeNoiseMixin):
    def meow(self):
        print("meow!")


felix = Cat()
felix.make_noise()
$ mypy example.py --check-untyped-defs
Success: no issues found in 1 source file
fzzylogic
  • 2,183
  • 1
  • 19
  • 25
  • I've just expanded the example - and it still works! That's actually pretty nice. I'll have to check the `super()` case tomorrow again – Martin Thoma May 19 '21 at 20:36