-
Notifications
You must be signed in to change notification settings - Fork 239
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
Spec: Annotating the self
argument in __init__
methods
#1563
Comments
I think you forgot to add |
Thanks for the catch, updated. |
Don't forget intersections with from typing import Self
class H:
def __init__(self: Self):
return
reveal_type(H()) and old- from typing import TypeVar
Self = TypeVar("Self")
class H:
def __init__(self: Self, other: type[Self]):
return
reveal_type(H(H)) |
Old self there is missing a bound=H |
Thanks for starting this discussion. The example you posted above is fine, but it don't really demonstrate why The unspecified behavior occurs when the annotation for Normally, type variables appears in input parameter types and the return types, and it's the job of a type checker to "solve" the type variables based on the arguments passed to the call and then specialize the return type based on the solved type variables. In this example, def func(x: T, y: T) -> list[T]:
return [x, y]
reveal_type(func(1, 2)) # list[int]
reveal_type(func("", b"")) # list[str | bytes] The return type of an T = TypeVar("T")
S = TypeVar("S")
# A class-scoped type variable is used in this example:
class Foo(Generic[T]):
def __init__(self: Foo[T], value: T) -> None: ...
reveal_type(Foo(1)) # Foo[int]
reveal_type(Foo("")) # Foo[str]
# A method-scoped type variable is used in this example:
class Bar(Generic[T]):
def __init__(self: "Bar[S]", value: S) -> None:
...
reveal_type(Bar(1)) # Bar[int]
reveal_type(Bar("")) # Bar[str] The That means we need to consider the intended behavior for cases like this: class Foo(Generic[T]):
def __new__(cls, value: T) -> Foo[T]: ...
def __init__(self: Foo[T], value: T) -> None: ... |
@erictraut Is your |
The example is correct. My point in providing that example is to demonstrate that there are edge cases that are currently undefined — and therefore produce behaviors that may be undesirable with the current type checker implementations. The behavior in this case is unspecified, so arguably any behavior is "correct" according to the current spec. |
Thanks for the detailed feedback. I made some updates and still need to continue working on the requested feature. I'm not sure I follow you here. Is the following example (which I've removed from the first post) already valid and formalized somewhere in the spec? from typing import Generic, Literal, TypeVar, overload
T = TypeVar("T")
class A(Generic[T]):
@overload
def __init__(self: A[int], is_int: Literal[True]) -> None:
...
@overload
def __init__(self: A[str], is_int: Literal[False] = ...) -> None:
... Or is still a valid example regarding the requested feature? According to your comment here, it doesn't seem to be specified, but is a trivial example. In that case, I will probably have to add some more complex examples involving unions and type variables. Regarding this example: # A class-scoped type variable is used in this example:
class Foo(Generic[T]):
def __init__(self: Foo[T], value: T) -> None: ... Is there really a need to add support for this, as you could simply omit Staying on class-scoped type vars, it might be interesting to see what should happen with the following: class Foo(Generic[T]):
def __init__(self: Foo[T | None], value: T) -> None: ...
f = Foo(1) Currently pyright discards the @A5rocks, Thanks, examples added in the already supported use cases. |
I may be coming late to this party, but if I take this example and simply remove the annotation for |
My previous comment raised the same question, I'm waiting for an answer on this one. However, the second example is making use of a method-scoped type variable where an explicit annotation on |
But why would you need this in real life? Please tell the story of the actual use case that led you to this proposal. |
I was the one who made the mypy issue that led to this; our motivation is proper typing for an exception group-friendly pytest.raises API. See python/mypy#16752 which has a code sample, if that's not concrete enough for you. We worked around this by subclassing |
I've updated the motivation section, providing a simplified example that I encountered when trying to correctly type hint Django fields, along with a list of already existing examples. The "introduction" was also updated to make it clear that it doesn't actually unblocks any existing issue (afaik), as a workaround is already available with |
Following my previous comment, I also updated the specification with examples that hopefully cover what needs to be specified. Feel free to add any other use cases that were missed. There's a couple places where I wasn't sure what the wanted behavior is, and are open to discussion. These are marked with this Note mark: Note |
Thanks @Viicos. I've been working to fill in missing chapters in the typing spec. One of the next that I have on my list is a chapter on type evaluation for constructor calls. This mini-spec will fit nicely within that chapter. I'll post a draft in the Python typing discourse channel when I have something ready to review. That will be a good forum for us to pin down the remaining behaviors. |
Great to hear, and thanks for all the work on the typing spec lately. Indeed I think this "mini spec" would fit better in a more general chapter about constructor calls. As you mentioned earlier, will this include behavior with both a |
@Viicos, I just posted a draft typing spec chapter on constructors. It incorporates parts of your earlier spec, although it deviates from your proposal in one significant way: it completely disallows the use of class-scoped TypeVars within a Please review the draft spec and add comments to this thread. |
The draft typing spec by @erictraut is merged, does that resolve the issues presented in the OP such that this can be closed? |
It does, thanks! |
I'm opening this issue to discuss a typing feature supported by major type checkers (at least pyright and partially mypy), that allows users to annotate the
self
argument of an__init__
method. This is currently used as a convenience feature (I haven't encountered any use case that would not be possible with the workaround described in the motivation section, but do let me know if you are aware of any), so the goal is to formally specify the behavior so that users can feel confident using it.Annotating
self
is already supported in some cases:Already supported use cases
self
as a subclass:playgrounds: mypy / pyright.
In this case, instantiating
B
should only match overload 1 and 3.typing.Self
:playgrounds: mypy / pyright.
typing.Self
was introduced, using aTypeVar
:playgrounds: mypy / pyright.
This issue proposes adding support for a special use case of the
self
type annotation that would only apply when the annotation includes one or more type variables (class-scoped, method-scoped, or both), and would convey a special meaning for the type checker.Motivation
This feature can be useful because the return type of
__init__
is alwaysNone
, meaning there is no way to influence the parametrization of a instance when relying solely on the type checker solver is not enough. In most cases, the type variable can be inferred correctly without any explicit annotation:However, there are some situations where more complex logic is involved, e.g. by using the dynamic features of Python (metaclasses for instance). Consider this example:
Ideally, we would like
NullableWrapper(int, null=True)
to be inferred asNullableWrapper[int | None]
. A way to implement this is by making use of the__new__
method:However, this
__new__
method might not exist at runtime, meaning users would have to add anif TYPE_CHECKING
block.What the example above tries to convey is the fact that some constructs can't be reflected natively by type checkers. The example made use of a
null
argument that should translate toNone
in the resolved type variable, and here is a list of already existing examples:dict
/collections.UserDict
: a lot of custom logic is applied to the provided arguments, and the type stubs definition is making use of this feature to cover the possible use cases.mypy
doesn't support solving a type variable from a default value (see issue), overloads are used to explicitly specify the solved type from the default value: see the stub definition ofcontextlib.nullcontext
.UUID
type: see the definition.Specification
Definitions
TypeVar
used in conjunction withGeneric
(or as specified with the new 3.12 syntax):TypeVar
used in function / method:Context
When instantiating a generic class, the user should generally explicitly specify the type(s) of the type variable(s), for example
var: list[str] = list()
. However, type checkers can solve the class-scoped type variable(s) based on the arguments passed to the__init__
method, similarly to functions where type variables are involved. For instance:This proposal aims at standardizing the behavior when the
self
argument of a generic class'__init__
method is annotated.Canonical examples
Whenever a type checker encounters the
__init__
method of a generic class whereself
is explicitly annotated, it should use this type annotation as the single source of truth to solve the type variable(s) of that class. That includes the following examples:Currently supported by both mypy and pyright. ✅
Currently unsupported by both mypy and pyright. ❌
Note
Although using class-scoped type variables to annotate
self
is already quite common (see examples in motivation), we can see diverging behavior between mypy and pyright in theBar
example. If theself
type annotation should be the only source of truth, then type checkers should inferBar(1, "1")
asBar[str, int]
, but this is open to discussion.Behavior with subclasses
As stated in the motivation section,
__new__
can be used as a workaround. However, it does not play well with subclasses (as expected):The same would look like this with
__init__
:As with
__new__
, subclasses shouldn't be supported in this case (i.e.reveal_type(SubFoo(1))
shouldn't beSubFoo[int]
).Note
I think shouldn't be supported should mean undefined behavior in this case, although this can be discussed. While the given example does not show any issues as to why it shouldn't be supported, consider the following example:
However, this is open to discussion if you think type checkers could handle these specific scenarios.
Appendix - Invalid use cases
For reference, here are some invalid use cases that are not necessarily related to the proposed feature:
Using an unrelated class as a type annotation
playgrounds: mypy / pyright.
Both type checkers raise an error, but a different one (mypy explicitly disallows the annotated
self
in the first overload, pyright doesn't raise an error but instead discards the first overload, meaningTrue
can't be used foris_int
).Using a supertype as a type annotation
playgrounds: mypy / pyright.
No error on both type checkers, but they both infer
A[Unknown/Never]
. I don't see any use case where this could be allowed? Probably better suited for__new__
.The text was updated successfully, but these errors were encountered: