Skip to content
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

Check expr that are implicitly true for lack dunder bool/len #10666

Merged
merged 33 commits into from
Sep 3, 2021

Conversation

ikonst
Copy link
Contributor

@ikonst ikonst commented Jun 18, 2021

Optional check which disallows using implicitly true expressions (whose type has no __bool__ nor __len__) in boolean context. The check is only enabled when using strict optionals.

This does not affect type narrowing / reachability which would continue seeing such types as "can be true / can be false". The rationale is that it's not an error per the data model to evaluate any object as boolean, and an always-true behavior could be overridden in a derived type.

Some builtin fixtures are updated to make sure common types (str, bool, int) are not considered to be always true.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ikonst ikonst changed the title WIP: recognize objects as truthy by default Recognize objects as truthy by default Jun 20, 2021
mypy/types.py Outdated
@@ -842,6 +842,14 @@ def __init__(self, typ: mypy.nodes.TypeInfo, args: Sequence[Type],
# Literal context.
self.last_known_value = last_known_value

if (
self.type and
not self.type.has_readable_member('__bool__') and
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we have to check for Python 2 __nonzero__?

@ikonst ikonst marked this pull request as ready for review June 20, 2021 04:58
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@JukkaL
Copy link
Collaborator

JukkaL commented Jun 21, 2021

Teaches mypy to recognize instances that don't implement __bool__ or __len__ as always true.

Since subclasses can freely add __bool__, this doesn't sounds right. This is something we could perhaps infer for final classes, but the benefit would be marginal, since most users don't use @final much.

@ikonst
Copy link
Contributor Author

ikonst commented Jun 21, 2021

Since subclasses can freely add bool, this doesn't sounds right.

I see that. Factoring such "knowledge" into unreachable code detection would be practically incorrect.

def f(a: object) -> None:
   a.foobar()
   _ = len(a)

For the cases above it's easy to justify a type error since, while foobar and __len__ could be added in a subclass, those operations would raise an error on object, while using object in boolean context succeeds "blindly".

I wonder how it'll be practical to add such check, perhaps through an optional warning, i.e. make it more of a linting rule and not something that affects analysis?

I keep seeing this in real world code, e.g.

def f(a: Iterable) -> None:
   if a:  # signature says Iterable but we're really expecting Collection and up
       ...

and while it's not technically incorrect, it's likely an error in typing (s/Iterable/Collection/).

Here's another favorite:

class MyMessage(google.protobuf.Message):
    ...

def f(a: MyMessage) -> None:
    if a.sub_message:  # user assumes missing Protobuf field manifests as None but it's an eagerly created empty message
        ...

Per Protobuf guidance, one shouldn't inherit from messages, so those could indeed be marked @final (e.g. in mypy-protobuf) but I agree @final is not widely used and not very practical.

@JukkaL
Copy link
Collaborator

JukkaL commented Jun 21, 2021

This sounds like a good match for an optional check that can be activated via enabling some error code. Also --strict could enable it. If it's an optional check, it should have no impact on type checking or type inference when disabled, but that seems doable.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ikonst
Copy link
Contributor Author

ikonst commented Jun 25, 2021

@JukkaL Would it be OK to bolt this unto find_isinstance_check_helper like in 801f274? This checker seems to be already in the business of figuring out types in boolean context.

Also, is there anything that can print a node in a way that's short enough for messages, e.g.

note: NameExpr(m [l]) has type 'typing.Match[builtins.str*]' which does not implement __bool__ or __len__ so it will always be truthy in boolean context

Obviously I'd like something friendlier than NameExpr(m [l]). For some nodes getting a "name" is more obvious than others. Perhaps the source code of the node is the closest to what I'd expect. It's also possible that I'm worrying too much and that printing the type in context (line, column) is enough to explain what's going on.

@github-actions

This comment has been minimized.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Jun 25, 2021

I don't know that anything else in mypy prints nodes, and in general it looks pretty noisy to me. Most mypy error messages just print out a type and it seems to be fine. If you want, consider special casing RefExpr, and not printing the node for anything else.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

mypy/checker.py Outdated Show resolved Hide resolved
mypy/checker.py Outdated Show resolved Hide resolved
mypy/checker.py Outdated Show resolved Hide resolved
mypy/checker.py Outdated Show resolved Hide resolved
mypy/checker.py Outdated Show resolved Hide resolved
test-data/unit/check-errorcodes.test Outdated Show resolved Hide resolved
test-data/unit/check-errorcodes.test Outdated Show resolved Hide resolved
test-data/unit/check-errorcodes.test Outdated Show resolved Hide resolved
@ikonst
Copy link
Contributor Author

ikonst commented Aug 5, 2021

Thanks for updating the copy @hauntsaninja.

@hauntsaninja
Copy link
Collaborator

@ikonst Just made some more copy changes (including changing the error code name) and added some test case for if not truthy_expression. If it looks good to you, I'll merge.

My concerns with the phrasing:

  1. The phrasing of "always" wasn't quite true, since subclasses could implement __bool__, so I tried to fudge that a little.
  2. I don't know that "implicit bool" clearly describes the feature. I was also a little concerned about the potential for confusion with another requested mypy feature: to not treat bool as a subtype of int. Lmk if you have other suggestions.

@hauntsaninja
Copy link
Collaborator

hauntsaninja commented Aug 8, 2021

Oh actually, I think I like the error code "bool-conversion" for this. Thoughts?

@ikonst
Copy link
Contributor Author

ikonst commented Aug 8, 2021

Didn't yet review all changes, but "bool-conversion" rings like that "strict bool" feature we used to have. That is, it insinuates that relying on bool conversion is wrong perhaps even in cases where it's idiomatic Python.

--------------------------------------------------------------------------------

Warn when an expression whose type does not implement __bool__ or __len__ is used in boolean context
since it could always evaluate to true (if one also assumes that no subtype implements __bool__ or
Copy link
Contributor Author

Choose a reason for hiding this comment

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

"You should always take an umbrella since it could always rain" -- not exactly what we mean :)

Maybe something like "since by default objects evaluate as true"?

The thing with subtypes is correct but I wonder if we can position it differently? Essentially we're warning due to lack of type-based guarantee of meaningful bool. What would we say if the python data model would be defined in such a way that len(object()) == 0)? What would our warning, if any, be like?

@ikonst
Copy link
Contributor Author

ikonst commented Sep 2, 2021

@hauntsaninja ping :)

@hauntsaninja
Copy link
Collaborator

Yeah, something like "since by default objects always evaluate to true (this warning assumes that no subtype implements bool ..." seems good to me.
Feel free to make phrasing changes, or if you like it as is, let me know and I'll merge. I'm happy with most versions of the error code and docs now that we have the subtype thing mentioned.

@hauntsaninja hauntsaninja merged commit a72fab3 into python:master Sep 3, 2021
@hauntsaninja
Copy link
Collaborator

Thanks for making this happen!

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.

3 participants