Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to add hint to factory method? #58

Closed
methane opened this issue Mar 5, 2015 · 23 comments
Closed

How to add hint to factory method? #58

methane opened this issue Mar 5, 2015 · 23 comments

Comments

@methane
Copy link
Member

methane commented Mar 5, 2015

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()
@gvanrossum
Copy link
Member

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

@methane
Copy link
Member Author

methane commented Mar 9, 2015

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

@gvanrossum
Copy link
Member

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

@methane
Copy link
Member Author

methane commented Mar 29, 2015

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

@malina-kirn
Copy link

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?

@gvanrossum
Copy link
Member

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)

@malina-kirn
Copy link

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!

@vlasovskikh
Copy link
Member

See also #107.

@davidparsson
Copy link

davidparsson commented Aug 31, 2017

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.

@wsanchez
Copy link

@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.

@gvanrossum
Copy link
Member

The current way to write this is fine IMO.

@wsanchez
Copy link

@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. :-)

@gvanrossum
Copy link
Member

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.

@JukkaL
Copy link
Contributor

JukkaL commented Mar 27, 2018

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

@wsanchez
Copy link

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

@Ricyteach
Copy link

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.

@gvanrossum
Copy link
Member

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.

@kwikwag
Copy link

kwikwag commented Aug 3, 2020

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

@JelleZijlstra
Copy link
Member

@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.

@Chandler
Copy link

@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

@jacksonthall22
Copy link

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.

It seems this code still doesn't warn when trying to return an incorrect type from the subclass, am I missing something? Example. Can give system info on request.

@erictraut
Copy link
Collaborator

I recommend using the new Self type in this case. See PEP 673 for details.

from typing import Type
from typing_extensions import Self

class Base:
    @classmethod
    def create(cls: Type[Self]) -> Self:
        ...

class Child1(Base):
    pass

class Child2(Base):
    @classmethod
    def create(cls: Type[Self]) -> Self:
        return Child1() # Type error

I don't think mypy has support for Self yet, but pyright and pyre do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests