python / python/mypy

`not isinstance(x, cls)` inside classmethod does not narrow type

Open
#21,271 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug topic-self-types topic-type-narrowing
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

Bug Report

Inside a @classmethod, not isinstance(x, cls) does not appear to narrow the type, while isinstance(x, cls) does.

To Reproduce

This is what I wanted to write:

class Foo:
    def __init__(self, x: float) -> None:
        ...
    
    @classmethod
    def from_foo_or_float_cls(cls, x: Self | float) -> Self:
        if isinstance(x, cls):
            return x
        return cls(x)  # error: Argument 1 to "Foo" has incompatible type "Self | float"; expected "float"  [arg-type]

Further reduced:

class Foo:
    @classmethod
    def foo_1(cls, x: Self | float) -> None:
        assert isinstance(x, cls)
        reveal_type(x)  # expected: "Self"; was: "Self`0"; ✅

    @classmethod
    def foo_2(cls, x: Self | float) -> None:
        assert not isinstance(x, cls)
        reveal_type(x)  # expected: "float"; was: "Self`0" | builtins.float"; ❌

Expected Behavior

not isinstance(x, cls) should narrow the type union to remove Self.

Actual Behavior

Self is still part of the type union.

Your Environment

  • Mypy version used: 1.19.1
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.10

Same behavior can be observers on mypy master with Python 3.14 (tested on the playground).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the two reduced classmethod examples with mypy 1.19.1 or current master, then inspect the isinstance type-narrowing path for Self. Add regression coverage showing that assert not isinstance(x, cls) narrows Self | float to float, while preserving the existing positive narrowing behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.