python / python/mypy

false positive: classmethod constructor on generic class

Open
#9,201 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

needs discussion topic-type-variables
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

I'm inclined to think this is a bug, but could be argued it's a missing feature.

How to reproduce

# makes_bug.py

from typing import Generic, Type, TypeVar

T = TypeVar('T')

class Foo(Generic[T]):
    def __init__(self, value: T) -> None: ...
    @classmethod
    def construct(cls) -> Foo[str]:
        return cls('bar')

Expected outcome

This code should pass type checks.

Actual outcome

mypy, to my great surprise, fails:

$ python --version
Python 3.8.1
$ mypy --version
mypy 0.790+dev.65186ae1e23fd44f1d7e6aa2c4458bdf55640742
$ mypy src/makes_bug.py
makes_bug.py:11: error: Incompatible return value type (got "Foo[T]", expected "Foo[str]")
makes_bug.py:11: error: Argument "value" to "Foo" has incompatible type "str"; expected "T"
Found 2 errors in 1 file (checked 1 source file)

Workaround

Changing line 10 to def construct(cls: Type[Foo]) -> Foo[str]: fixes it; or, probably more or less equivalently, def construct(cls: Type[Foo[Any]]) -> Foo[str]:.

Investigation / suggested fix

Adding reveal_type(cls) indicates that cls is inferred to have type Foo[T], which cannot return a Foo[str].

It's not clear to me why classmethods should have the lead argument inferred to a bound type. I'd propose that my workaround be made a default: class methods should infer the type of the lead argument type as the unbound class type, or bound with all Anys.

I'd imagine the change would go here: https://github.com/python/mypy/blob/65186ae1e23fd44f1d7e6aa2c4458bdf55640742/mypy/semanal.py#L609,L610
or (more likely) in the referenced method class_type():
https://github.com/python/mypy/blob/65186ae1e23fd44f1d7e6aa2c4458bdf55640742/mypy/semanal.py#L4790,L4791

Happy to take a stab at it, but wanted to solicit some feedback first. This is a complex system and it's possible I'm missing some potential consequence here.

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

Start with the generic classmethod reproduction in makes_bug.py and inspect mypy/semanal.py at the referenced lines, especially class_type(). Use reveal_type(cls) and run mypy on the example to confirm the inferred type. Done means the example passes type checking without the Type[Foo] workaround, with the existing generic behavior preserved.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.