Kanishk
Kanishk

Reputation: 328

Type hinting the return value of a class method that returns self?

as the questions describes, I wanna type hint a self return , something like:

class A:
    def foo(self) -> [what goes here?]:
        # do something
        return self

Things I already tried:

  1. annotating it as A ( adding from __future__ import annotations at the top ): this means the method returns an instantiated A() object, not necessarily self.
  2. annotating it as Type[A] ( adding from typing import Type ): this means the method is returning is returning an un-instantiated A, which isnt remotely close to self.
  3. annotating is as Self ( adding from typing_extensions import Self ): mypy gives an error: Variable "typing_extensions.Self" is not valid as a type [valid-type]mypy(error)

Things that might be of help: hovering over the method foo with no annotations of a return value, VScode hints shows - Self@A, i dont understand it but, this definitely differentiates between returning another instantiated class A() and returning self ... Thanks

Upvotes: 11

Views: 11584

Answers (2)

STerliakov
STerliakov

Reputation: 7877

I can't find any question that covers this closely, so will try to explain.

What does "same type" mean for type checking

Well, probably this is the long form of "you can't, and you shouldn't".

Type checking aims to confirm that all functions are called with proper argument types and return expected types. I suggest to read PEP483 first to understand concept of type better. Suppose you have the following:

s1 = ''.join(['a', 'b', 'c'])
s2 = ''.join(['a', 'b', 'c'])
assert s1 is not s2
assert s1 == s2

(join to avoid optimization, but it's another story). Are they the same object? No, is not clearly states this (they have different memory addresses). But will s2 be acceptable whenever you want s1? Definitely yes. You will not create a function that operates only on s1 and checks this fact with is, right?

Now what is the difference between self as a reference to an exact object and self as any A instance? When we talk about type checking, all A instances are completely equivalent and indistinguishable. They have the same set of methods and attributes (including types). We can ask: "which type errors can be introduced or removed, if we explicitly declare object to be self instance and not just self type?" I really cannot think of any. If you want this for semantics, use docstring - types should not be abused for everything. self object is absolutely the same as any other A() instance for type checker.

Simple solution: return class instance

Your first code sample is almost fine. Annotate the return type as A to tell that it returns an instance of class A, it will work for a final class:

class A:
    def foo(self) -> A:
        return self

However, this approach has a drawback (it is well explained in PEP673 about Self type):

class AChild(A):
    pass

# mypy
reveal_type(AChild().foo())  # N: revealed type is "__main__.A"

If you create a new A in foo and return it, then this approach is perfect. If you return self - it is valid, but not precise. That's why we need Self type.

Self type

Self type was introduced in PEP673, and was not supported by mypy at the time of writing this. (update: supported since mypy 1.0 released on Feb. 6, 2023) Your usage in 3rd example was perfectly valid and will work after implementation in type checkers (see 5-th code block in "Motivation" section of PEP).

Here's how you can use Self (assuming python>=3.11 not to bother with typing_extensions):

from typing import Self

class A:
    def foo(self) -> Self:
        return self

class AChild(A):
    pass

# mypy
reveal_type(AChild().foo())  # N: revealed type is "__main__.AChild"
reveal_type(A().foo())  # N: revealed type is "__main__.A"

Implementing Self type without using it

However, you can mimic Self accurately with a few lines of code (and python >= 3.7, AFAIR).

from typing import TypeVar

_Self = TypeVar('_Self', bound='A')

class A:
    def foo(self: _Self) -> _Self:
        return self

class AChild(A):
    pass

# mypy
reveal_type(AChild().foo())  # N: revealed type is "__main__.AChild"
reveal_type(A().foo())  # N: revealed type is "__main__.A"

Now this works. All subclasses will return their class instance.

Upvotes: 14

Tim C.
Tim C.

Reputation: 179

A simple way is to put the class name in the quotation: e.g:

class A:
  def foo(self) -> 'A':
    # do something
    return self

In IDE(e.g. Pycharm) you can navigate to the class by ctl(or cmd in macos) and click 'A'. Also, when refactoring the code, e.g to rename the class A in Pycharm, the 'A' would be renamed as well.

Upvotes: 7

Related Questions