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

mypy fails to narrow Union[float,int] to float after isinstance(x, float) #6060

Closed
DarwinAwardWinner opened this issue Dec 12, 2018 · 5 comments · Fixed by #6114 or #13781
Closed

mypy fails to narrow Union[float,int] to float after isinstance(x, float) #6060

DarwinAwardWinner opened this issue Dec 12, 2018 · 5 comments · Fixed by #6114 or #13781
Assignees
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal topic-type-narrowing Conditional type narrowing / binder

Comments

@DarwinAwardWinner
Copy link

Mypy fails to typecheck the following valid code:

from typing import Union
def maybe_convert_to_int(x: Union[float,int]) -> Union[float,int]:
    '''Convert x from float to int if it can be done losslessly.'''
    if isinstance(x, float):
        # x must be a float here, so this is a legal operation, but
        # mypy still thinks its type is Union[float,int]
        assert isinstance(x, float)
        if x.is_integer():
            x = int(x)
    return x

When I run mypy on it, I get:

$ mypy ~/temp/test_mypy.py
temp/test_mypy.py:8: error: Item "int" of "Union[float, int]" has no attribute "is_integer"
$ python --version
Python 3.7.1
$ mypy --version
mypy 0.650

I would expect this program to pass the type checker, since I am using isinstance to narrow the type to float before performing the float-specific operation float.is_integer. If I replace the assertion with x = float(x), that satisfies the type checker.

@ilevkivskyi
Copy link
Member

This is because some special-casing in mypy called "type promotions". The point is that int is not a subclass of float at runtime, but mypy considers int a subtype of float. Because of this, int sneaks into the if statement, where mypy complains about the missing method.

We try to ignore the type promotions while working with isinstance(), but it looks like there is a bug and type promotions are still used. Another possible workaround (apart from the one you mentioned) for you would be to just annotate the functions as (float) -> float. You can probably also change the logic to

def maybe_convert_to_int(x: float) -> float:
    if isinstance(x, int):
        return x
    if x.is_integer():
        x = int(x)
    return x

@ilevkivskyi ilevkivskyi added bug mypy got something wrong priority-1-normal false-positive mypy gave an error on correct code labels Dec 12, 2018
@ilevkivskyi
Copy link
Member

@Michael0x2a was among the last who worked with this code, so probably he has the most context.

@DarwinAwardWinner
Copy link
Author

I discovered a workaround that eliminated this code entirely for my specific use case. The reason for this code was to ensure that a float like 1.0 would print as an integer (with no decimal point), but I figured out that '{:g}'.format(x) does that without having to check the type of x.

However, I guess this is still a bug in the general case.

DarwinAwardWinner added a commit to DarwinAwardWinner/roll that referenced this issue Dec 12, 2018
As described here: python/mypy#6060

As a special case, int is considered a subtype of float, so the type
union is redundant.
@Michael0x2a Michael0x2a self-assigned this Dec 12, 2018
Michael0x2a added a commit to Michael0x2a/mypy that referenced this issue Dec 29, 2018
This pull request resolves python#6060
by modifying `conditional_type_map` function in `checker.py` to ignore
promotions.
Michael0x2a added a commit that referenced this issue Dec 31, 2018
This pull request resolves #6060
by modifying `conditional_type_map` function in `checker.py` to ignore
promotions.
@msullivan msullivan reopened this Jan 11, 2019
@GregHilston
Copy link

I believe I'm still experiencing this issue:

processed_corpus: Dict[str, Dict[str, Union[List[Dict[Any, Any]], float]]] = {}

# bunch of code removed

if sentence not in processed_corpus:
    processed_corpus[sentence] = {"extra": extra}
 # satisfying mypy due to Union in type hinting
elif sentence in processed_corpus and isinstance(processed_corpus[sentence]["extra"], list):
    processed_corpus[sentence]["extra"].extend(extra)

Still getting a mypy error of

error: Item "float" of "Union[List[Dict[Any, Any]], float]" has no attribute "extend"

@syastrov
Copy link
Contributor

I can also reproduce this, or a similar issue having to do with statement reachability, both on mypy 0.740 and master (mypy 0.750+dev.e97377c454a1d5c019e9c56871d5f229db6b47b2)

from typing import Union
def get_number_type(v: Union[float, int]) -> str:
    if isinstance(v, float):
        return "float"
    if isinstance(v, int):
        return "int"  # Error: Statement is unreachable

Changing the order of the isinstance checks makes the error go away.

@AlexWaygood AlexWaygood added the topic-type-narrowing Conditional type narrowing / binder label Mar 27, 2022
ilevkivskyi added a commit that referenced this issue Oct 3, 2022
Fixes #13760 
Fixes #6060
Fixes #12824

This is a right thing to do, but let's what `mypy_primer` will be. This
also required re-applying #6181 (which is also a right thing to do)
otherwise some tests fail.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code priority-1-normal topic-type-narrowing Conditional type narrowing / binder
Projects
None yet
7 participants