Skip to content

structurally equivalent namedtuples can confuse mypy in unions in some cases #18520

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

Closed
asottile-sentry opened this issue Jan 23, 2025 · 5 comments · Fixed by #18564
Closed

structurally equivalent namedtuples can confuse mypy in unions in some cases #18520

asottile-sentry opened this issue Jan 23, 2025 · 5 comments · Fixed by #18564
Labels
bug mypy got something wrong topic-named-tuple

Comments

@asottile-sentry
Copy link

Bug Report

some narrowing goes wrong and mypy ends up getting confused with a bunch of self-union types (A | A) and spurious errors

I'm not entirely sure the cause however

To Reproduce

from typing import NamedTuple


class AKey(NamedTuple):
    k: str

class A(NamedTuple):
    key: AKey
    s: str


class BKey(NamedTuple):
    k: str

class B(NamedTuple):
    key: BKey
    s: str


def f(x: A | B | str) -> A | B | str:
    if isinstance(x, str):
        return x
    else:
        return x._replace(s='wat')

Expected Behavior

no errors from the given snippet

Actual Behavior

$ mypy t3.py
t3.py:24: error: Invalid self argument "A | A" to attribute function "_replace" with type "Callable[[_NT, DefaultNamedArg(AKey, 'key'), DefaultNamedArg(str, 's')], _NT]"  [misc]
t3.py:24: error: Invalid self argument "B | B" to attribute function "_replace" with type "Callable[[_NT, DefaultNamedArg(BKey, 'key'), DefaultNamedArg(str, 's')], _NT]"  [misc]
t3.py:24: error: Incompatible return value type (got "A | B | A | B", expected "A | B | str")  [return-value]
Found 3 errors in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: 1.14.0
  • Mypy command-line flags: n/a
  • Mypy configuration options from mypy.ini (and other config files): n/a
  • Python version used: 3.13.1
@asottile-sentry asottile-sentry added the bug mypy got something wrong label Jan 23, 2025
@asottile-sentry
Copy link
Author

breaking the structural equivalence "fixes" the type errors -- but can break code

 class B(NamedTuple):
     key: BKey
     s: str
+    b_hax: bool = True

@asottile-sentry
Copy link
Author

#15600 seems related -- but I don't understand type algebra enough to know if this is the same as what's explained here: #15600 (comment)

@asottile-sentry
Copy link
Author

breaking the structural equivalence "fixes" the type errors -- but can break code

class B(NamedTuple):
key: BKey
s: str

  • b_hax: bool = True

a more clever workaround which preserves the runtime behaviour is to duplicate the definition in/out of a TYPE_CHECKING block:

if TYPE_CHECKING:
    class B(NamedTuple):
        key: BKey
        s: str
        b_hax: bool = True
else:  # real implementation here
    class B(NamedTuple):
        key: BKey
        s: str

@sterliakov
Copy link
Collaborator

We create an "impossible" meet of tuples with different fallbacks here (TODO is apparently referring to this issue):

mypy/mypy/meet.py

Lines 987 to 992 in 4a76a1a

if isinstance(self.s, TupleType):
items = self.meet_tuples(self.s, t)
if items is None:
return self.default(self.s)
# TODO: What if the fallbacks are different?
return TupleType(items, tuple_fallback(t))

@sterliakov
Copy link
Collaborator

sterliakov commented Jan 23, 2025

The monster produced from meeting A and B looks like

Union[
    tuple[tuple[builtins.str, fallback=__main__.AKey], builtins.str, fallback=__main__.A],
    tuple[tuple[builtins.str, fallback=__main__.AKey], builtins.str, fallback=__main__.B],
    tuple[tuple[builtins.str, fallback=__main__.BKey], builtins.str, fallback=__main__.A],
    tuple[tuple[builtins.str, fallback=__main__.BKey], builtins.str, fallback=__main__.B]
]

which looks suspicious: the cross-bred types cannot exist.

A5rocks added a commit to A5rocks/mypy that referenced this issue Jan 29, 2025
@JukkaL JukkaL closed this as completed in e2b821b Jan 30, 2025
x612skm pushed a commit to x612skm/mypy-dev that referenced this issue Feb 24, 2025
…ython#18564)

Fixes python#18562.

Fixes python#6623.

Fixes python#18520. (Only incidentally
and I'm not exactly sure why.)

I was investigating what input mypy thought satisfied both overloads and
I found that mypy is missing a check on the fallback type.
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-named-tuple
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants