pythonpython-typingpyright

Why can't Pylance fully infer the return type of this function?


I am using Python 3.11.4, and Pydantic 2.10.0. The following is a toy example of a real-world problem.

I have defined two Pydantic Basemodels, and created a list containing instances of both, as follows.

import pydantic


class Foo(pydantic.BaseModel):
    a: int
    b: int


class Bar(pydantic.BaseModel):
    c: int
    d: int

non_homogeneous_container = [
    Foo(a=1, b=5),
    Foo(a=7, b=8),
    Bar(c=5, d=3),
    Bar(c=15, d=12),
]

I want to write a function that takes in such a list, as well as a target_type, and returns a list containing only those objects from the original list that conform to the specified target_type. I have written such a function, as below, and provided (what I believe to be) the appropriate type annotation for my type checker (Pylance, in VSCode).

from typing import Any, Type, TypeVar

T = TypeVar("T", bound=pydantic.BaseModel)


def extract_objects_of_specified_type(
    mixed_up_list: list[Any],
    target_type: Type[T],
) -> list[T]:
    extracted_list: list[T] = []
    for each_object in mixed_up_list:
        if isinstance(each_object, target_type):
            extracted_list.append(each_object)
    return extracted_list

Invoking my function with my mixed up list of objects, I obtain the expected list containing only objects of the target_type.

list_of_only_foos = extract_objects_of_specified_type(
    mixed_up_list=non_homogeneous_container, target_type=Foo
)

print (list_of_only_foos)
# Result: [Foo(a=1, b=5), Foo(a=7, b=8)]

However, I have a yellow underline on the line where I invoke my function; Pylance says the argument type is partially unknown. The bit that's underlined and the associated Pylance report are as below:

# list_of_only_foos: list[Foo] = extract_objects_of_specified_type(
#     mixed_up_list=non_homogeneous_container, target_type=Foo
# )                 ^^^^^^^^^^^^^^^^^^^^^^^^^
# Linter report: 
# Argument type is partially unknown
#   Argument corresponds to parameter "mixed_up_list" in function "extract_objects_of_specified_type"
#   Argument type is "list[Unknown]"PylancereportUnknownArgumentType

The linter report goes away when I annotate my input list of objects as below:

non_homogeneous_container: list[Foo | Bar] = [
    Foo(a=1, b=5),
    Foo(a=7, b=8),
    Bar(c=5, d=3),
    Bar(c=15, d=12),
]

or

non_homogeneous_container: list[pydantic.BaseModel] = [
    Foo(a=1, b=5),
    Foo(a=7, b=8),
    Bar(c=5, d=3),
    Bar(c=15, d=12),
]

I find this unsatisfactory because it obliges me to know in advance what objects are expected to be in the list I feed to my function.

I have tried to get around this issue by wrapping my function in a class that employs a Generic (below), but this made no difference.

class ExtractSpecificType(Generic[T]):
    @staticmethod
    def extract_objects_of_specified_type(
        mixed_up_list: list[Any],
        target_type: Type[T],
    ) -> list[T]:
        extracted_list: list[T] = []
        for each_object in mixed_up_list:
            if isinstance(each_object, target_type):
                extracted_list.append(each_object)
        return extracted_list

My function as originally constructed does what I want it to do - my question is about WHY the type checker is unhappy. Is my type annotation imprecise in some way? If so, what should I do differently?

Thank you.

EDIT

user2357112's comment and InSync's answer indicate that that this behaviour has something to do with the prevailing type-checker settings about when and where to warn about missing/ambiguous type annotations. For context, I have reproduced my VSCode type-checking settings below.

  "python.analysis.typeCheckingMode": "standard",
  "python.analysis.diagnosticSeverityOverrides": {
    "reportGeneralTypeIssues": true,
    "reportUnknownArgumentType": "warning",
    "reportUnknownParameterType": "warning",
    "reportMissingTypeArgument ": "warning",
    "reportMissingParameterType": "warning",
    "reportReturnType": "error",
    "reportUnusedImport": "warning",
    "reportUnnecessaryTypeIgnoreComment": "error"
  },

Solution

  • You have reportUnknownArgumentType enabled, but not strictListInference, which controls how Pyright/Pylance infers list types.

    When inferring the type of a list, use strict type assumptions. For example, the expression [1, 'a', 3.4] could be inferred to be of type list[Any] or list[int | str | float]. If this setting is true, it will use the latter (stricter) type.

    A small example demonstrating how it works:

    class A: ...
    class B: ...
    
    l = [A(), A(), B(), B()]
    

    (playground)

    # strictListInference = false (default)
    reveal_type(l)  # list[Unknown]
    

    (playground)

    # strictListInference = true
    reveal_type(l)  # list[A | B]
    

    This setting can be specified using either pyrightconfig.json or pyproject.toml:

    // pyrightconfig.json
    {
      "strictListInference": true,
    }
    
    # pyproject.toml
    [tool.pyright]
    strictListInference = true