Typing: How to add hint to factory method?

Created on 5 Mar 2015  路  21Comments  路  Source: python/typing

How to hint to method receiving class object and returns instance of the class?

Case 1:

class Base:
    registry = set()

    @classmethod
    def create(cls, *args) -> 'instanceof(cls)':
        a = cls(*args)
        cls.registry.add(a)
        return a

Case 2:

class Base:
    @classmethod
    def query(cls) -> 'Query[instanceof(cls)]':
        return session.query(cls)

class Employee(Base):
    # ...

employees = Employee.query().filter_by(Employee.age >= 30).all()

Most helpful comment

For anyone who might find this now, a way to do this is:

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(...)

From https://stackoverflow.com/a/39205612/384617.

All 21 comments

I suppose the logical way to write these would be -> cls and -> Query[cls], but I think it is perfectly fine to use dynamic typing for cases like this, and I don't think we shouldn't bother trying to support it. (We do support cls: type for the argument of course.)

FYI, this issue was originally posted on PyCharm.
https://youtrack.jetbrains.com/issue/PY-11615

We're not going to support this. The issue seems fairly academic.

FYI, this issue is very programatic, as commented at https://youtrack.jetbrains.com/issue/PY-11615

I agree that this issue is not just academic, it comes up for me daily. This has practical implications whenever one wants to use class factory methods. Python's first-class treatment of classes enables an elegant implementation of factories via class methods. Because I can pass a class and call class methods on it, I don't need to create an independent factory class and pass an instance of it. Yet I can't have a type hint that indicates a class is expected, not an instance of the class.

This is a typical pattern I utilize (sadly, I'm still stuck in Python 2 using PyCharm type hinting; please forgive the antiquated syntax):

class Base:
    __metaclass__ = ABCMeta   

    @classmethod
    @abc.abstractmethod
    def create(cls, some_constructor_arg):
        """:rtype: Base"""

class ImplementsBase(Base):

    def __init__(self, some_constructor_arg):
        ...

    @classmethod
    def create(cls, some_constructor_arg):
        return ImplementsBase(some_constructor_arg)

Instead of creating a BaseFactory class and instantiating an implementation of BaseFactory to pass, Python allows me to pass the class instead:

class InstantiatesBases(object):
    def __init__(self, base_class):
        """:type base_class: Class[Base]"""
        self._base_class = base_class

    def instantiates_base(self, some_constructor_arg):
        """:rtype: Base"""
        return self._base_class.create(some_constructor_arg)

The type hint I want to provide for base_class in my constructor is something like Class[Base], indicating that I'm expecting to receive a Class of type Base, not an instance of Base. But this capability doesn't exist, as far as I can tell. Am I mistaken?

You should be able to hint the base_class argument to InstantiatesBases as a callable returning a Base. E.g.

class InstantiatesBases(object):
    def __init__(self, base_class):
       # type: (Callable[..., Base) -> None
        self._base_class = base_class

    def instantiates_base(self, some_constructor_arg):
        # type: (Any) -> Base
        return self._base_class.create(some_constructor_arg)

Thank you @gvanrossum . Although I knew to use Callable for functions, it didn't occur to me that one could use it for classes as well. I suspect this is not correctly supported in PyCharm, at least in Python 2 (the syntax in PyCharm for Python 2 docstrings for Callable[..., Base] should translate to (...) -> Base, I believe); but this allows me to follow up with PyCharm to ask they support this syntax. Thank you again!

See also #107.

For anyone who might find this now, a way to do this is:

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(...)

From https://stackoverflow.com/a/39205612/384617.

@gvanrossum Any chance you might reconsider the "academic" brush-off of this request? Factory methods are a fairly common pattern, and having a natural way to express them in type hints seems more than an academic corner case thing, no?

The bounded TypeVar works, but having to define a TypeVar for every class with factories seems rather clunky, whereas your suggestion of -> cls seems a lot more elegant.

The current way to write this is fine IMO.

@gvanrossum a'ight. I've been reminded that there's a better option without the TypeVar now, given forward refs:

class TrivialClass:
    def from_int(cls, int_arg: int) -> 'TrivialClass':
        return cls(...)

Still looking for a "non-academic" endorsement of the use case, but the current mechanisms worked out for the best. :-)

To all your academicians out there, that example is less powerful than the one using TypeVar, because it doesn't show that if you subclass TrivialClass the from_int method returns an instance of the subclass.

Also, forward references have been supported far longer than the TypeVar option -- they are not a new thing.

Thanks for the clarification. Just piping up for the academy. :-)

It would be helpful if the TypeVar way of type hinting class methods were mentioned in the mypy type hints cheat sheet. I know it's mentioned in the PEP 484 but I must have missed it since I have been doing this all wrong for weeks now and only came upon this topic by chance.

It is intentionally not mentioned there. There is more to the docs than the cheat sheets, and showing TypeVar there is likely to cause more misunderstanding.

I would like to add another example I just encountered -- implementing any binary operation on an immutable instance, for instance, this doesn't compile:

class Vector(tuple):
  def __add__(self, other: Vector) -> Vector:
    return tuple(sum(vals) for vals in zip(self, other))

@kwikwag why should that pass? It currently returns a tuple, not a Vector as annotated. You could make some progress on that function using self types, though you might need integer variadics (a feature that's in development in some type checkers) to catch some categories of errors.

@kwikwag is right, you cannot use the type hints to write a method that consumes an instance of itself

class Vector(tuple):
  def __add__(self, other: Vector) -> Vector:
     ....

With mypy you get:

def __add__(self, other: Vector) -> Vector:
NameError: name 'Vector' is not defined

This would be useful for many classes with operations defined between members of the class

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sid-kap picture sid-kap  路  7Comments

ilevkivskyi picture ilevkivskyi  路  8Comments

gvanrossum picture gvanrossum  路  5Comments

LiraNuna picture LiraNuna  路  7Comments

Phlogistique picture Phlogistique  路  4Comments