-
Notifications
You must be signed in to change notification settings - Fork 287
Annotating custom constructor methods for generic types. #2025
-
How to annotate Foo.new in the example below so that it behaves as expected? Code sample in pyright playground
# fmt: off from collections.abc import Mapping from typing import overload dict(bar=3) # SHOULD PASS (pyright: ✅, mypy: ✅) dict[str, int](bar=3) # SHOULD PASS (pyright: ✅, mypy: ✅) dict[int, int](bar=3) # SHOULD ERROR (pyright: ✅, mypy: ❌) class Foo[K, V](Mapping[K, V]): @overload @classmethod def new(cls, items: Mapping[K, V], /) -> "Foo[K, V]": ... @overload @classmethod def new(cls, items: Mapping[str, V] = ..., /, **kwargs: V) -> "Foo[str, V]": ... Foo.new(bar=3) # SHOULD PASS (pyright: ✅, mypy: ✅) Foo[str, int].new(bar=3) # SHOULD PASS (pyright: ✅, mypy: ✅) Foo[int, int].new(bar=3) # SHOULD ERROR (pyright: ❌, mypy: ❌)
My analysis: mypy doesn't error on dict[int, int](bar=3). I think this is a bug. pyright does error, but it's based on a __init__ overload, where we can constrain self. This is not available for a custom constructor. I tried to no avail:
- constraining
cls: "Foo[str, V]"similarly innew, but it raises new errors. Code sample in pyright playground - use polymorphic overload, but it doesn't change the inference. Code sample in pyright playground
Beta Was this translation helpful? Give feedback.
All reactions
Any idea why it fails to bind
V=int(or possiblyV=Literal[3])?
Yes, Foo.new binds Foo[Any, Any] to the new method. The types of class-scoped type variables are inferred from argument types only when you call a constructor. This doesn't work for other methods. In other cases, the specialization comes from the bound class — in this case, Foo or Foo[Any, Any] since you haven't specified any default values for the type variables.
Replies: 1 comment 6 replies
-
I think option 1 is the right approach, but you need to use type[Foo[str, V]] for the cls parameter annotation. If I make that change, it seems to work fine in pyright.
Beta Was this translation helpful? Give feedback.
All reactions
-
Like so? Code sample in pyright playground
This does seem to work with pyright, mypy seems to bug out on any new call with K≠str. Also:
_m0 = Foo.new(bar=3) reveal_type(_m0) # "Foo[str, Unknown]"
Any idea why it fails to bind V=int (or possibly V=Literal[3])?
Beta Was this translation helpful? Give feedback.
All reactions
-
Any idea why it fails to bind
V=int(or possiblyV=Literal[3])?
Yes, Foo.new binds Foo[Any, Any] to the new method. The types of class-scoped type variables are inferred from argument types only when you call a constructor. This doesn't work for other methods. In other cases, the specialization comes from the bound class — in this case, Foo or Foo[Any, Any] since you haven't specified any default values for the type variables.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Just to make sure that I get this right, when you say:
The types of class-scoped type variables are inferred from argument types only when you call *a constructor.* This doesn't work for other methods.
What exactly counts as *a constructor*? Only the standard constructor metaclass.__call__/__new__/__init__?
Well, actually, ... it does seem to produce the desired inference if we define new inside a custom metaclass. 🤓 Code sample in pyright playground
I guess this makes sense insofar as that if we were to access, say, some generic class-attributes inside .new, it wouldn't be clear what their types are inside the constructor itself.
So if we want full type-checking for a custom constructor, one either should use a function defined outside the class, a @staticmethod or a custom metaclass (if we need access to certain class-variables at construction time).
Learned something new today, thanks as always for your insights.
Beta Was this translation helpful? Give feedback.
All reactions
-
Though I have to say, having to define metaclasses makes this whole thing quite un-ergonomic. In fact, there seems to be another type-checker divergence here:
Code sample in pyright playground, https://mypy-play.net/?mypy=latest&python=3.13&gist=d902e42012932c4a8892c75c46d170d4
from typing import Self class Vector[T]: @classmethod def from_list(cls, x: list[T]) -> Self: ... # shows mypy: Vector[str], pyright: Vector[Unknown] reveal_type(Vector.from_list(["abc"]))
Beta Was this translation helpful? Give feedback.
All reactions
-
Would you consider mypy's inference in the example above a bug then?
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes,
Foo.newbindsFoo[Any, Any]to thenewmethod.
The typing specs IMO are silent rather than clear on this. The only examples of omitting generics I see are
-
At the end of Generics: Introduction:
def count_truthy(elements: list[Any]) -> int: return sum(1 for elem in elements if elem)
This is equivalent to omitting the generic notation and just saying
elements: list.
In both of these examples, the omission resulting in an implicit parameterisation by Any is in the context of an annotation expression, not a value expression. Foo.new, on the other hand, is a value expression, and we're not accessing .new as Foo[Any, Any].new; Foo is certainly not equivalent to the generic alias Foo[Any, Any] at runtime, and I think assuming this could lead to divergent behaviour between static and runtime type-checking.
Beta Was this translation helpful? Give feedback.