Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mypy.checkmember import analyze_member_access, has_operator
from mypy.checkstrformat import StringFormatterChecker
from mypy.constant_fold import constant_fold_expr
from mypy.constraints import SUBTYPE_OF
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error
from mypy.expandtype import (
Expand All @@ -30,7 +31,7 @@
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.infer import ArgumentInferContext, infer_function_type_arguments, infer_type_arguments
from mypy.literals import literal
from mypy.maptype import map_instance_to_supertype
from mypy.maptype import as_type, map_instance_to_supertype
from mypy.meet import is_overlapping_types, narrow_declared_type
from mypy.message_registry import ErrorMessage
from mypy.messages import MessageBuilder, format_type
Expand Down Expand Up @@ -6143,30 +6144,28 @@ def is_valid_var_arg(self, typ: Type) -> bool:
def is_valid_keyword_var_arg(self, typ: Type) -> bool:
"""Is a type valid as a **kwargs argument?"""
typ = get_proper_type(typ)
return (
(
# This is a little ad hoc, ideally we would have a map_instance_to_supertype
# that worked for protocols
isinstance(typ, Instance)
and typ.type.fullname == "builtins.dict"
and is_subtype(typ.args[0], self.named_type("builtins.str"))
)
or isinstance(typ, ParamSpecType)
or is_subtype(
typ,
self.chk.named_generic_type(
"_typeshed.SupportsKeysAndGetItem",
[self.named_type("builtins.str"), AnyType(TypeOfAny.special_form)],
),
)
or is_subtype(
typ,
self.chk.named_generic_type(
"_typeshed.SupportsKeysAndGetItem", [UninhabitedType(), UninhabitedType()]
),
)
if isinstance(typ, ParamSpecType | AnyType):
return True

# Check if 'typ' is a SupportsKeysAndGetItem[T, Any] for some T <: str
# Note: is_subtype(typ, SupportsKeysAndGetItem[str, Any])` is too harsh
# since SupportsKeysAndGetItem is invariant in the key type parameter.

# create a TypeVar and template type
T = TypeVarType(
"T",
"T",
id=TypeVarId(-1, namespace="<kwargs>"),
values=[],
upper_bound=self.named_type("builtins.str"),
default=AnyType(TypeOfAny.from_omitted_generics),
)
template = self.chk.named_generic_type(
"_typeshed.SupportsKeysAndGetItem", [T, AnyType(TypeOfAny.special_form)]
)

return as_type(typ, SUBTYPE_OF, template) is not None

def not_ready_callback(self, name: str, context: Context) -> None:
"""Called when we can't infer the type of a variable because it's not ready yet.

Expand Down
57 changes: 55 additions & 2 deletions mypy/maptype.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,61 @@
from __future__ import annotations

from mypy.expandtype import expand_type_by_instance
from typing import cast

from mypy.expandtype import expand_type, expand_type_by_instance
from mypy.nodes import TypeInfo
from mypy.types import AnyType, Instance, TupleType, TypeOfAny, has_type_vars
from mypy.types import AnyType, Instance, TupleType, Type, TypeOfAny, has_type_vars


def as_type(typ: Type, direction: int, target: Type) -> Type | None:
"""Attempts to solve type variables in `target` so that `typ` is a subtype/supertype of
the resulting type.
Args:
typ: The type to map from.
direction: One of SUBTYPE_OF or SUPERTYPE_OF
target: The target instance type to map to.
Returns:
None: if the mapping is not possible.
Type: the mapped type if the mapping is possible.
Examples:
(list[int], Iterable[T]) -> Iterable[int]
(list[list[int]], Iterable[list[T]]) -> Iterable[list[int]]
(dict[str, int], Mapping[K, int]) -> Mapping[str, int]
(list[int], Mapping[K, V]) -> None
"""
from mypy.subtypes import is_subtype
from mypy.typeops import get_all_type_vars
Comment on lines +29 to +30
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to put these there due to circular import issues

Copy link
Member

@ilevkivskyi ilevkivskyi Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function should definitely not be in this module (ideally we shouldn't even have the existing typeops import below, but that is a separate story).

Also the scope of this function is misleadingly broad. It should probably accept a TypeInfo as target (which probably must be a protocol), and then use fill_typevars(...) as a constraint inference target.

Finally, it is worth doing some performance measurements, we don't want any visible slow-down for something that is only needed for rare edge cases.


# 1. get type vars of target
tvars = get_all_type_vars(target)

# fast path: if no type vars, just check subtype
if not tvars:
return target if is_subtype(typ, target) else None

from mypy.constraints import SUBTYPE_OF, Constraint, infer_constraints
from mypy.solve import solve_constraints

# 2. determine constraints
constraints: list[Constraint] = infer_constraints(target, typ, not direction)
for tvar in tvars:
# need to manually include these because solve_constraints ignores them
# apparently
constraints.append(Constraint(tvar, SUBTYPE_OF, tvar.upper_bound))
Comment on lines +45 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that solve_constraints ignores the upper bounds of the tvars it solves for?


# 3. solve constraints
solution, _ = solve_constraints(tvars, constraints)

if None in solution:
return None

# 4. build resulting Type by substituting type vars with solution
env = {tvar.id: s for tvar, s in zip(tvars, cast("list[Type]", solution))}
target = expand_type(target, env)
return target if is_subtype(typ, target) else None


def map_instance_to_supertype(instance: Instance, superclass: TypeInfo) -> Instance:
Expand Down
32 changes: 25 additions & 7 deletions test-data/unit/check-kwargs.test
Original file line number Diff line number Diff line change
Expand Up @@ -572,11 +572,29 @@ main:41: error: Argument 1 to "foo" has incompatible type "**dict[str, str]"; ex
[builtins fixtures/dict.pyi]

[case testLiteralKwargs]
from typing import Any, Literal
kw: dict[Literal["a", "b"], Any]
def func(a, b): ...
func(**kw)

badkw: dict[Literal["one", 1], Any]
func(**badkw) # E: Argument after ** must have string keys
from typing import Literal, Mapping, Iterable
def func(a: int, b: int) -> None: ...

class GOOD_KW:
def keys(self) -> Iterable[Literal["a", "b"]]: ...
def __getitem__(self, key: str) -> int: ...

class BAD_KW:
def keys(self) -> Iterable[Literal["one", 1]]: ...
def __getitem__(self, key: str) -> int: ...

def test(
good_kw: GOOD_KW,
bad_kw: BAD_KW,
good_dict: dict[Literal["a", "b"], int],
bad_dict: dict[Literal["one", 1], int],
good_mapping: Mapping[Literal["a", "b"], int],
bad_mapping: Mapping[Literal["one", 1], int],
) -> None:
func(**good_kw)
func(**bad_kw) # E: Argument after ** must have string keys
func(**good_dict)
func(**bad_dict) # E: Argument after ** must have string keys
func(**good_mapping)
func(**bad_mapping) # E: Argument after ** must have string keys
[builtins fixtures/dict.pyi]