python / python/mypy

Incorrect `Any` class method return type inference with Generic TypeVars in separate modules

Open
#18,411 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
20.6k
Forks
3.3k
PR merge metrics
PR metrics pending

Description

Bug Report

The setup:
I have a generic class (Instantiator) which contains a class method (instantiate()) which is passed a desired type (MyClass) and instantiates an instance of the generic type. I then have another function (returns_instance()) outside of the class which calls instantiate and returns an instance of the passed desired type.

What is happening:
When the definition for the generic type and the generic class lives in the same module as the calling function, there are no mypy errors. However, if the definition for the generic type and the generic class live in a different module from the calling function, mypy throws a error: Returning Any from function declared to return "MyClass" [no-any-return] error.

To Reproduce

File 1: a.py

from typing import Any, Dict, Generic, Type, TypeVar
from dataclasses import dataclass
from spotify_typed_config.b import InstantiatorB


TYPEVAR_A = TypeVar("TYPEVAR_A")


@dataclass
class MyClass:
    i: int = 10

    def print(self) -> None:
        print(self.i)


class InstantiatorA(Generic[TYPEVAR_A]):
    @classmethod
    def instantiate(cls, class_to_instantiate: Type[TYPEVAR_A], x: Dict[str, Any]) -> TYPEVAR_A:
        return class_to_instantiate(**x)


# This works fine -- no errors!
def returns_instance_a(i: int) -> MyClass:
    return InstantiatorA.instantiate(MyClass, {"i": i})

# a.py:29: error: Returning Any from function declared to return "MyClass"  [no-any-return]
def returns_instance_b(i: int) -> MyClass:
    return InstantiatorB.instantiate(MyClass, {"i": i})

# b.py:14: error: Redundant cast to "TYPEVAR_B"  [redundant-cast]
# a.py:33: error: Returning Any from function declared to return "MyClass"  [no-any-return]
def returns_instance_b_with_cast(i: int) -> MyClass:
    return InstantiatorB.instantiate_with_cast(MyClass, {"i": i})

# a.py:37: error: Returning Any from function declared to return "MyClass"  [no-any-return]
def returns_instance_b_no_params(i: int) -> MyClass:
    return InstantiatorB.instantiate_no_params(MyClass)

# This works fine -- no errors!
def returns_instance_b_reassignment(i: int) -> MyClass:
    b: MyClass = InstantiatorB.instantiate_no_params(MyClass)
    return b

# a.py:47: error: Returning Any from function declared to return "MyClass"  [no-any-return]
def returns_instance_b_inner_reassign(i: int) -> MyClass:
    return InstantiatorB.instantiate_with_reassign(MyClass)

# a.py:51: error: Returning Any from function declared to return "MyClass"  [no-any-return]
def returns_instance_b_with_generic(i: int) -> MyClass:
    return InstantiatorB[MyClass].instantiate(MyClass, {"i": i})

File 2: b.py

from typing import Any, Dict, Generic, Type, TypeVar, cast

TYPEVAR_B = TypeVar("TYPEVAR_B")

class InstantiatorB(Generic[TYPEVAR_B]):
    @classmethod
    def instantiate(cls, class_to_instantiate: Type[TYPEVAR_B], x: Dict[str, Any]) -> TYPEVAR_B:
        return class_to_instantiate(**x)

    @classmethod
    def instantiate_with_cast(cls, class_to_instantiate: Type[TYPEVAR_B], x: Dict[str, Any]) -> TYPEVAR_B:
        return cast(TYPEVAR_B, class_to_instantiate(**x))

    @classmethod
    def instantiate_no_params(cls, class_to_instantiate: Type[TYPEVAR_B]) -> TYPEVAR_B:
        return class_to_instantiate()

    @classmethod
    def instantiate_with_reassign(cls, class_to_instantiate: Type[TYPEVAR_B]) -> TYPEVAR_B:
        result: TYPEVAR_B = class_to_instantiate()
        return result

Expected Behavior

I expect that mypy would be able to correctly infer that the return type of the instantiate_* methods as it does for InstantiatorA, but for InstantiatorB it incorrectly reports Any as the return type.
I would expect that maybe passing the desired return type as a subscript was needed such as in returns_instance_b_with_generic, and that actually does result in VSCode showing the correct return type, but mypy still does not.

Actual Behavior

Errors are reported inline above.

Your Environment

  • Mypy version used: 1.14.1
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files):
# Mypy configuration equal to mypy --strict. Read more about the tools at
# https://backstage.spotify.net/docs/python/
[tool.mypy]
ignore_missing_imports = true
warn_unused_configs = true
disallow_any_generics = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
no_implicit_reexport = true
strict_equality = true

- Python version used: 3.8.12

<!-- You can freely edit this text, please remove all the lines you believe are unnecessary. -->

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 reproducer in a.py and b.py using mypy 1.14.1 with the shown strict configuration, comparing same-module and imported-module behavior. Trace how the instantiate_* class methods' Generic TypeVars are inferred across the module boundary; done means the reproduced calls infer MyClass and no longer report Returning Any or the related redundant-cast error.

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
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.