Skip to content
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

Warn when PEP695 type param clashes with a TypeVar name #18499

Open
leamingrad opened this issue Jan 21, 2025 · 7 comments
Open

Warn when PEP695 type param clashes with a TypeVar name #18499

leamingrad opened this issue Jan 21, 2025 · 7 comments
Labels

Comments

@leamingrad
Copy link

Bug Report

When defining a generic class with a type variable bounded to number classes, arithmetic operations are not available in class methods.

To Reproduce

Playground link: https://mypy-play.net/?mypy=master&python=3.12&gist=97147ba435ef98e5566e5c6a01424f92

Sample program

from typing import TypeVar
from decimal import Decimal

TAmount = TypeVar("TAmount", int, Decimal)

class Box[TAmount]:
    def __init__(self, amount: TAmount) -> None:
        self.amount = amount
    
    def box_method_with_arithmetic(self, other: Box[TAmount]) -> None:
        add = self.amount + other.amount
        sub = self.amount - other.amount
        mul = self.amount * other.amount
        div = self.amount / other.amount
        floordiv = self.amount // other.amount
        pow = self.amount ** other.amount

def value_function_with_arithmetic(left: TAmount, right: TAmount) -> None:
    add = left + right
    sub = left - right
    mul = left * right
    div = left / right
    floordiv = left // right
    pow = left ** right

def box_function_with_arithmetic(left: Box[TAmount], right: Box[TAmount]) -> None:
    add = left.amount + right.amount
    sub = left.amount - right.amount
    mul = left.amount * right.amount
    div = left.amount / right.amount
    floordiv = left.amount // right.amount
    pow = left.amount ** right.amount

Expected Behavior

The program should typecheck successfully.

Actual Behavior

main.py:11: error: Unsupported left operand type for + ("TAmount")  [operator]
main.py:12: error: Unsupported left operand type for - ("TAmount")  [operator]
main.py:13: error: Unsupported left operand type for * ("TAmount")  [operator]
main.py:14: error: Unsupported left operand type for / ("TAmount")  [operator]
main.py:15: error: Unsupported left operand type for // ("TAmount")  [operator]
main.py:16: error: Unsupported left operand type for ** ("TAmount")  [operator]
Found 6 errors in 1 file (checked 1 source file)

This seems to only be a problem in methods on the generic class. The functions which operate on values and class instances typecheck cleanly.

Note that the TypeVar is defined as being exactly int or Decimal as per #12899 (comment).

@leamingrad leamingrad added the bug mypy got something wrong label Jan 21, 2025
@sterliakov
Copy link
Collaborator

sterliakov commented Jan 21, 2025

mypy is correct here. Do not mix old-style and new-style generics: your class Box is generic in unconstrained typevar that happens to also be called TAmount, it isn't related to the typevar declared in global scope (so e.g. Box[str] is perfectly fine).

The correct syntax is either

class Box[TAmount: (int, Decimal)]:
    ...

or

from typing import Generic, TypeVar

TAmount = TypeVar("TAmount", int, Decimal)

class Box(Generic[TAmount]):
    ...

We may consider adding an optional warning when a PEP695 type parameter name clashes with the name of a globally defined typevar.

@sterliakov
Copy link
Collaborator

Proposed title: Warn when PEP695 type param clashes with a TypeVar name

@sterliakov sterliakov added feature and removed bug mypy got something wrong labels Jan 21, 2025
@leamingrad
Copy link
Author

@sterliakov: Thanks for that - you are entirely right!

Should I change the ticket up to track a feature request now, or just close it (I'm happy with either)?

@sterliakov
Copy link
Collaborator

I think this proposal is sound, let's keep this as a feature request - please just update the title to match the core idea.

@leamingrad leamingrad changed the title Arithmetic operarators are not available in methods of generic classes Warn when PEP695 type param clashes with a TypeVar name Jan 24, 2025
@leamingrad
Copy link
Author

@sterliakov Could I check if the following is also an example of mixing the syntaxes incorrectly?

from decimal import Decimal

class Box[TAmount: (int, Decimal)]:
    def __init__(self, amount: TAmount) -> None:
        self.amount: TAmount = amount

def non_generic_function(amount: int | float) -> Box:
    # This does not error
    return Box(amount)

def generic_function[SAmount: (int, float)](amount: SAmount) -> Box[SAmount]:
    # This correctly errors
    return Box(amount)
    
# This correctly errors as float is not an allowed type
my_instance = Box(1.5)

https://mypy-play.net/?mypy=master&python=3.12&gist=ddc7e7bb4d76aab148657cf95d8a70a0

In particular, if you declare a generic class, do you also need to declare all functions which build instances of that class as generic in order to get them to check the types being passed to the constructor?

@sterliakov
Copy link
Collaborator

@leamingrad not exactly, the problem in your snippet is that bare Box in annotations (with generic omitted) is strictly equivalent to Box[Any], and Box[Any](1.5) is allowed (Any is an escape mechanism, it is compatible with nearly anything).

In the following snippet mypy will detect invalid arg outside of return (for return there is some bidirectional inference, propagating Any further):

def non_generic_function(amount: int | float) -> Box:
    Box(amount)  # This emits a diagnostic
    return Box(amount)  # This does not

I highly recommend running mypy --strict to detect missing generic parameters among other problems.

So yes, builders should either be generic or return a concrete subtype (e.g. -> Box[int] if you have some factory that builds solely boxes of integers).

@leamingrad
Copy link
Author

Thanks for that - I completely missed that in the docs (it is covered in https://mypy.readthedocs.io/en/stable/generics.html#generic-type-aliases but I missed the relevant part).

I highly recommend running mypy --strict to detect missing generic parameters among other problems.

I completely agree. Unfortunately the codebase I work on doesn't have --strict enabled (and likely won't for a while due to its size), but I'l definitely start using it when I put snippets in the mypy playground before posting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants