python / python/mypy

Sub-types of a union type need to be treated as covariant

Offen
#11,730 1 Kommentar 2 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug topic-type-variables
Vorherrschende Sprache
Python
Sterne
20.6k
Forks
3.3k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

Bug Report

Starting from microsoft/pylance-release#2172.

Currently, A or B seems not to be considered covariant with the union type A | B when A or B comes as a type variable. Because of this, Mypy tends to give false positive error about variance settings in protocols.

To Reproduce

Here are two sample files.

https://mypy-play.net/?mypy=latest&python=3.8&gist=2c061e7b00dbf6024dc4bf13cea7d896
from __future__ import annotations

from typing import Protocol, TypeVar

_T0 = TypeVar("_T0")
_T0_co = TypeVar("_T0_co", covariant=True)

_T1 = TypeVar("_T1")
_T1_co = TypeVar("_T1_co", covariant=True)

#%%

class FP0(Protocol[_T0, _T1]):
    def __call__(self, arg0: _T0 | _T1, /) -> tuple[_T0, _T1]:
        ...

def return_fp0_float_float() -> FP0[float, float]:
    def f(x: float) -> tuple[float, float]:
        x += 1
        return (0, 0)

    return f

def return_fp0_object_object() -> FP0[object, object]:
    # Dangerous cast, reported.
    return return_fp0_float_float()

#%%

class FP0A(Protocol[_T0_co, _T1_co]):
    def __call__(self, arg0: _T0_co | _T1_co, /) -> tuple[_T0_co, _T1_co]:
        ...

def return_fp0a_float_float() -> FP0A[float, float]:
    def f(x: float) -> tuple[float, float]:
        x += 1
        return (0, 0)

    return f

def return_fp0a_object_object() -> FP0A[object, object]:
    # Dangerous cast, but not reported.
    return return_fp0a_float_float()

fp0a_object_object: FP0A[object, object] = return_fp0a_object_object()
_ = fp0a_object_object(object())
https://mypy-play.net/?mypy=latest&python=3.8&gist=686538007d253a9241dfeaf4c7a3598d
from __future__ import annotations

from typing import Protocol, TypeVar

_T0 = TypeVar("_T0")
_T0_contra = TypeVar("_T0_contra", contravariant=True)

_T1 = TypeVar("_T1")
_T1_contra = TypeVar("_T1_contra", contravariant=True)

#%%

class FP1(Protocol[_T0, _T1]):
    def __call__(self, arg0: _T0, arg1: _T1, /) -> _T0 | _T1:
        ...

def return_fp1_object_object() -> FP1[object, object]:
    def f(a: object, b: object) -> object:
        return object()

    return f

def return_fp1_float_float() -> FP1[float, float]:
    # Dangerous cast, reported.
    return return_fp1_object_object()

#%%

class FP1A(Protocol[_T0_contra, _T1_contra]):
    def __call__(self, arg0: _T0_contra, arg1: _T1_contra, /) -> _T0_contra | _T1_contra:
        ...

def return_fp1a_object_object() -> FP1A[object, object]:
    def f(a: object, b: object) -> object:
        return object()

    return f

def return_fp1a_float_float() -> FP1A[float, float]:
    # Dangerous cast, but not reported.
    return return_fp1a_object_object()

fp1a_float_float: FP1A[float, float] = return_fp1a_float_float()
result: float = fp1a_float_float(1.0, 2.0)
result += 1

FP0 and FP1 are in my expected forms, but get reported with errors because of variance settings. FP0A and FP1A are the revised versions based on the errors Mypy gives for FP0 and FP1.

Running either file would lead to a crash on the last line respectively, because of the unreported dangerous casts which are marked with comments.

Expected Behavior

Variance settings in FP0 and FP1 are right, and those in FP0A and FP1A are wrong.

_T0 and _T1 are both covariant with the argument type _T0 | _T1 in FP0, so they should be invariant when they also appear in the return type.
_T0 and _T1 are both covariant with the return type _T0 | _T1 in FP1, so they should be invariant when they also appear in the argument types.

Actual Behavior

Variance settings in FP0 and FP1 are considered wrong, and those in FP0A and FP1A are considered right.

Your Environment

  • Mypy version used: Latest(0.910) | Master
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): N/A
  • Python version used: 3.8
  • Operating system and version: Mypy Playground

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne damit, die beiden Reproduktions-Snippets aus dem Issue in den verlinkten mypy playground-Beispielen auszuführen, und vergleiche die Varianzdiagnosen für FP0/FP1 mit FP0A/FP1A. Verfolge die Varianzbehandlung des Type-Checkers für Union-Typen und Typvariablen; abgeschlossen ist die Aufgabe, wenn die erwarteten Varianzeinstellungen akzeptiert und die unsicheren Zuweisungen abgelehnt werden.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
compilers
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Veraltet
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
38/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.