Skip to content

Detect impossible unpacking? #18783

Open
@ego-thales

Description

@ego-thales

Hi,

Consider the following code, which passes with no error.

from foo import bar  # type: ignore[import-not-found]

def baz(x: int):
    return bar(*x, **x)

I think it would be neat to have mypy raising error on this, since int cannot be unpacked in any way.

Is it something to consider or am I missing something?

Thanks in advance.
Élie

Activity

sterliakov

sterliakov commented on Mar 11, 2025

@sterliakov
Collaborator

This is clearly a bug, unpacking should be checked consistently. It already happens if the callable is "good", but plain Any is not "good" enough. The same problem arises when the callable used is not a callable at all (e.g. a plain int). playground

from typing import Any

def fn1(*args: Any, **kwargs: Any) -> None: ...
fn2: Any
fn3: int

def baz(x: int) -> None:
    fn1(*x, **x)  # E: Expected iterable as variadic argument  [misc] \
                  # E: Argument after ** must be a mapping, not "int"  [arg-type]
    fn2(*x, **x)
    # Note there's an error, but an unrelated one
    fn3(*x, **x)  # E: "int" not callable  [operator]
added
bugmypy got something wrong
topic-callsFunction calls, *args, **kwargs, defaults
and removed on Mar 11, 2025
hauntsaninja

hauntsaninja commented on Mar 12, 2025

@hauntsaninja
Collaborator

See related https://github.com/python/mypy/pull/18207/files and check_any_type_call if someone is interested in putting up a PR

ego-thales

ego-thales commented on Mar 13, 2025

@ego-thales
Author

Hey,

I'm really not fluent in mypy source code and it's a bit hard to comprehend at first glance what is in charge of what.

Is it a good starting point to look around here?

mypy/mypy/checkexpr.py

Lines 2484 to 2492 in e37d92d

for arg_type, arg_kind in zip(arg_types, arg_kinds):
arg_type = get_proper_type(arg_type)
if arg_kind == nodes.ARG_STAR and not self.is_valid_var_arg(arg_type):
self.msg.invalid_var_arg(arg_type, context)
if arg_kind == nodes.ARG_STAR2 and not self.is_valid_keyword_var_arg(arg_type):
is_mapping = is_subtype(
arg_type, self.chk.named_type("_typeshed.SupportsKeysAndGetItem")
)
self.msg.invalid_keyword_var_arg(arg_type, is_mapping, context)

I'm unsure because it seems that this would confront an actual call to a desired signature, whereas we only need to analyze the call here.

Also I could not really understand quickly how check_any_type_call should be looked at.

sobolevn

sobolevn commented on Mar 15, 2025

@sobolevn
Member

@ego-thales hi! Thanks for your interest. Here's a little prototype to help you working on this feature:

diff --git mypy/checkexpr.py mypy/checkexpr.py
index 1017009ce..eac3a759d 100644
--- mypy/checkexpr.py
+++ mypy/checkexpr.py
@@ -1583,7 +1583,7 @@ class ExpressionChecker(ExpressionVisitor[Type]):
                 callee, args, arg_kinds, arg_names, callable_name, object_type, context
             )
         elif isinstance(callee, AnyType) or not self.chk.in_checked_function():
-            return self.check_any_type_call(args, callee)
+            return self.check_any_type_call(args, callee, arg_kinds, context)
         elif isinstance(callee, UnionType):
             return self.check_union_call(callee, args, arg_kinds, arg_names, context)
         elif isinstance(callee, Instance):
@@ -2481,15 +2481,7 @@ class ExpressionChecker(ExpressionVisitor[Type]):
         # Keep track of consumed tuple *arg items.
         mapper = ArgTypeExpander(self.argument_infer_context())
 
-        for arg_type, arg_kind in zip(arg_types, arg_kinds):
-            arg_type = get_proper_type(arg_type)
-            if arg_kind == nodes.ARG_STAR and not self.is_valid_var_arg(arg_type):
-                self.msg.invalid_var_arg(arg_type, context)
-            if arg_kind == nodes.ARG_STAR2 and not self.is_valid_keyword_var_arg(arg_type):
-                is_mapping = is_subtype(
-                    arg_type, self.chk.named_type("_typeshed.SupportsKeysAndGetItem")
-                )
-                self.msg.invalid_keyword_var_arg(arg_type, is_mapping, context)
+        self.check_args_unpacking(arg_types, arg_kinds, context)
 
         for i, actuals in enumerate(formal_to_actual):
             orig_callee_arg_type = get_proper_type(callee.arg_types[i])
@@ -3292,8 +3284,17 @@ class ExpressionChecker(ExpressionVisitor[Type]):
             skip_unsatisfied=skip_unsatisfied,
         )
 
-    def check_any_type_call(self, args: list[Expression], callee: Type) -> tuple[Type, Type]:
-        self.infer_arg_types_in_empty_context(args)
+    def check_any_type_call(
+        self,
+        args: list[Expression],
+        callee: Type,
+        arg_kinds: list[ArgKind],
+        context: Context,
+    ) -> tuple[Type, Type]:
+        arg_types = self.infer_arg_types_in_empty_context(args)
+
+        self.check_args_unpacking(arg_types, arg_kinds, context)
+
         callee = get_proper_type(callee)
         if isinstance(callee, AnyType):
             return (
@@ -3303,6 +3304,17 @@ class ExpressionChecker(ExpressionVisitor[Type]):
         else:
             return AnyType(TypeOfAny.special_form), AnyType(TypeOfAny.special_form)
 
+    def check_args_unpacking(self, arg_types: list[Type], arg_kinds: list[ArgKind], context: Context) -> None:
+        for arg_type, arg_kind in zip(arg_types, arg_kinds):
+            arg_type = get_proper_type(arg_type)
+            if arg_kind == nodes.ARG_STAR and not self.is_valid_var_arg(arg_type):
+                self.msg.invalid_var_arg(arg_type, context)
+            if arg_kind == nodes.ARG_STAR2 and not self.is_valid_keyword_var_arg(arg_type):
+                is_mapping = is_subtype(
+                    arg_type, self.chk.named_type("_typeshed.SupportsKeysAndGetItem")
+                )
+                self.msg.invalid_keyword_var_arg(arg_type, is_mapping, context)
+
     def check_union_call(
         self,
         callee: UnionType,

I didn't test this, but it should probably work :)

Jdwashin9

Jdwashin9 commented on Apr 27, 2025

@Jdwashin9

I'd like to work on this, too.

sobolevn

sobolevn commented on Apr 27, 2025

@sobolevn
Member

@Jdwashin9 go ahead! My diff above can be a nice starting point.

ego-thales

ego-thales commented on Apr 28, 2025

@ego-thales
Author

Oh thank you so much, because despite the really helpful contribution from @sobolevn, I could not find enough time to go further with this, requiring time to understand the core machanisms of the project. I had not forgotten, it still sat in my todo list, but it was not realistically going to happen before summer from my side.

Thanks in advance and good luck @Jdwashin9!

linked a pull request that will close this issue on Apr 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wronggood-second-issuetopic-callsFunction calls, *args, **kwargs, defaults

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @sobolevn@hauntsaninja@sterliakov@Jdwashin9@ego-thales

      Issue actions

        Detect impossible unpacking? · Issue #18783 · python/mypy