Skip to content

Type checking fails to distribute across Tuple types #18922

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
bcmills opened this issue Apr 14, 2025 · 8 comments
Open

Type checking fails to distribute across Tuple types #18922

bcmills opened this issue Apr 14, 2025 · 8 comments
Labels
bug mypy got something wrong false-positive mypy gave an error on correct code topic-union-types

Comments

@bcmills
Copy link

bcmills commented Apr 14, 2025

Bug Report

Union types fail to distribute across Tuple types as they ought to when the set of concrete values of each type are identical.

To Reproduce

(https://mypy-play.net/?mypy=latest&python=3.12&gist=9d0052b0ed30856e5ad3985237f8754b)

from typing import Union, Tuple, Optional

def id(x: Tuple[float, Optional[float]]) -> Union[Tuple[float, float], Tuple[float, None]]:
    return x

Expected Behavior

Values of type Tuple[float, Optional[float]] and Union[Tuple[float, float], Tuple[float, None]] should be mutually compatible.

That is: Union ought to distribute across Tuple.

Actual Behavior

main.py:4: error: Incompatible return value type (got "tuple[float, float | None]", expected "tuple[float, float] | tuple[float, None]")  [return-value]

Your Environment

(See Playground link.)

  • Python version used: 3.12
@bcmills bcmills added the bug mypy got something wrong label Apr 14, 2025
@VallinZ
Copy link

VallinZ commented Apr 14, 2025

Is this a bug that need a fix? If so, can I work on this?

@A5rocks
Copy link
Collaborator

A5rocks commented Apr 14, 2025

Yes this is a bug that needs a fix, but it probably touches some decently complicated subtyping rules so it's probably not the best first contribution :^)

Feel free to work on it though.

@A5rocks A5rocks added the false-positive mypy gave an error on correct code label Apr 14, 2025
@A5rocks
Copy link
Collaborator

A5rocks commented Apr 21, 2025

To follow up on this, this is actually in the typing spec! See https://typing.python.org/en/latest/spec/tuples.html#type-compatibility-rules

@VallinZ
Copy link

VallinZ commented Apr 21, 2025

Thank you for proving more information!

@VallinZ
Copy link

VallinZ commented Apr 24, 2025

Hi, I tried working on this bug and I think I successful catch the example mention in the bug report, but my implementation leads to a maximum recursive depth error for recursive data types. Can anyone take a look and see whether this implementation is on the correct idea or is totally off? I didn't want to PR to the mypy repo broken code, so I just provided a link to a PR in my fork repo with the current issue: VallinZ#4

@A5rocks
Copy link
Collaborator

A5rocks commented Apr 24, 2025

I think eagerly expanding tuples of unions as you do is probably a bad idea. I haven't thought too much about it (and my first instinct was IMO wrong), but I think you could add a special case next to the current exhaustive enum/bool one which:

  1. narrows the rhs union to just things with the same length as lhs
  2. for every item + position in lhs, check that type is a subtype of the union of items at that position in each rhs tuple.

(I haven't really played through this in my head so I can't say whether that's totally correct logic or not. Hopefully it at least gives an idea!)

However, when I asked a few days ago people recommended that maybe normalizing (like you did) is a good idea, so... well, I don't know the specific reason you're running into issues with recursive types either.

@A5rocks
Copy link
Collaborator

A5rocks commented Apr 24, 2025

Nevermind that approach is wrong. Sorry, I don't know if there's a better way to implement this than what you have.

(maybe avoiding creating all the tuples up front is possible? Left isn't in right and left is a tuple and right is a union, then you could split left on the first union element it has -- so that e.g. tuple[str, int | str, int | str] becomes tuple[str, int, int | str] | tuple[str, str, int | str], which you can then toss back into subtyping. I suspect there's something not good about being eager about things in face of recursive types.)

@VallinZ
Copy link

VallinZ commented Apr 24, 2025

Got it, thank you for providing feedback! I'll look into the approach you suggested.

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 topic-union-types
Projects
None yet
Development

No branches or pull requests

4 participants