Search code examples
pythonmypypython-typing

Mypy complaining about [no-any-return] rule being violated for obvious boolean expression


The following code throws a mypy error:

from typing import Dict, Any

def asd(x: Dict[str, Any]) -> bool:
    return x['a'] == 1

asd({"x": 2})

IMO it doesn't matter what is passed as a dict. x['a'] == 1 should always be a boolean. But mypy complains with:

test.py:4: error: Returning Any from function declared to return "bool"  [no-any-return]

Any Idea how to fix is besides peppering our codebase with # type: ignore[no-any-return]?

my setup.cfg for reference:

namespace_packages = True
explicit_package_bases = True
check_untyped_defs = True
warn_return_any = True
warn_unused_ignores = True
show_error_codes = True

Solution

  • The __eq__ method, one of Python's rich comparison methods, does not always return True or False, it can also return NotImplemented (or other values).

    This is used, for example, in the case where the first type cannot compare itself with the second, so a.__eq__(b) returns NotImplemented. In that case, it then tries b.__eq__(a) as a fallback (a). If that also returns NotImplemented, then I believe it falls back to the base object type to figure out equality.

    You can see the way mypy has defined the typing for this in its source code, specifically mypy/mypy/typeshed/stdlib/_operator.pyi:

    def eq(__a: object, __b: object) -> Any: ...
    

    That's why you're getting that particular message.

    As a quick fix, you can change the function to return bool(x['x'] == 1). This is exactly what Python itself does when evaluating the return value in a boolean context, so should be acceptable for placating mypy (b).


    (a) It's a little more complex than that, involving also the hierarchical type relationships of the two items, but the bottom line still stands: rich comparison operators do not always return a bool.


    (b) This is covered in the Python documentation for the data model (my emphasis):

    A rich comparison method may return the singleton NotImplemented if it does not implement the operation for a given pair of arguments. By convention, False and True are returned for a successful comparison. However, these methods can return any value, so if the comparison operator is used in a Boolean context (e.g., in the condition of an if statement), Python will call bool() on the value to determine if the result is true or false.

    Interestingly, though this doesn't change the validity of this answer, the section after that states:

    By default, object implements __eq__() by using is, returning NotImplemented in the case of a false comparison: True if x is y else NotImplemented.

    But I'm not convinced that's accurate. In the actual code, both == and != seem to result in True or False, depending on object identity, and never returning NotImplemented or raising an exception (the exceptions happen only for ordering comparisons like < or >=):

    /* If neither object implements it, provide a sensible default
       for == and !=, but raise an exception for ordering. */
    switch (op) {
        case Py_EQ:
            res = (v == w) ? Py_True : Py_False;
            break;
        case Py_NE:
            res = (v != w) ? Py_True : Py_False;
            break;
        default:
            _PyErr_Format(tstate, PyExc_TypeError, ...
    

    As pointed out in a comment, the identity checking is done at a different level to base objects, for example,the type objects. So I'd take that part of the documentation with a grain of salt :-)

    In any case, it shows that, while base objects may only give you back true or false, some derived objects can also give other values, which is why the warning you're seeing is correct.