52

Given a class with a helper method for initialization:

class TrivialClass:
    def __init__(self, str_arg: str):
        self.string_attribute = str_arg

    @classmethod
    def from_int(cls, int_arg: int) -> ?:
        str_arg = str(int_arg)
        return cls(str_arg)

Is it possible to annotate the return type of the from_int method?

I'v tried both cls and TrivialClass but PyCharm flags them as unresolved references which sounds reasonable at that point in time.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Jonatan
  • 1,096
  • 1
  • 18
  • 28

4 Answers4

101

Starting with Python 3.11 you can use the new typing.Self object. For older Python versions you can get the same object by using the typing-extensions project:

try:
    from typing import Self
except ImportError:
    from typing_extensions import Self


class TrivialClass:
    # ...

    @classmethod
    def from_int(cls, int_arg: int) -> Self:
        # ...
        return cls(...)

Note that you don't need to annotate cls in this case.

Warning: mypy support for the Self type has not yet been released; you'll need to wait for the next version after 0.991. Pyright already supports it.

If you can't wait for Mypy support, then you can use a generic type to indicate that you'll be returning an instance of cls:

from typing import Type, TypeVar

T = TypeVar('T', bound='TrivialClass')

class TrivialClass:
    # ...

    @classmethod
    def from_int(cls: Type[T], int_arg: int) -> T:
        # ...
        return cls(...)

Any subclass overriding the class method but then returning an instance of a parent class (TrivialClass or a subclass that is still an ancestor) would be detected as an error, because the factory method is defined as returning an instance of the type of cls.

The bound argument specifies that T has to be a (subclass of) TrivialClass; because the class doesn't yet exist when you define the generic, you need to use a forward reference (a string with the name).

See the Annotating instance and class methods section of PEP 484.


Note: The first revision of this answer advocated using a forward reference naming the class itself as the return value, but issue 1212 made it possible to use generics instead, a better solution.

As of Python 3.7, you can avoid having to use forward references in annotations when you start your module with from __future__ import annotations, but creating a TypeVar() object at module level is not an annotation. This is still true even in Python 3.10, which defers all type hint resolution in annotations.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 1
    Is it possible to create a forward reference to cls? – Jonatan Aug 29 '16 at 11:57
  • @Jonatan: no, because `cls` is not a type. – Martijn Pieters Aug 29 '16 at 11:59
  • 2
    @Jonatan: note that any subclass of `TrivialClass` will also satisfy the type hint. – Martijn Pieters Aug 29 '16 at 12:00
  • 2
    A similar solution can be applied to class methods with `self`: `def create_something(self: T) -> T` – David Pärsson Aug 31 '17 at 09:19
  • Then i can't use cls method anymore? Since cls has `Type[T]`, there is no suggestion for classmethod `cls.abc()` inside `from_int()` – TomSawyer Dec 07 '19 at 04:20
  • @TomSawyer I’m not sure what you are referring to here. Are you having issues with a specific tool perhaps, like PyCharm? – Martijn Pieters Dec 07 '19 at 23:46
  • @MartijnPieters yes. i'm using pycharm. if i set `cls: Type[T]`, it won't suggest other class method. Already submit new question here, so i'm not sure there is the error of Pycharm or there must be another way to archive it with Python? https://stackoverflow.com/questions/59226096/python-how-to-access-to-other-classmethod-while-using-generic-type – TomSawyer Dec 08 '19 at 03:40
  • 1
    @TomSawyer I’d say that that’s a PyCharm bug. Report it to them – Martijn Pieters Dec 08 '19 at 12:17
10

From Python 3.7 you can use __future__.annotations:

from __future__ import annotations


class TrivialClass:
    # ...

    @classmethod
    def from_int(cls, int_arg: int) -> TrivialClass:
        # ...
        return cls(...)

Edit: you can't subclass TrivialClass without overriding the classmethod, but if you don't require this then I think it's neater than a forward reference.

rusheb
  • 1,004
  • 9
  • 15
7

A simple way to annotate the return type is to use a string as the annotation for the return value of the class method:

# test.py
class TrivialClass:
  def __init__(self, str_arg: str) -> None:
    self.string_attribute = str_arg

  @classmethod
  def from_int(cls, int_arg: int) -> 'TrivialClass':
    str_arg = str(int_arg)
    return cls(str_arg)

This passes mypy 0.560 and no errors from python:

$ mypy test.py --disallow-untyped-defs --disallow-untyped-calls
$ python test.py
Jean-François Corbett
  • 37,420
  • 30
  • 139
  • 188
Noah Gilmore
  • 1,319
  • 2
  • 15
  • 24
  • Much better, as it doesn't cause an error about returning TrivialClass instead of 'T' - or if you change it to return cls(...) an error about cls not being callable when using the init constructor. – Mitchell Currie Mar 17 '18 at 10:12
  • 5
    You can’t now subclass TrivialClass without overriding the classmethod however. My first revision used a forward reference exactly like this. – Martijn Pieters May 23 '18 at 23:41
  • 1
    Ah, good point! If you need to subclass, the TypeVar solution is probably better. – Noah Gilmore May 29 '18 at 19:06
7

In Python 3.11 there is a nicer way to do this using the new Self type:

from typing import Self

class TrivialClass:
    def __init__(self, str_arg: str):
        self.string_attribute = str_arg

    @classmethod
    def from_int(cls, int_arg: int) -> Self:
        str_arg = str(int_arg)
        return cls(str_arg)

This also works correctly with sub classes as well.

class TrivialSubClass(TrivialClasss):
    ...

TrivialSubclass.from_int(42)

The IDE shows return type TrivialSubClass and not TrivialClass.

This is described in PEP 673.

Jonatan
  • 1,096
  • 1
  • 18
  • 28
  • 1
    I had held off mentioning `Self` until mypy support was ready. There is no current mypy release that supports it, support landed after 0.991 came out. We'll have to wait for the next version before this can be used by more projects. – Martijn Pieters Jan 26 '23 at 16:50