Skip to content

Explicit type declaration overridden/merged with Any #19034

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
PeterJCLaw opened this issue May 5, 2025 · 2 comments
Open

Explicit type declaration overridden/merged with Any #19034

PeterJCLaw opened this issue May 5, 2025 · 2 comments
Labels
bug mypy got something wrong topic-join-v-union Using join vs. using unions

Comments

@PeterJCLaw
Copy link
Contributor

PeterJCLaw commented May 5, 2025

Bug Report

It appears that explicit type definitions of loop variables can be overridden or merged with values inferred from the iterator. Unfortunately this happens even when the iterator type is Any, somewhat undermining the explicit definition.

To Reproduce

import itertools, typing

class ErrorType:
    pass

def report_errors(error_type: ErrorType, id_: object, messages: list[str]) -> None:
    pass

errors: list[typing.Any]

source: tuple[ErrorType, object] | None
for source, errors_group in errors:
    reveal_type(source)  # good: Union[tuple[demo.ErrorType, builtins.object], None]
                         # bad: Union[tuple[demo.ErrorType, builtins.object], Any]
    if source:
        reveal_type(source)  # good: tuple[demo.ErrorType, builtins.object]
                             # bad: Union[tuple[demo.ErrorType, builtins.object], Any]

        messages: list[str]
        report_errors(*source, messages)  # bad: Too many arguments for "report_errors"

I've tried reproducing this without the loop, however that seems to make the issue go away.

Removing the messages argument to report_errors also seems to avoid the "too many arguments" error, though that doesn't affect the revealed types so I've left it in to give a clearer pass/fail when running mypy.

Expected Behavior

Under 1.15 and up to 8dd616b the above code passes cleanly with the "good" noted revealed types.

Actual Behavior

From b50f3a1 onwards the above code fails type checking due to the errant message to the report_errors function, and outputs the "bad" noted revealed types. I'm not familiar enough with mypy to explain why that commit is the cause, however I have narrowed this to that commit using git bisect so I'm fairly confident those changes are related.

Your Environment

Versions as noted above. No command line flags or other config needed.

Python 3.12.3 on Ubuntu 24.04.

@PeterJCLaw PeterJCLaw added the bug mypy got something wrong label May 5, 2025
@sterliakov sterliakov added the topic-join-v-union Using join vs. using unions label May 5, 2025
@sterliakov
Copy link
Collaborator

Adding Any to union should be harmless, I think that's the desired outcome of #18538 (but let's ask @ilevkivskyi to confirm).

However, the error on the last line is really bad - we should not reject func(*Any, arg) if func takes one or more arguments, and so a tuple[correct, length] | Any shouldn't be rejected either.

In other words, this is a bug IMO:

from typing import Any

def report_errors(error_type: int, id_: str, messages: list[str]) -> None:
    pass

any_: Any
messages: list[str]
report_errors(*any_)  # OK
report_errors(*any_, messages)  # E: Too many arguments for "report_errors"  [call-arg]

When there's Any unpacked in the arguments list, it can be something unpacking to the right number of arguments. Since Any is an escape hatch, that should be good enough - we aren't able to prove there's an error. Why do we reject such calls?

@PeterJCLaw
Copy link
Contributor Author

PeterJCLaw commented May 7, 2025

Thanks for looking at this, I'd be interested to hear more about what's expected here.

Note that the addition of the Any to the union does also remove the None from it, which is there explicitly in the declaration. That also feels like it would lead to bugs that would have been caught previously.

Even if adding Any to a union is normally ok, it's not clear to me that that is what should be happening here. The intent of the code is to declare an exact type, which is the modified implicitly in a manner which (to me) feels out of step with what happens elsewhere.

Consider for example:

foo: int | None
def bar(): ...

foo = bar()

reveal_type(foo)    # previously: "Union[builtins.int, None]"
                    # now: "Union[builtins.int, Any]"
reveal_type(bar())  # "Any"

foo.bit_length()    # previously: error: Item "None" of "int | None" has no attribute "bit_length"  [union-attr]
                    # now: no error

Which I think is equivalent -- the key thing is that previously the type explicitly declared was respected and this was a way to indicate what the type is locally when we know the return type even if it's not annotated. With the change, Any appears explicitly and replaces None. This seems likely to cause lots of false negatives which would previously have been caught.

This does seem to be specific to the case of the union having None though. Testing with int | str in the above works as I'd expect -- the union remains just int | str throughout and the final line errors as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-join-v-union Using join vs. using unions
Projects
None yet
Development

No branches or pull requests

2 participants