Skip to content

Detect impossible unpacking? #18783

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
ego-thales opened this issue Mar 11, 2025 · 7 comments · May be fixed by #18990
Open

Detect impossible unpacking? #18783

ego-thales opened this issue Mar 11, 2025 · 7 comments · May be fixed by #18990
Labels
bug mypy got something wrong good-second-issue topic-calls Function calls, *args, **kwargs, defaults

Comments

@ego-thales
Copy link

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

@sterliakov
Copy link
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]

@hauntsaninja
Copy link
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
Copy link
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
Copy link
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
Copy link

I'd like to work on this, too.

@sobolevn
Copy link
Member

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

@ego-thales
Copy link
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!

@Jdwashin9 Jdwashin9 linked a pull request Apr 28, 2025 that will close this issue
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
bug mypy got something wrong good-second-issue topic-calls Function calls, *args, **kwargs, defaults
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants