python / python/mypy

Mypy is unable to detect if a builtin class supports a generic protocol. Unexpected behavior with custom classes too.

Open
#18,190 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

Bug Report

Mypy is unable to detect if a class supports a generic protocol.

To Reproduce

Create a Wrapper[T] that supports arithmetic on its values, that is

if
    A + B -> C
then
    Wrapper[A] + Wrapper[B] -> Wrapper[C]
    Wrapper[A] + B -> Wrapper[C]
    A + Wrapper[B] -> Wrapper[C]

https://mypy-play.net/?mypy=latest&python=3.12&flags=strict&gist=f78c1a5896a21c3c76a9f4380a02ae69

from __future__ import annotations

from typing import TypeVar, Protocol, Generic
from typing_extensions import reveal_type


_T_contra = TypeVar('_T_contra', contravariant=True)
_T_co = TypeVar('_T_co', covariant=True)


class SupportsAdd(Protocol[_T_contra, _T_co]):
    def __add__(self, other: _T_contra, /) -> _T_co: ...

class SupportsRAdd(Protocol[_T_contra, _T_co]):
    def __radd__(self, other: _T_contra, /) -> _T_co: ...


_T = TypeVar('_T', covariant=True)
_T_left = TypeVar('_T_left', covariant=True)
_T_right = TypeVar('_T_right', covariant=True)
_T_out = TypeVar('_T_out', covariant=True)


class Wrapper(Generic[_T]):
    val: _T

    def __init__(self, val: _T):
        self.val = val

    def __add__(self: Wrapper[SupportsAdd[_T_right, _T_out]], right: _T_right | Wrapper[_T_right]) -> Wrapper[_T_out]:
        if isinstance(right, Wrapper):
            return Wrapper(self.val + right.val)
        return Wrapper(self.val + right)
    
    def __radd__(self: Wrapper[SupportsRAdd[_T_left, _T_out]], left: _T_left | Wrapper[_T_left]) -> Wrapper[_T_out]:
        if isinstance(left, Wrapper):
            return Wrapper(left.val + self.val)
        return Wrapper(left + self.val)

    def __repr__(self) -> str:
        return f'Wrapper({self.val})'




class Snake:
    # Snake + Legs = Lizard
    def __add__(self, other: Legs) -> Lizard:
        if not isinstance(other, Legs):
            return NotImplemented
        return Lizard()
    
    # Legs + Snake = Dragon
    def __radd__(self, other: Legs) -> Dragon:
        if not isinstance(other, Legs):
            return NotImplemented
        return Dragon()

class Legs: ...
class Lizard: ...
class Dragon: ...



# What works as expected
reveal_type(Wrapper(10) + Wrapper(1j))  # Wrapper[complex]
reveal_type(10 + Wrapper(1j))  # Wrapper[complex]
reveal_type(Wrapper(1j) + Wrapper(10))  # Wrapper[complex]
reveal_type(Wrapper(1j) + 10)  # Wrapper[complex]
reveal_type(Wrapper(10.0) + Wrapper(2))   # Wrapper[float]
reveal_type(Wrapper(10.0) + 2)   # Wrapper[float]
reveal_type(Snake() + Legs())  # Lizard
reveal_type(Legs() + Snake())  # Dragon
reveal_type(Wrapper(Snake()) + Wrapper(Legs()))  # Wrapper[Lizard]
reveal_type(Wrapper(Snake()) + Legs())  # Wrapper[Lizard]
reveal_type(Wrapper(Legs()) + Wrapper(Snake()))  # Wrapper[Dragon]
reveal_type(Legs() + Wrapper(Snake()))  # Wrapper[Dragon]
Wrapper(Snake()) + Wrapper(Snake())  # correctly gives error: error: Argument 1 to "Wrapper" has incompatible type "Snake"; expected "Legs"  [arg-type]
Wrapper(10.0) + Wrapper('haha')  # correctly gives error: Argument 1 to "Wrapper" has incompatible type "str"; expected "float"  [arg-type]


# What doesn't work as expected    
reveal_type(Wrapper(10) + 1j)  # unexpected error: Unsupported operand types for + ("Wrapper[int]" and "complex")  [operator]
reveal_type(1j + Wrapper(10))  # unexpected error: Unsupported operand types for + ("complex" and "Wrapper[int]")  [operator]
reveal_type(Snake() + Wrapper(Legs()))  # unexpected error: Unsupported operand types for + ("Snake" and "Wrapper[Legs]")  [operator]
reveal_type(Wrapper(Legs()) + Snake())  # unpexpected error: Unsupported operand types for + ("Wrapper[Legs]" and "Snake")  [operator]

Expected Behavior

Wrapper[int] + complex => Wrapper[complex]
complex + Wrapper[int] => Wrapper[complex]
Snake + Wrapper[Legs] => Wrapper[Lizard]
Wrapper[Legs] + Snake => Wrapper[Dragon]

Actual Behavior

error: Unsupported operand types for + ("Wrapper[int]" and "complex")  [operator]
error: Unsupported operand types for + ("complex" and "Wrapper[int]")  [operator]
error: Unsupported operand types for + ("Snake" and "Wrapper[Legs]")  [operator]
error: Unsupported operand types for + ("Wrapper[Legs]" and "Snake")  [operator]

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked mypy-play reproduction and compare the four failing operator expressions with the expected types in the issue. Trace the type-checking path for generic protocols and reflected arithmetic methods; done means those expressions produce the expected Wrapper or result types without errors while the existing valid and invalid examples remain correct.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.