Skip to content

RUF038 Surprising fix for Literal[True, False] in functions with overloads #16129

@Geo5

Description

@Geo5

Description

I stumbled upon some code of mine where it seems to me that mypy and ruff do not agree whether Literal[True, False] == bool and rule RUF038 suggest a fix which introduces a mypy error.

Edit: I read the docs for the rule again and this general case seems to be mentioned in the "Fix safety" already. Maybe this example is different, but if you feel like it is already covered well enough, feel free to close the issue!


Consider this code containing two @overloads and illustrating perhaps two different problems:

import random
from typing import Literal, overload


class Foo:
    @overload
    def foo(self, *, bar: Literal[True]) -> int: ...

    @overload
    def foo(self, *, bar: Literal[False]) -> float: ...

    def foo(self, *, bar: Literal[True, False]) -> int | float:
        return 0


class FooSub(Foo):
    @overload
    def foo(self, *, bar: Literal[True]) -> int: ...

    @overload
    def foo(self, *, bar: Literal[False]) -> float: ...

    def foo(self, *, bar: Literal[True, False]) -> int | float:
        return super().foo(bar=bar)


def compute_bool() -> bool:
    return random.random() > 10


Foo().foo(bar=compute_bool())  # Problem 1: For this line mypy raises an error
$ mypy --strict test.py
test.py:31: error: No overload variant of "foo" of "Foo" matches argument type "bool"  [call-overload]
test.py:31: note: Possible overload variants:
test.py:31: note:     def foo(self, *, bar: Literal[True]) -> int
test.py:31: note:     def foo(self, *, bar: Literal[False]) -> float
Found 1 error in 1 file (checked 1 source file)

And ruff suggest fixing the annotations like this:

$ ruff check --isolated --select RUF --preview test.py
test.py:12:27: RUF038 `Literal[True, False]` can be replaced with `bool`
   |
10 |     def foo(self, *, bar: Literal[False]) -> float: ...
11 |
12 |     def foo(self, *, bar: Literal[True, False]) -> int | float:
   |                           ^^^^^^^^^^^^^^^^^^^^ RUF038
13 |         return 0
   |
   = help: Replace with `bool`

test.py:23:27: RUF038 `Literal[True, False]` can be replaced with `bool`
   |
21 |     def foo(self, *, bar: Literal[False]) -> float: ...
22 |
23 |     def foo(self, *, bar: Literal[True, False]) -> int | float:
   |                           ^^^^^^^^^^^^^^^^^^^^ RUF038
24 |         return super().foo(bar=bar)
   |
   = help: Replace with `bool`

Found 2 errors.
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).

Resulting in:

import random
from typing import Literal, overload


class Foo:
    @overload
    def foo(self, *, bar: Literal[True]) -> int: ...

    @overload
    def foo(self, *, bar: Literal[False]) -> float: ...

    def foo(self, *, bar: bool) -> int | float:
        return 0


class FooSub(Foo):
    @overload
    def foo(self, *, bar: Literal[True]) -> int: ...

    @overload
    def foo(self, *, bar: Literal[False]) -> float: ...

    def foo(self, *, bar: bool) -> int | float:
        return super().foo(bar=bar)  # Problem 2 introduced


def compute_bool() -> bool:
    return random.random() > 10


Foo().foo(bar=compute_bool()) # Problem 1 again, same as unfixed code

And again mypy on the fixed code:

$ mypy --strict test_fixed.py
test_fixed.py:24: error: Returning Any from function declared to return "int | float"  [no-any-return]
test_fixed.py:24: error: No overload variant of "foo" of "Foo" matches argument type "bool"  [call-overload]
test_fixed.py:24: note: Possible overload variants:
test_fixed.py:24: note:     def foo(self, *, bar: Literal[True]) -> int
test_fixed.py:24: note:     def foo(self, *, bar: Literal[False]) -> float
test_fixed.py:31: error: No overload variant of "foo" of "Foo" matches argument type "bool"  [call-overload]
test_fixed.py:31: note: Possible overload variants:
test_fixed.py:31: note:     def foo(self, *, bar: Literal[True]) -> int
test_fixed.py:31: note:     def foo(self, *, bar: Literal[False]) -> float
Found 3 errors in 1 file (checked 1 source file)

$ python --version
Python 3.11.10
$ ruff --version
ruff 0.9.6
$ mypy --version
mypy 1.15.0 (compiled: yes)

Metadata

Metadata

Assignees

No one assigned

    Labels

    ruleImplementing or modifying a lint rule

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions