python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.49k stars 2.83k forks source link

Support PEP 696 – Type defaults for TypeVarLikes #14851

Open intgr opened 1 year ago

intgr commented 1 year ago

Feature

See PEP 696 – Type defaults for TypeVarLikes for details. The PEP has not yet been accepted though. Example from the PEP:

T = TypeVar("T", default=int)  # This means that if no type is specified T = int

@dataclass
class Box(Generic[T]):
    value: T | None = None

reveal_type(Box())                      # type is Box[int]
reveal_type(Box(value="Hello World!"))  # type is Box[str]

Since I did not find an existing tracking issue for this feature, I decided to open one.

The default= argument is already supported in typing_extensions version 4.4.0+ and typeshed (https://github.com/python/typeshed/pull/8821)


Edit from maintainer: mypy supports basic PEP 696 use cases, but is missing some support for less common use cases. Give it a try!

JelleZijlstra commented 8 months ago

The original example passes now with mypy 1.9.0, thanks to @cdce8p's work (https://mypy-play.net/?mypy=latest&python=3.10&gist=ca609b35e9cbf97a600343b249c6b0a9). What still needs to be done here?

intgr commented 8 months ago

One missing feature is TypeVar(default=) referencing another TypeVar. django-stubs has been patiently waiting for this feature, it would allow us to clean up some pretty ugly hacks.

T = TypeVar("T")
U = TypeVar("U", default=T)  # If just T is specified, then U = T

class Box(Generic[T, U]):  # Box[T] would be expanded to Box[T, T]
    ...

Right now there's no warning when using this, mypy silently does the wrong thing. Playground: https://mypy-play.net/?mypy=latest&python=3.12&gist=22121ee5ef0b55d52d651a1d8636df73

JelleZijlstra commented 8 months ago

Ah yes, that doesn't quite work yet:

from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U", default=T)  # If just T is specified, then U = T

class Box(Generic[T, U]):  # Box[T] would be expanded to Box[T, T]
    ...

x = Box[int]()
reveal_type(x)  # E: Revealed type is "__main__.Box[builtins.int, T?]"
intgr commented 7 months ago

@cdce8p Are you interested in implementing this part of PEP 696 too -- allowing TypeVar to default to another TypeVar? Thanks for your work so far!

cdce8p commented 7 months ago

@cdce8p Are you interested in implementing this part of PEP 696 too -- allowing TypeVar to default to another TypeVar?

I've already started working on it. See #16878. That one didn't make in time for the 1.9.0 release though. I do plan to continue the work, just not sure when I'll get to it. It might take some time.

EwoutH commented 2 months ago

If I'm correct, this issue is required for Python 3.13 support right? What's needed to move it forward?

JelleZijlstra commented 2 months ago

It's required to support some new syntax in Python 3.13, but mypy will type check most code in 3.13 just fine without this, so it depends on what you count as "Python 3.13 support".

As an example (probably relevant to why you're here :) ), to make Black support 3.13 mypyc compilation, this issue isn't relevant because Black doesn't use TypeVars with defaults at the moment.

However, @cdce8p implemented much of what's needed for this feature in any case (thanks!). I don't know how much is left, but most basic use cases should already work.

cdce8p commented 2 months ago

As an example (probably relevant to why you're here :) ), to make Black support 3.13 mypyc compilation, this issue isn't relevant because Black doesn't use TypeVars with defaults at the moment.

The issue you're looking for is probably https://github.com/mypyc/mypyc/issues/1056.

However, @cdce8p implemented much of what's needed for this feature in any case (thanks!). I don't know how much is left, but most basic use cases should already work.

The next step would be to add support for the new TypeVar defaults syntax in 3.13. I've started working on it already, just need to find some time soon to finish it and open a PR.

Besides that, the support for ParamSpec / TypeVarTuple defaults and recursive TypeVar defaults is still quite limited unfortunately. Although it would be great to fully support these, PEP 696 is already useable in the most common cases.

hauntsaninja commented 1 week ago

Examples like the following still fail:

from typing import Generic, assert_type
from typing_extensions import TypeVar

DefaultStrT = TypeVar("DefaultStrT", default=str)
DefaultIntT = TypeVar("DefaultIntT", default=int)

class NoNonDefaults(Generic[DefaultStrT, DefaultIntT]): ...

assert_type(NoNonDefaults, type[NoNonDefaults[str, int]])
assert_type(NoNonDefaults[str], type[NoNonDefaults[str, int]])
assert_type(NoNonDefaults[str, int], type[NoNonDefaults[str, int]])

It looks like this is due to the thing I find slightly confusing where mypy represents type objects as CallableType. One possible way to fix is to replace type variables with their defaults when comparing CallableType to TypeType and vice versa. Maybe there's a better way too