13

Before you dive in, here is my question: how can I use type hints in a subclass to specify a different type on an instance attribute?

If you are unclear on what that means, read below, where I have drawn up an example to clarify things.


Full Explanation

I have an abstract class Foo, and a subclass of Foo called SubclassOfFoo.

Foo has an abstract method get_something that returns an object of type Something.

Something has a subclass called SubclassOfSomething. SubclassOfSomething has an additional method something_special.

SubclassOfFoo overrides get_something to return an object of type SubclassOfSomething. Then, SubclassOfFoo tries to use SubclassOfSomething's method something_special.

However, currently my PyCharm's inspections are reporting Unresolved attribute reference 'something_special' for class 'Something'. I am trying to figure out the correct way to fix this.

This is all very confusing, so I have made a nice little code snippet to help here:

from abc import ABC, abstractmethod


class Something:
    def __init__(self):
        self.attr = 0


class SubclassOfSomething(Something):
    def __init__(self):
        Something.__init__(self)

    def something_special(self):
        self.attr = 1


class Foo(ABC):
    def __init__(self):
        self.my_class = self.get_something()

    @abstractmethod
    def get_something(self) -> Something:
        pass


class SubclassOfFoo(Foo):
    def __init__(self):
        Foo.__init__(self)

    def get_something(self) -> SubclassOfSomething:
        return SubclassOfSomething()

    def do_something_special(self):
        self.my_class.something_special()

Basically, in order to get everything to work out, I can do one of several things:

  1. Remove the type hint on the return of get_something within Foo
  2. Use a type hint in SubclassOfFoo for self.my_class to clear things up
  3. Use generics?

Option 1. is what I am trying to avoid

Option 2. is not bad, but I can't figure it out

Option 3. is also an option.

I am also open to other options, as I am sure there is a better way.

Can you please help me figure out the correct way to handle this?


What I Have Tried

To emulate option 2., I tried using typing.Type as suggested here: Subclass in type hinting

However, this was not working for me.

Neuron
  • 5,141
  • 5
  • 38
  • 59
Intrastellar Explorer
  • 3,005
  • 9
  • 52
  • 119
  • 2
    @101arrowz: That type hint is there because the more specific return type information is important. An arbitrary instance of `Something` cannot be assumed to have a `something_special` method. – user2357112 Sep 24 '19 at 23:44
  • 1
    I suspect the best option will be to make `Foo` generic. – user2357112 Sep 24 '19 at 23:45

3 Answers3

12

Using generics:

from abc import ABC, abstractmethod
from typing import Generic, TypeVar


SomethingT = TypeVar('SomethingT', bound='Something')


...


class Foo(ABC, Generic[SomethingT]):
    my_class: SomethingT

    def __init__(self):
        self.my_class = self.get_something()

    @abstractmethod
    def get_something(self) -> SomethingT:
        pass


class SubclassOfFoo(Foo[SubclassOfSomething]):
    def __init__(self):
        super().__init__()

    def get_something(self) -> SubclassOfSomething:
        return SubclassOfSomething()

    def do_something_special(self):
        # inferred type of `self.my_class` will be `SubclassOfSomething`
        self.my_class.something_special()
user2235698
  • 7,053
  • 1
  • 19
  • 27
7

You can give a type hint on my_class attribute in the beginning of class definition:

class SubclassOfFoo(Foo):
    my_class: SubclassOfSomething  # <- here

    def get_something(self) -> SubclassOfSomething:
        return SubclassOfSomething()

    def do_something_special(self):
        self.my_class.something_special()

After that there is no warning Unresolved attribute reference 'something_special' for class 'Something' from PyCharm inspection because now my_class is known to be SubclassOfSomething not Something.

sanyassh
  • 8,100
  • 13
  • 36
  • 70
  • That does solve the problem @sanyash. I am curious, what is that general method called? – Intrastellar Explorer Sep 25 '19 at 17:53
  • Also, I don't quite understand why it works. `my_class` is an instance attribute within `SubclassOfFoo`. Putting the type hint `my_class: SubclassOfSomething ` inside of `__init__` does not fix the problem. It only fixes the problem when put into the class scope of `SubclassOfFoo`. Why is that? Shouldn't `self.my_class` be scoped to the instance, and not affected by a type hint within a class scope? – Intrastellar Explorer Sep 25 '19 at 17:55
  • 2
    @IntrastellarExplorer please check this PEP: https://www.python.org/dev/peps/pep-0526/#class-and-instance-variable-annotations. It is stated that variables described as `my_class: SubclassOfSomething` in class scope are treated as instance attributes while `my_class: ClassVar[SubclassOfSomething]` is treated like class variable, not an instance attribbute. – sanyassh Sep 25 '19 at 18:03
  • Okay that clears me up, thank you again @sanyash! This is a nice functionality – Intrastellar Explorer Sep 25 '19 at 18:12
2

You could provide the something_special method on Something too, and raise a NotImplementedError

class Something:
    def __init__(self):
        self.attr = 0

    def something_special(self):
        raise NotImplementedError()

This resolves your type hinting issue, although functionally it will raise an exception at the same point (if you managed to get a Something somehow and try to call something_special, just will be NotImplementedError instead of AttributeError).

Maybe in some situations you might want to just pass instead, depending on what something_special actually is.

class Something:
    def __init__(self):
        self.attr = 0

    def validate(self):
        # doesn't want to perform validation
        pass


class SubclassOfSomething(Something):
    def __init__(self):
        Something.__init__(self)

    def validate(self):
        if self.attr < 0:
            raise ValueError()

The important underlying thing is making sure your class hierarchy conforms to a common interface - public methods on subclasses but not on parents goes against that and reduces the polymorphism of objects in your class hierarchy.

Rach Sharp
  • 2,324
  • 14
  • 31
  • Thank you for your answer @RachSharp! Yes both of those also are valid, upvoted! – Intrastellar Explorer Sep 25 '19 at 17:57
  • Unfortunately, in my actual use case (not the idealized code I have placed here), `Something` and `SubclassOfSomething` are both inside a 3rd party package, and I would prefer to have the type hinting taken care of within my own code – Intrastellar Explorer Sep 25 '19 at 17:57