Skip to content

Inferred attribute type feeds back as call context, rejecting its own defining assignment #21809

Description

@Kache

Bug Report

Assigning a generic call returning K | None to a previously undeclared
self. attribute spuriously fails [arg-type] on that same assignment. The
attribute's inferred type (str | None) apparently comes back as call context
on a later pass, solving K = str | None; the invariant Mapping key then
rejects dict[str, str].

The same expression passes with no context, an annotated local, an annotated
attribute, or a reassigned local (whose second assignment is likewise checked
against its own inferred str | None) — the optional-stripping special case
seems skipped on the inferred-attribute path alone. A sync version (no
async/await) also passes, pointing at the deferred re-check await
triggers.

To Reproduce

from collections.abc import Mapping


async def select_one[K](options: Mapping[K, str]) -> K | None: ...


class ViaInferredAttr:
    async def m(self) -> None:
        cases = {"a": "x"}
        self._done = await select_one(cases)  # error (spurious)


async def via_local() -> None:
    cases = {"a": "x"}
    done = await select_one(cases)  # ok
    done = await select_one(cases)  # ok (context = inferred str | None)
    done2: str | None = await select_one(cases)  # ok


class ViaAnnotatedAttr:
    _done: str | None

    async def m(self) -> None:
        cases = {"a": "x"}
        self._done = await select_one(cases)  # ok

Expected Behavior

All call sites typecheck, with K = str.

Actual Behavior

repro.py:10: error: Argument 1 to "select_one" has incompatible type "dict[str, str]"; expected "Mapping[str | None, str]"  [arg-type]

The str | None key shows the return's | None leaked into K.

Your Environment

  • mypy 1.20.2 (compiled), no flags
  • Python 3.13

Related: #5874 (over-eager outer context with unions), #19304 (assignment target
changes results). New here: the context is self-inferred — no annotation exists
anywhere — and explicit spellings of the same type behave differently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrong

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions