Skip to content

(🐞) overloads that exhaust finite Literals(bool/Enum) not treated as exhaustive #14764

Description

@KotlinIsland
@overload
def foo(a: Literal[True]) -> int: ...

@overload
def foo(a: Literal[False]) -> str: ...

def foo(a: bool) -> object: ...

a: bool
reveal_type(foo(a))  # error: No overload variant of "foo" matches argument type "bool"
@overload
def foo(a: Literal[True]) -> int: ...

@overload
def foo(a: Literal[False]) -> str: ...

@overload
def foo(a: bool) -> object: ...  # no error regarding impossible to match overload

a: bool
reveal_type(foo(a))  # object
@overload
def foo(a: Literal[True, False]) -> int | str: ...

@overload
def foo(a: bool) -> object: ...  # error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader

def foo(a: bool) -> object: ...

a: bool
reveal_type(foo(a))  # int | str

Here mypy incorrectly forces us to implement a completely redundant overload for the non Literal case when it is already exhaustively covered by both literals. It is only when the literals are in the same overload (useless in practice, but just for demonstration) that mypy correctly handles this case.

Mypy should be doing 'union math' (or what ever it's called) to apply both literal overloads at once.

This also affects all other exhaustible Literals such as Enums.

This example is pulled directly from the docs, so I think they should be updated as well to an example that doesn't contain this confusing defect.

Activity

  1. Zeckie commented on Feb 23, 2023

    @Zeckie

    Is this issue really specific to bool, or does it also occur with enums and unions?

  2. yehoshuaviles commented on Feb 23, 2023

    @yehoshuaviles

    You are correct that in this example, mypy is not able to correctly handle the case where the argument a is a boolean value and not a Literal. This is because the Literal types in the overloads are not being unioned with the bool type in the function definition.

    To address this issue, you can modify the function definition to include a union type that includes all possible values of a:

    from typing import Literal, overload, Union
    
    @overload
    def foo(a: Literal[True]) -> int: ...
    
    @overload
    def foo(a: Literal[False]) -> str: ...
    
    def foo(a: Union[bool, Literal[True], Literal[False]]) -> Union[int, str]:
        if a is True:
            return 42
        else:
            return "Hello, world!"
    
    a: bool
    reveal_type(foo(a))  # This now correctly shows Union[int, str]

    In this version, we have modified the foo function definition to include a union type that includes all possible values of a. We have also modified the return type to be a union of the possible return types from the two overload functions.

    Now, mypy is able to correctly handle the case where a is a boolean value and not a Literal.

  3. changed the title [-](🐞) `Literal` `True` and `False` not an exhaustive `bool` when in `overload`s[/-] [+](🐞) `Literal` overloads not treated as exhaustive[/+] on Feb 23, 2023
  4. KotlinIsland commented on Feb 23, 2023

    @KotlinIsland
    ContributorAuthor

    Is this issue really specific to bool, or does it also occur with enums and unions?

    It works correctly with unions, but not other exhaustible Literals like Enums.

    from typing import *
    from enum import Enum
    
    class A(Enum):
        a = 1
    
    @overload
    def foo(a: Literal[A.a]) -> int: ...
    
    @overload
    def foo(a: A) -> object: ...  # no error
    
    def foo(a: object) -> object: ...
    
    a: A
    reveal_type(foo(a))  # object
  5. changed the title [-](🐞) `Literal` overloads not treated as exhaustive[/-] [+](🐞) `overload`s that exhaust finite `Literal`s(`bool`/`Enum`) not treated as exhaustive[/+] on Feb 23, 2023
  6. KotlinIsland commented on Feb 24, 2023

    @KotlinIsland
    ContributorAuthor
  7. mimre25 commented on May 16, 2023

    @mimre25

    I also stumbled upon this. There is also a problem with type inference or so:

    @overload
    def test_overload(is_fun: Literal[True]) -> str:
        ...
    
    @overload
    def test_overload(is_fun: Literal[False]) -> int:
        ...
        
    def test_overload(is_fun: bool = True) -> str | int:
        return "1" if is_fun else 1

    this yields no error, but as soon as we use test_overload, we will get an error:

    def foo(bar: bool = True) -> str | int:
       return test_overload(bar) 
    
    ####
    t.py:76: error: Returning Any from function declared to return "Union[str, int]"  [no-any-return]
    t.py:76: error: No overload variant of "test_overload" matches argument type "bool"  [call-overload]
    t.py:76: note: Possible overload variants:
    t.py:76: note:     def test_overload(is_fun: Literal[True]) -> str
    t.py:76: note:     def test_overload(is_fun: Literal[False]) -> int
    Found 2 errors in 1 file (checked 1 source file)            

    What could go wrong here? The only way to get out of this situation is to add an overload that access bool and returns the union a la:

    @overload
    def test_overload(is_fun: bool) -> str | int:
        ...
  8. gandhis1 commented on May 16, 2023

    @gandhis1

    I might not be following entirely. Isn't this correct behavior? There are two cases to be handled for all non-literal types - the value is known statically, and the value is not known statically. Covering the two literal cases with overloads could only ever be used for the former, and not for the latter. The bool case is necessary for when the value is not known.

    If instead of taking a bool, you accepted a Literal[True, False], that should work, because you are properly communicating that all callers must be using a variable that can be statically narrowed to a specific value (or an actual literal).

  9. KotlinIsland commented on May 16, 2023

    @KotlinIsland
    ContributorAuthor

    @gandhis1 That's not how finite types work, and Literal types are not 'literal', they are value/singleton inhabitant types. The type bool is exactly the same as the type True | False in the same way that a union of all derived types within a sealed hierarchy is the same type as the root of that hierarchy.

  10. penguinolog commented on Jul 5, 2023

    @penguinolog

    Looks like the same issue in pyright, but for them it's regression now: microsoft/pyright#5421

  11. finite-state-machine commented on Apr 22, 2024

    @finite-state-machine

    Likely related: #15456

  12. flying-sheep commented on Feb 28, 2025

    @flying-sheep

    This leads to false positives:

    from typing import overload, Literal
    
    class ndarray: ...
    class DaskArray: ...
    
    @overload
    def to_dense(a: ndarray, *, to_memory: bool = False) -> ndarray: ...
    @overload
    def to_dense(a: DaskArray, *, to_memory: Literal[False] = False) -> DaskArray: ...
    @overload
    def to_dense(a: DaskArray, *, to_memory: Literal[True]) -> ndarray: ...
    
    def to_dense(*args: object, **kw: object) -> object: ...
    
    x: ndarray | DaskArray
    to_memory: bool
    to_dense(x, to_memory)

    →

    main.py:17: error: No overload variant of "to_dense" matches argument types "ndarray | DaskArray", "bool"  [call-overload]
    main.py:17: note: Possible overload variants:
    main.py:17: note:     def to_dense(a: ndarray, *, to_memory: bool = ...) -> ndarray
    main.py:17: note:     def to_dense(a: DaskArray, *, to_memory: Literal[False] = ...) -> DaskArray
    main.py:17: note:     def to_dense(a: DaskArray, *, to_memory: Literal[True]) -> ndarray
    Found 1 error in 1 file (checked 1 source file)
    

    https://mypy-play.net/?mypy=latest&python=3.13&gist=3947314f9a3a3016e4e5b0dc82a696f8&flags=strict

  13. erikbwu commented on May 23, 2025

    @erikbwu

    I am facing the same problem. Any fixes yet?

  14. srittau commented on Jul 10, 2025

    @srittau
    Contributor

    The typing spec now also agrees that the current behavior is wrong: https://typing.python.org/en/latest/spec/overload.html#argument-type-expansion

  15. mimre25 commented on Jul 13, 2025

    @mimre25

    I've created a draft PR here with some small change set that gets the job done for bools and enums:
    #19431

    Note: this would be my first contribution to mypy and thus I will certainly need to do some more reading into the code base and contribution guidelines to see if this is a sound contribution.
    That said, early feedback is welcome 🙂

  16. truenicoco commented on Dec 19, 2025

    @truenicoco

    I am unsure if this is the same issue or not, but I have this similar issue, a MWE is worth a thousand words, isn't it?

    from typing import Literal, overload
    
    
    @overload
    def get(arg: Literal[True]) -> str: ...
    
    
    @overload
    def get(arg: Literal[False]) -> str: ...
    
    
    def get(arg: bool) -> str:
        raise NotImplementedError
    
    
    get(True) # OK
    get(False) # OK
    
    x: bool
    get(x)      # No overload variant of "get" matches argument type "bool"  [call-overload] 
    get(0 == 0) # No overload variant of "get" matches argument type "bool"  [call-overload]

    Should I open a new issue or is it covered here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions