Skip to content

problem with the type definition of sum #7974

Closed

Description

@ChristophePapazian

Everything is explained in microsoft/pylance-release#2865. I copy it to avoid redirection.

Environment data

Language Server version: 2022.5.3
OS and version: macOS 10.14.6 (Mojave)
Python version (& distribution if applicable, e.g. Anaconda): 3.10.4
Code Snippet

def test(N:int) -> bool:
  from random import choice
  return sum(a for a in [choice([True,False]) for _ in range(N)])==2

Expected behavior

This code should be perfectly fine.

Actual behavior

Problem detected on the last line
Pylance computes that it will always return False. Because it implies that sum on bool is always 0 or a bool.

Condition will always evaluate to False since the types "bool | Literal[0]" and "Literal[2]"
have no overlapPylance[reportUnnecessaryComparison]
(https://github.com/microsoft/pylance-release/blob/main/DIAGNOSTIC_SEVERITY_RULES.md#diagnostic-severity-rules)

Two problems here :
1/ bool should be considered as int, because they are ! (True+True==2)
2/ Even with ==1 at the end of the line, pylance seems to think that (True==1) is False.


Answer of Eric Traut when I posted this bug report on pylance :

I'm glad to see that you're using the reportUnnecessaryComparison diagnostic check! It's a strict-mode check, so most pylance users are not aware of it.

This looks like a bug in the type definition of sum, which is defined in the typeshed stubs here:

def sum(__iterable: Iterable[_SumT]) -> _SumT | Literal[0]: ...

That leads to the following:

x = sum([True, True, True])
reveal_type(x) # bool | Literal[0]

However, the correct (runtime) type of this expression is int.

So the bug will need to be fixed in the typeshed stubs. It will probably require a special-case overload for sum to properly model its behavior. Please file a bug (or a PR) in the typeshed project.

Here are a few suggested workarounds:

Use ints rather than bools in your code:

def test(N: int) -> bool:
    return sum([choice([1, 0]) for _ in range(N)]) == 2

Add a # pyright: ignore[reportUnnecessaryComparison] or # type: ignore comment to the end of the line.
Disable that diagnostic check for the file by adding a # pyright: reportUnnecessaryComparison=false comment at the top of the file.

Activity

  1. AlexWaygood commented on May 28, 2022

    @AlexWaygood
    Member

    We can add another overload to the stub here to fix this special case. I do agree with @erictraut that it's probably more idiomatic to use ints here, though.

  2. ChristophePapazian commented on May 28, 2022

    @ChristophePapazian
    Author

    I want to count the number of True in a generator. Sum does that.
    What's really wrong to me is that python considers True as 1 subtyped as a bool and False as 0 subtyped as bool. So 3-True is perfectly ok (because bool are int) but the type system doesn't seem to treat bool as a subtype of int

    We can certainly add another overload to the stub to account for this special case. I think the more idiomatic way of writing your particular snippet would be this, however:

    def test(N:int) -> bool:
      from random import choice
      return sum(1 for a in [choice([True,False]) for _ in range(N)])==2
  3. AlexWaygood commented on May 28, 2022

    @AlexWaygood
    Member

    We can certainly add another overload to the stub to account for this special case. I think the more idiomatic way of writing your particular snippet would be this, however:

    def test(N:int) -> bool:
      from random import choice
      return sum(1 for a in [choice([True,False]) for _ in range(N)])==2

    This comment of mine was incorrect; I deleted it shortly after posting it :)

    the type system doesn't seem to treat bool as a subtype of int

    Not quite. The type system does recognise bool as a subtype of int; that's not what causes the issue here.

    Here's the stub as it exists currently. _SumT here is solved as being bool, which is what is causing the error:

    typeshed/stdlib/builtins.pyi

    Lines 1548 to 1552 in 3191c1a

    _SumT = TypeVar("_SumT", bound=SupportsAdd)
    _SumS = TypeVar("_SumS", bound=SupportsAdd)
    @overload
    def sum(__iterable: Iterable[_SumT]) -> _SumT | Literal[0]: ...

    But when you add True + False, the result is of type int, not type bool:

    >>> type(True + False)
    <class 'int'>

    None of the sum overloads currently take account of the fact that adding two objects of the same type does not always result in the same type being returned. This is a general problem that's quite hard to solve here, but we can certainly add a special case for this specific instance.

  4. added a commit that references this issue on May 28, 2022
    38772e4
  5. ChristophePapazian commented on May 28, 2022

    @ChristophePapazian
    Author

    Perfect ! Thank you 😀

  6. added a commit that references this issue on May 28, 2022
    d4eba8b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions