9

I am playing around with python's 3.5 type hints. I was wondering how can I type hint the return type of a class method.

This is what I had in mind:

>>> class A():
    @classmethod
    def a(cls) -> A:
        pass

Traceback (most recent call last):
  File "<pyshell#24>", line 1, in <module>
    class A():
  File "<pyshell#24>", line 3, in A
    def a(cls) -> A:
NameError: name 'A' is not defined

Clearly it does not work. I guess one way would be to do some sort of "forward declaration" like so:

>>> class A():
    pass

>>> class A():
    @classmethod
    def a(cls) -> A:
        pass

But that does not feel "pythonic". How do people normally solve this?

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
Mac
  • 3,397
  • 3
  • 33
  • 58
  • Not only little "pythonic", but outright incorrect - as the "A" in the second class refers to the first, now shadowed, "A" class, not the actual one that will be used. – jsbueno Mar 18 '16 at 18:42
  • @jsbueno agreed, but the spec solution points to a string, which also is not the correct class... – Mac Mar 18 '16 at 21:21
  • Yes, but since the spec states that, the tools that will make the inspection are responsible to retrieve the object which is named in the string from the odule namespace. With a string, that object will be the current class (unless it is shadowed by another class beneath it); By declaring a stub, the "live" object in the annotation will point to the stub, and any checking tool would retrieve the stub class instead of the actual class. – jsbueno Mar 18 '16 at 21:38
  • In short, if you ever need to do like above (I already had in another context) don't redeclare the class - declar it normally, and use normal assignments to it to add new members - like in `class A: pass\n def b(self) -> A: return A()\nA.b = b` – jsbueno Mar 18 '16 at 21:41
  • @jsbueno how to apply the classmethod though? – Mac Mar 18 '16 at 22:42
  • I think it is better if I augment the answer with these comments – jsbueno Mar 19 '16 at 01:27
  • Related: https://stackoverflow.com/questions/39205527/can-you-annotate-return-type-when-value-is-instance-of-cls – Anton Tarasenko Aug 26 '18 at 13:48

1 Answers1

13

Forward references When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.

https://www.python.org/dev/peps/pep-0484/#forward-references

The PEP text goes on to provide the following example:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

Apart from the provision for strings, there is a problem with your proposed workaround - if you do:

class A():
    pass

class A():
    @classmethod
    def a(cls) -> A:
        pass

The return value annotated in the (final and real) class A is the object that was carrying A name at the time the class body was processed - and that object is the stub class A. So any tool that would check in a static or runtime analysis the return value of A.a() would find it not to be equal the class A.

So, if a work around as this is ever needed (I had needed it once, I forgot the context -it had not to do with annotations - ah - I recall now: it is in a project that automatically generates Python ctypes wrappers for a specific C library), the way to go is to create one single class, and to augment it afterwards with assignment statements. For your example, that would be:

class A:
    pass

def a(self) -> A:
    pass

A.a = a
del a
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • 2
    As of Python 3.7, forward-references are now supported, so it is no longer necessary to make it a string. – Peter Jul 03 '19 at 15:26
  • 1
    @Peter This [requires a `__future__` import in 3.7 and up. As of 3.10 it has still not yet been made the default behaviour](https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class). – Karl Knechtel Sep 02 '22 at 06:15