5

I like using __qualname__ for the return-type annotation of factory-style class methods, because it doesn't hardcode the classname and therefore keeps working subclasses (cf. this answer).

class Foo:
    @classmethod
    def make(cls) -> __qualname__:
        return cls()

Currently this seems to work fine, but I am not sure whether this will still be possible with the postponed evaluation of annotations (PEP 563): the PEP says that

Annotations can only use names present in the module scope as postponed evaluation using local names is not reliable (with the sole exception of class-level names resolved by typing.get_type_hints()).

The PEP also says:

The get_type_hints() function automatically resolves the correct value of globalns for functions and classes. It also automatically provides the correct localns for classes.

However, the following code

from __future__ import annotations

import typing

class Foo():
    @classmethod
    def make(cls) -> __qualname__:
        return cls()

print(typing.get_type_hints(Foo.make))

fails with

  File "qualname_test.py", line 11, in <module>
    print(typing.get_type_hints(Foo.make))
  File "/var/local/conda/envs/py37/lib/python3.7/typing.py", line 1004, in get_type_hints
    value = _eval_type(value, globalns, localns)
  File "/var/local/conda/envs/py37/lib/python3.7/typing.py", line 263, in _eval_type
    return t._evaluate(globalns, localns)
  File "/var/local/conda/envs/py37/lib/python3.7/typing.py", line 467, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
NameError: name '__qualname__' is not defined

Commenting the __future__ import makes the code work again, in that case it outputs

{'return': <class '__main__.Foo'>}

as expected.

Is my use case __qualname__ supported (which would mean that get_type_hints is buggy), or is this approach not possible with PEP 563?

Florian Brucker
  • 9,621
  • 3
  • 48
  • 81
  • 2
    Your use of `__qualname__` is just incorrect. Consider `class Bar(Foo): pass`. `Bar.make()` doesn't return an instance of `Foo`; it returns an instance of `Bar`. – chepner Dec 17 '19 at 13:48
  • 2
    The correct type hint would probably be a `TypeVar`: `def make(cls: Type[T]) -> T`. – deceze Dec 17 '19 at 13:50
  • @chepner: You're correct, and I hadn't thought of this. However, with postponed evaluation that should start to work, right? Because then the annotation of `Bar.make` should be evaluated within the context of `Bar`, where `__qualname__` refers to `Bar`. – Florian Brucker Dec 17 '19 at 13:52
  • FWIW: While `mypy` does not support `__qualname__` currently, there is issue [mypy/#6473](https://github.com/python/mypy/issues/6473) on it. It has been accepted as describing a bug and is being worked on. I have no idea whether `get_type_hints` breaking is a bug or not – it seems to break for all class-level attributes (including types and aliases) but there is no bug report yet. – MisterMiyagi Jul 07 '20 at 10:08

1 Answers1

5

Foo.make doesn't always return an instance of Foo (which is what __qualname__ expands to). It returns an instance of the class it receives as an argument, which isn't necessarily Foo. Consider:

>>> class Foo:
...     @classmethod
...     def make(cls):
...         return cls()
...
>>> class Bar(Foo):
...     pass
...
>>> type(Bar.make())
<class '__main__.Bar'>

The correct type hint would be a type variable:

T = TypeVar("T", bound="Foo")

class Foo:
    @classmethod
    def make(cls: Type[T]) -> T:
        return cls()
chepner
  • 497,756
  • 71
  • 530
  • 681
  • You're correct, and I hadn't thought of this. However, with postponed evaluation that should start to work, right? Because then the annotation of `Bar.make` should be evaluated within the context of `Bar`, where `__qualname__` refers to `Bar`. Assuming that `__qualname__` can still be used in annotations, that is. – Florian Brucker Dec 17 '19 at 13:52
  • With `from __future__ import annotations`, if you try to use `__qualname__` as the return type, `mypy` reports "`error: Name '__qualname__' is not defined`" – chepner Dec 17 '19 at 14:00
  • `__qualname__` only exists within the namespace of the `class` statement, not in the global scope, so (I think) by the time `mypy` tries to use it, it is out of scope. – chepner Dec 17 '19 at 14:04
  • Yes, `__qualname__` only exists in the `class` namespace. The question is whether, with postponed evaluation, this should work (in which case mypy and `get_type_hints` would be buggy). – Florian Brucker Dec 18 '19 at 10:02
  • Postponed evaluation is somewhat of a misnomer. All it means is that whatever you provide is simply stored as a string in `Foo.__annotations__`; when (or if) that string gets evaluated is entirely up to you. The matter is further complicated by the fact that `Bar.make` doesn't exist; the *lookup* resolves to `Foo.make`, and static typechecking doesn't extended to modifying the value of `__qualname__` to take into account the *runtime* type of the argument passed to `Foo.make` when you call `Bar.make()`. – chepner Dec 18 '19 at 13:51
  • 1
    `__qualname__` is simply the wrong thing to use here. – chepner Dec 18 '19 at 13:51
  • Well, there is `typing.get_type_hints` as part of the standard library, so I would expect that its implementation sets (or at least follows) the standard of how to evaluate annotations. And its documentation says that it indeed does take into account the class's namespace. The question is whether it ignores `__qualname__` on purpose or on accident. – Florian Brucker Dec 18 '19 at 14:44
  • It doesn't say anything about the class namespace; it says it evaluates them using the given global and local namespaces. – chepner Dec 18 '19 at 14:47
  • From the documentation of `get_type_hints`: "It also automatically provides the correct localns for classes". – Florian Brucker Dec 18 '19 at 14:49
  • I don't see that line at https://docs.python.org/3/library/typing.html. – chepner Dec 18 '19 at 14:50
  • In any case, I am not interested in debugging the treatment of `__qualname__` in an annotation. I don't believe it's the right thing to use here. – chepner Dec 18 '19 at 14:54
  • Sorry, it's [in the PEP](https://www.python.org/dev/peps/pep-0563/#id7). But you're right, this is probably not the right place to discuss this question. Thanks for your feedback! – Florian Brucker Dec 18 '19 at 15:01