8000 `sum`: `Protocol` defining `__add__` by jpy-git · Pull Request #7578 · python/typeshed · GitHub
[go: up one dir, main page]

Skip to content

sum: Protocol defining __add__ #7578

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

Merged
merged 3 commits into from
Apr 1, 2022
Merged

Conversation

jpy-git
Copy link
Contributor
@jpy-git jpy-git commented Apr 1, 2022

Partially addresses #7574.

As suggested by @JelleZijlstra in this comment we should define an __add__ protocol for use in sum to prevent things like

sum([1, None])

@overload
def sum(__iterable: Iterable[_T]) -> _T | Literal[0]: ...
def sum(__iterable: Iterable[_SumT]) -> Any | Literal[0]: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 90% of cases, I think people will probably be using sum to add up variables that are all of the same type, e.g. sum((1.4, 2.3, 3.7, 4.2)). We can safely say for this that the result will be of type float.

So, although strictly speaking you're entirely correct — the return type isn't guaranteed to always be _SumT, far from it! — I nonetheless think I prefer using _SumT over Any for the return type, so that we can preserve precise type inference for the common case.

But I'm curious about what other people think :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, note that the TypeVar is currently unbound in this overload, so either the return type needs to use _SumT instead of Any, or the parameter type needs to change to use _SupportsSum instead of _SumT :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we make the protocol generic over the return type of __add__, and make it sum(Iterable[_SupportsSum[_T]]) -> _T?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh shoot this was actually unintentional, was playing round with the typings and forgot to change it back 😅
I agree that _SumT makes sense, so will change it back for now 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we make the protocol generic over the return type of __add__, and make it sum(Iterable[_SupportsSum[_T]]) -> _T?

Good shout, will give it a go

Copy link
Member
@AlexWaygood AlexWaygood Apr 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JelleZijlstra, strictly speaking I'm not sure it's that simple, because there's __radd__ to consider, which can change the return type. If Y is a subclass of X, and Y defines __radd__, then Y.__radd__ takes precedence over X.__add__ when evaluating the operation x + y (where x is an instance of X, and y is an instance of Y)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk how much that actually matters though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, getting a completely precise return type in the face of mixed types and __radd__ is very complicated. The current version is technically incorrect for the case where two arguments are passed, the iterable is empty, and the default returns an unrelated type from its __add__.

mypy-primer found a lot of new false positives from my suggestion, so we should probably undo that and just go back to assuming that __add__ returns the same type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I've reverted back to using TypeVars in the latest commit 😄

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@JelleZijlstra
Copy link
Member
JelleZijlstra commented Apr 1, 2022

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

What happened to the freqtrade one?

(code link: https://github.com/freqtrade/freqtrade/blob/0b40c37cc54dbf97ff8ed1b82330e9d158d91d6b/freqtrade/persistence/models.py#L1204)

@AlexWaygood
Copy link
Member
AlexWaygood commented Apr 1, 2022

What happened to the freqtrade one?

Yes, that one looked like a true positive. I think close_profit_abs attribute is declared here as Optional[float]: https://github.com/freqtrade/freqtrade/blob/0b40c37cc54dbf97ff8ed1b82330e9d158d91d6b/freqtrade/persistence/models.py#L296

@jpy-git
Copy link
Contributor Author
jpy-git commented Apr 1, 2022

What happened to the freqtrade one?

Was just wondering the same thing!

All that's changed vs the first commit is altering the return type

diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi
index 9372b2a1..ce2042bc 100644
--- a/stdlib/builtins.pyi
+++ b/stdlib/builtins.pyi
@@ -1485,7 +1485,7 @@ _SumT = TypeVar("_SumT", bound=_SupportsSum)
 _SumS = TypeVar("_SumS", bound=_SupportsSum)
 
 @overload
-def sum(__iterable: Iterable[_SumT]) -> Any | Literal[0]: ...
+def sum(__iterable: Iterable[_SumT]) -> _SumT | Literal[0]: ...
 
 if sys.version_info >= (3, 8):
     @overload

@AlexWaygood
Copy link
Member
AlexWaygood commented Apr 1, 2022

This is very strange, I can't reproduce it on MyPy playground:

from typing import *

class _SupportsSum(Protocol):
    def __add__(self, __x: Any) -> Any: ...

_SumT = TypeVar("_SumT", bound=_SupportsSum)
_SumS = TypeVar("_SumS", bound=_SupportsSum)

@overload  # type: ignore[no-overload-impl]
def sum(__iterable: Iterable[_SumT]) -> _SumT | Literal[0]: ...
@overload
def sum(__iterable: Iterable[_SumT], start: _SumS) -> _SumT | _SumS: ...

it: Iterable[float | None]

sum(it)  # error: Value of type variable "_SumT" of "sum" cannot be "Optional[float]"

Can't reproduce it with the conditional overloads either.

@AlexWaygood
Copy link
Member

I've triggered a new run of mypy_primer

@github-actions
Copy link
Contributor
github-actions bot commented Apr 1, 2022

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

@jpy-git
Copy link
Contributor Author
jpy-git commented Apr 1, 2022

So testing locally these examples work as expected for me

from typing import Iterator, Union


def foo() -> Iterator[int]: ...

def bar() -> Iterator[Union[int, None]]: ...

sum([1, 2])
sum([1, None])
sum(foo())
sum(bar())
test.py:9: error: Value of type variable "_SumT" of "sum" cannot be "Optional[int]"
test.py:11: error: Value of type variable "_SumT" of "sum" cannot be "Optional[int]"

But if I run mypy --custom-typeshed-dir . ../freqtrade/freqtrade/persistence/models.py | grep "models.py:1203" on freqtrade it returns nothing when we have the _SumT return type
However, with the Any return type I get:
../freqtrade/freqtrade/persistence/models.py:1203: error: Value of type variable "_SumT" of "sum" cannot be "Optional[float]" 🤔

@JelleZijlstra
Copy link
Member

I thought type context might help, but this variation that's closer to the freqtrade code doesn't repro it either: https://mypy-play.net/?mypy=latest&python=3.10&flags=strict&gist=3ba27f6c0834c95cfdb5b2d752387275

@jpy-git
Copy link
Contributor Author
jpy-git commented Apr 1, 2022

Okay here's something odd. I've put a reveal_type around the line in question:

total_profit = sum(
                reveal_type(t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False)))

If I run pyright then I get this output:

/Users/joe/repos/freqtrade/freqtrade/persistence/models.py:1204:29 - information: Type of "t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False)" is "Generator[float | None, None, None]"
  /Users/joe/repos/freqtrade/freqtrade/persistence/models.py:1204:17 - error: Argument of type "Generator[float | None, None, None]" cannot be assigned to parameter "__iterable" of type "Iterable[_SumT@sum]" in function "sum"

All works as expected

However, with mypy:

../freqtrade/freqtrade/persistence/models.py:1204: note: Revealed type is "typing.Generator[Union[builtins.float, None], None, None]"
../freqtrade/freqtrade/persistence/models.py:1204: note: Revealed type is "typing.Generator[Any, None, None]"

No error raised and 2 revealed types?? Seems like that second one could be causing the error to get ignored?

And when I use the Any return type I get:

../freqtrade/freqtrade/persistence/models.py:1203: error: Value of type variable "_SumT" of "sum" cannot be "Optional[float]"
../freqtrade/freqtrade/persistence/models.py:1204: note: Revealed type is "typing.Generator[Union[builtins.float, None], None, None]"

error raised and only 1 revealed type

So is this a mypy specific issue?

@AlexWaygood
Copy link
Member

So is this a mypy specific issue?

I think so, almost certainly :) We should probably merge this as it is, if @JelleZijlstra is in agreement with me. It's just very strange that we can't reproduce it in miniature.

The "double output for reveal_type" thing is similar to python/mypy#10739, which might give us a clue.

@AlexWaygood
Copy link
Member

Something else that's interesting: this PR has also exposed a (completely unrelated) pyright bug, I think. The first commit, where the TypeVar was unbound in the first overload, should have triggered pyright's reportInvalidTypeVarUse check. So we really hit the jackpot here!

@JelleZijlstra
Copy link
Member

Yes, I think we should merge.

I tried some other variations in mypy playground but couldn't get the double reveal type. Normally that happens with TypeVars with constraints, but there aren't any here (or in the issue Alex linked above). Not sure what's happening there.

Copy link
Member
@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@Akuli
Copy link
Collaborator
Akuli commented Apr 2, 2022

I think I know what's going on with freqtrade. The change in this PR that silenced mypy_primer was:

diff --git a/stdlib/builtins.pyi b/stdlib/builtins.pyi
index 9372b2a1..ce2042bc 100644
--- a/stdlib/builtins.pyi
+++ b/stdlib/builtins.pyi
@@ -1485,7 +1485,7 @@ _SumT = TypeVar("_SumT", bound=_SupportsSum)
 _SumS = TypeVar("_SumS", bound=_SupportsSum)
 
 @overload
-def sum(__iterable: Iterable[_SumT]) -> Any | Literal[0]: ...
+def sum(__iterable: Iterable[_SumT]) -> _SumT | Literal[0]: ...
 
 if sys.version_info >= (3, 8):
     @overload

In freqtrade, the result of sum(...) is assigned to total_profit, which has type Any. Previously mypy tried _SumT = Optional[float] when solving the TypeVars, and that caused the error. But now that _SumT is also used in the return type, and the result is assigned to Any, it doesn't give up there. It instead tries _SumT = Any, which succeeds and silences the error. You can see the two tries in the reveal_type() output, too.

@JelleZijlstra
Copy link
Member

Thanks @Akuli, I missed that the variable was of type Any. I can reproduce the double reveal_type() now: https://mypy-play.net/?mypy=latest&python=3.10&flags=strict&gist=4b96aaf8e1261db4bf484c0f76138638 (though interestingly only with overloads involved).

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

Successfully merging this pull request may close these issues.

4 participants
0