Skip to content

bool() return type on with no arguments #6069

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

Open
sobolevn opened this issue Sep 24, 2021 · 13 comments · May be fixed by #10465
Open

bool() return type on with no arguments #6069

sobolevn opened this issue Sep 24, 2021 · 13 comments · May be fixed by #10465

Comments

@sobolevn
Copy link
Member

sobolevn commented Sep 24, 2021

Right now bool has this __new__ signature: def __new__(cls: Type[_T], __o: object = ...) -> _T: ...

Should not it be:

@overload
def __new__(cls) -> Literal[False]: ...
@overload
def __new__(cls: Type[_T], __o: object) -> _T: ...

?

Current source:

def __new__(cls: Type[_T], __o: object = ...) -> _T: ...

What do you think?

@srittau
Copy link
Collaborator

srittau commented Sep 24, 2021

Sounds worth trying.

@sobolevn
Copy link
Member Author

Will create a PR soon 🙂

@srittau
Copy link
Collaborator

srittau commented Sep 24, 2021

One problem I see:

x = bool()
...
x = determine_truth()  # error

@sobolevn
Copy link
Member Author

sobolevn commented Sep 24, 2021

Yes, looks like this is the case.

My main motivation was the fact that I saw this code today:

if bool():   # it should have been `self.bool()` in my case
    print('trying to do something, but it is unreachable')

It was not even caught by --warn-unreachable. Changing bool() signature will make it detectable.
Probably, there are other options, which are less radical 🙂

But, here's the problem you've described:

from typing import Literal

def lit() -> Literal[False]:
    """Imagine, that this is a new `bool.__new__` constructor."""

x = lit()
...
x = bool()  # E: Incompatible types in assignment (expression has type "bool", variable has type "Literal[False]")

@Akuli
Copy link
Collaborator

Akuli commented Sep 25, 2021

I think it's fine to return Literal[False] from bool(). If you have bool() with no arguments in your code, it's fine if it causes errors, because you really should use the more readable x = False instead.

We still can't make it so that bool() with no arguments is a type-checking error, because it would mean that collections.defaultdict(bool) no longer works. It calls bool() with no arguments to create values for missing dict keys.

def __new__(cls: Type[_T], __o: object) -> _T: ...

This isn't needed, because bool cannot be subclassed.

@Akuli
Copy link
Collaborator

Akuli commented Sep 25, 2021

Actually, collections.defaultdict(bool) won't work if bool() returns Literal[False]. It is defined like this:

class defaultdict(Dict[_KT, _VT], Generic[_KT, _VT]):
    ...
    @overload
    def __init__(self, default_factory: Callable[[], _VT] | None) -> None: ...
    ...

So if you pass in something that returns Literal[False] when called with no arguments, the value type _VT will also be Literal[False]. It is basically the same problem as in the x = bool() example, but perhaps more likely to happen in practice.

@sobolevn
Copy link
Member Author

I think it's fine to return Literal[False] from bool(). If you have bool() with no arguments in your code, it's fine if it causes errors, because you really should use the more readable x = False instead.

I fear that this change will have a lot of false positives. And types here would be inconsistent:

# 1. will have `bool` type:
a = False

# 2. will have `Literal[False]` type:
a = bool()

But, in runtime False is bool() 😞

@JelleZijlstra
Copy link
Member

This might cause problems for the mypy test suite, because it frequently does if bool(): to get a condition mypy doesn't understand.

Also, we shouldn't need the self type, since bool cannot be subclassed.

@hauntsaninja
Copy link
Collaborator

The mypy test suite's behaviour should be largely determined by the type fixtures, so hopefully not too bad. In any case, if this is a change we want to make, we should make it.

@sobolevn
Copy link
Member Author

sobolevn commented Sep 29, 2021

There are primitive types with the same logic: str() -> Literal[''], bytes() -> Literal[b''], int() -> Literal[0]. If we are going to make this, I guess it is a good idea to make this consistent across all primitives.

So, if str() will always be unreachable as well.

@sobolevn
Copy link
Member Author

sobolevn commented Oct 7, 2021

I had a moment to think about this problem. And it looks like we won't able to solve this without a slight modification to @overload and Literal type checking rules in mypy/etc.

So, I propose to add "weak literal" types. What are they?
Weak literal type is still a literal type with some primitive value, except it can be upcasted to its fallback type with no error.

How do we create "weak literal" types? It can be only be created when some Literal type is returned from @overload function and all other cases do return the same fallback type. Here are some example:

def some() -> Literal[False]: ...

s = some()  # Literal[False, weak=False]
s = True  # error

class bool:
  @overload
  def __init__() -> Literal[False]: ...
  @overload
  def __init__(arg) -> 'bool': ...  # `Literal[False]` has `bool` as a fallback type, so it is considered "weak"

b = bool()  # Literal[False, weak=True]
b = True  # ok

class other:
  @overload
  def __init__() -> Literal[False]: ...
  @overload
  def __init__(arg) -> str: ...  # `Literal[False]` is not compatible with `str`, literal is strong

o = other()  # Literal[False, weak=False]
o = True  # error

What do you think?

@Akuli
Copy link
Collaborator

Akuli commented Oct 7, 2021

I'm not aware of any use cases for non-weak literals, so I mostly see this as an annoying "feature" of mypy.

@AlexWaygood AlexWaygood linked a pull request Jul 19, 2023 that will close this issue
@macro1
Copy link

macro1 commented May 3, 2025

Reading through this and searching around.. I wonder if this would be a mypy feature?

Enum actually has it already:

from enum import Enum


class MyBool(Enum):
    TRUE = True
    FALSE = False


reveal_type(MyBool.TRUE)  # Revealed type is "Literal[test.MyBool.TRUE]?"
x = MyBool.TRUE
reveal_type(x)  # Revealed type is "test.MyBool"

Based on some searching I think this behavior is related to tracking last_known_value within mypy.

So mypy would need to know that bool() returns a bool type with last known value of Literal[False] which would be acceptable as a literal, but would not coerce any variable created from it to be a literal.

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 a pull request may close this issue.

6 participants