Skip to content

Detect impossible unpacking? #18783

New issue

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

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

Already on GitHub? Sign in 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in 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