Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Annotating custom constructor methods for generic types. #2025

Answered by erictraut
randolf-scholz asked this question in Q&A
Discussion options

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:

  1. constraining cls: "Foo[str, V]" similarly in new, but it raises new errors. Code sample in pyright playground
  2. use polymorphic overload, but it doesn't change the inference. Code sample in pyright playground
You must be logged in to vote

Any idea why it fails to bind V=int (or possibly V=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

Comment options

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.

You must be logged in to vote
6 replies
Comment options

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

Comment options

Any idea why it fails to bind V=int (or possibly V=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.

Answer selected by randolf-scholz
Comment options

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.

Comment options

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"]))
Comment options

Would you consider mypy's inference in the example above a bug then?

Comment options

Yes, Foo.new binds Foo[Any, Any] to the new method.

The typing specs IMO are silent rather than clear on this. The only examples of omitting generics I see are

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /