From 139b2a9e3df11b07ffb7ed54c9d222d3889adad4 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 16 Jan 2026 02:53:04 -0500 Subject: [PATCH 1/8] Detect missing positional args and suggest argument in error message --- mypy/checkexpr.py | 196 +++++++++++++++++++++++++--- test-data/unit/check-functions.test | 48 +++++++ 2 files changed, 226 insertions(+), 18 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9990caaeb7a17..2ff78ae2525d8 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1798,19 +1798,9 @@ def check_callable_call( arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual) - self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) - - self.check_argument_types( - arg_types, arg_kinds, args, callee, formal_to_actual, context, object_type=object_type + self.check_call_arguments( + callee, arg_types, arg_kinds, arg_names, args, + formal_to_actual, context, callable_name, object_type, ) if ( @@ -2340,6 +2330,171 @@ def apply_inferred_arguments( # arguments. return self.apply_generic_arguments(callee_type, inferred_args, context) + def check_call_arguments( + self, + callee: CallableType, + arg_types: list[Type], + arg_kinds: list[ArgKind], + arg_names: Sequence[str | None] | None, + args: list[Expression], + formal_to_actual: list[list[int]], + context: Context, + callable_name: str | None, + object_type: Type | None, + ) -> None: + """Check argument count and types, consolidating errors for missing positional args.""" + with self.msg.filter_errors(): + _, missing_positional = self.check_argument_count( + callee, arg_types, arg_kinds, arg_names, formal_to_actual, + context, object_type, callable_name, + ) + + if missing_positional: + func_name = callable_name or callee.name or "function" + if "." in func_name: + func_name = func_name.split(".")[-1] + + shift_info = None + num_positional_args = sum(1 for k in arg_kinds if k == nodes.ARG_POS) + if num_positional_args >= 2: + shift_info = self.detect_shifted_positional_args( + callee, arg_types, arg_kinds, missing_positional + ) + + with self.msg.filter_errors() as type_error_watcher: + self.check_argument_types( + arg_types, arg_kinds, args, callee, formal_to_actual, context, + object_type=object_type + ) + has_type_errors = type_error_watcher.has_new_errors() + + if shift_info is not None: + _, param_name, expected_type, high_confidence = shift_info + if high_confidence and param_name: + type_str = format_type(expected_type, self.chk.options) + self.msg.fail( + f'Expected {type_str} for parameter "{param_name}"; ' + f'did you forget argument "{param_name}"?', + context, + code=codes.CALL_ARG, + ) + else: + self.msg.fail( + f'Incompatible arguments for "{func_name}"; check for missing arguments', + context, + code=codes.CALL_ARG, + ) + elif has_type_errors: + self.msg.fail( + f'Incompatible arguments for "{func_name}"; check for missing arguments', + context, + code=codes.CALL_ARG, + ) + else: + self.check_argument_count( + callee, arg_types, arg_kinds, arg_names, formal_to_actual, + context, object_type, callable_name, + ) + else: + self.check_argument_count( + callee, arg_types, arg_kinds, arg_names, formal_to_actual, + context, object_type, callable_name, + ) + self.check_argument_types( + arg_types, arg_kinds, args, callee, formal_to_actual, context, + object_type=object_type + ) + + def detect_shifted_positional_args( + self, + callee: CallableType, + actual_types: list[Type], + actual_kinds: list[ArgKind], + missing_positional: list[int], + ) -> tuple[int, str | None, Type, bool] | None: + """Detect if positional arguments are shifted due to a missing argument. + + Returns (1-indexed position, param name, expected type, high_confidence) if a + shift pattern is found, None otherwise. High confidence is set when the function + has fixed parameters (no defaults, *args, or **kwargs). + """ + if not missing_positional: + return None + + has_star_args = any(k == nodes.ARG_STAR for k in callee.arg_kinds) + has_star_kwargs = any(k == nodes.ARG_STAR2 for k in callee.arg_kinds) + has_defaults = any(k == nodes.ARG_OPT for k in callee.arg_kinds) + single_missing = len(missing_positional) == 1 + high_confidence = ( + single_missing and not has_star_args and not has_star_kwargs and not has_defaults + ) + + positional_actual_types = [ + actual_types[i] for i, k in enumerate(actual_kinds) if k == nodes.ARG_POS + ] + if len(positional_actual_types) < 2: + return None + + positional_formal_types: list[Type] = [] + positional_formal_names: list[str | None] = [] + for i, kind in enumerate(callee.arg_kinds): + if kind.is_positional(): + positional_formal_types.append(callee.arg_types[i]) + positional_formal_names.append(callee.arg_names[i]) + + # Find first position where arg doesn't match but would match next position + shift_position = None + for i, actual_type in enumerate(positional_actual_types): + if i >= len(positional_formal_types): + break + if is_subtype(actual_type, positional_formal_types[i], options=self.chk.options): + continue + next_idx = i + 1 + if next_idx >= len(positional_formal_types): + break + if is_subtype(actual_type, positional_formal_types[next_idx], options=self.chk.options): + shift_position = i + break + else: + break + + if shift_position is None: + return None + + # Validate that all args would match if we inserted one at shift_position + if not self._validate_shift_insertion( + positional_actual_types, positional_formal_types, shift_position + ): + return None + + return ( + shift_position + 1, + positional_formal_names[shift_position], + positional_formal_types[shift_position], + high_confidence, + ) + + def _validate_shift_insertion( + self, + actual_types: list[Type], + formal_types: list[Type], + insert_position: int, + ) -> bool: + """Check if inserting an argument at insert_position would fix type errors.""" + for i, actual_type in enumerate(actual_types): + if i < insert_position: + if i >= len(formal_types): + return False + expected = formal_types[i] + else: + shifted_idx = i + 1 + if shifted_idx >= len(formal_types): + return False + expected = formal_types[shifted_idx] + if not is_subtype(actual_type, expected, options=self.chk.options): + return False + return True + def check_argument_count( self, callee: CallableType, @@ -2350,13 +2505,15 @@ def check_argument_count( context: Context | None, object_type: Type | None = None, callable_name: str | None = None, - ) -> bool: + ) -> tuple[bool, list[int]]: """Check that there is a value for all required arguments to a function. Also check that there are no duplicate values for arguments. Report found errors using 'messages' if it's not None. If 'messages' is given, 'context' must also be given. - Return False if there were any errors. Otherwise return True + Return a tuple of: + - False if there were any errors, True otherwise + - List of formal argument indices that are missing positional arguments """ if context is None: # Avoid "is None" checks @@ -2374,12 +2531,15 @@ def check_argument_count( callee, actual_types, actual_kinds, actual_names, all_actuals, context ) + missing_positional: list[int] = [] + # Check for too many or few values for formals. for i, kind in enumerate(callee.arg_kinds): mapped_args = formal_to_actual[i] if kind.is_required() and not mapped_args and not is_unexpected_arg_error: # No actual for a mandatory formal if kind.is_positional(): + missing_positional.append(i) self.msg.too_few_arguments(callee, context, actual_names) if object_type and callable_name and "." in callable_name: self.missing_classvar_callable_note(object_type, callable_name, context) @@ -2418,7 +2578,7 @@ def check_argument_count( if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2 and paramspec_entries > 1: self.msg.fail("ParamSpec.kwargs should only be passed once", context) ok = False - return ok + return ok, missing_positional def check_for_extra_actual_arguments( self, @@ -2878,7 +3038,7 @@ def has_shape(typ: Type) -> bool: matches.append(typ) elif self.check_argument_count( typ, arg_types, arg_kinds, arg_names, formal_to_actual, None - ): + )[0]: if args_have_var_arg and typ.is_var_arg: star_matches.append(typ) elif args_have_kw_arg and typ.is_kw_arg: @@ -3251,7 +3411,7 @@ def erased_signature_similarity( with self.msg.filter_errors(): if not self.check_argument_count( callee, arg_types, arg_kinds, arg_names, formal_to_actual, None - ): + )[0]: # Too few or many arguments -> no match. return False diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index b54dffe836b8e..c4dff90283965 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3703,3 +3703,51 @@ foo(*args) # E: Argument 1 to "foo" has incompatible type "*list[object]"; expe kwargs: dict[str, object] foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P" [builtins fixtures/dict.pyi] + +[case testMissingPositionalArgumentShiftedTypes] +def f(x: int, y: str, z: bytes, aa: int) -> None: ... + +f(1, b'x', 1) +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Expected "str" for parameter "y"; did you forget argument "y"? + +[case testMissingPositionalArgumentShiftedTypesFirstArg] +def f(x: int, y: str, z: bytes) -> None: ... + +f("hello", b'x') +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Expected "int" for parameter "x"; did you forget argument "x"? + +[case testMissingPositionalArgumentNoShift] +def f(x: int, y: str, z: bytes) -> None: ... + +f("wrong", 123) +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Incompatible arguments for "f"; check for missing arguments + +[case testMissingPositionalArgumentShiftedTypesManyArgs] +def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... + +f(1, 1.5, [1, 2, 3], ("a", "b")) +[builtins fixtures/list.pyi] +[out] +main:3: error: Expected "str" for parameter "b"; did you forget argument "b"? + +[case testMissingPositionalArgumentShiftedWithDefaults] +def f(x: int, y: str, z: bytes = b'default') -> None: ... + +f("hello") +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Incompatible arguments for "f"; check for missing arguments + +[case testMissingPositionalArgumentShiftedWithStarArgs] +def f(x: int, y: str, z: bytes, *args: int) -> None: ... + +f("hello", b'x') +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Incompatible arguments for "f"; check for missing arguments From 7745bd2dad0ecc8af4e7d53463cb521528a4e96a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:56:17 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 68 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 2ff78ae2525d8..32a8b8062f93d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1799,8 +1799,15 @@ def check_callable_call( arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual) self.check_call_arguments( - callee, arg_types, arg_kinds, arg_names, args, - formal_to_actual, context, callable_name, object_type, + callee, + arg_types, + arg_kinds, + arg_names, + args, + formal_to_actual, + context, + callable_name, + object_type, ) if ( @@ -2345,8 +2352,14 @@ def check_call_arguments( """Check argument count and types, consolidating errors for missing positional args.""" with self.msg.filter_errors(): _, missing_positional = self.check_argument_count( - callee, arg_types, arg_kinds, arg_names, formal_to_actual, - context, object_type, callable_name, + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, ) if missing_positional: @@ -2363,8 +2376,13 @@ def check_call_arguments( with self.msg.filter_errors() as type_error_watcher: self.check_argument_types( - arg_types, arg_kinds, args, callee, formal_to_actual, context, - object_type=object_type + arg_types, + arg_kinds, + args, + callee, + formal_to_actual, + context, + object_type=object_type, ) has_type_errors = type_error_watcher.has_new_errors() @@ -2392,17 +2410,34 @@ def check_call_arguments( ) else: self.check_argument_count( - callee, arg_types, arg_kinds, arg_names, formal_to_actual, - context, object_type, callable_name, + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, ) else: self.check_argument_count( - callee, arg_types, arg_kinds, arg_names, formal_to_actual, - context, object_type, callable_name, + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, ) self.check_argument_types( - arg_types, arg_kinds, args, callee, formal_to_actual, context, - object_type=object_type + arg_types, + arg_kinds, + args, + callee, + formal_to_actual, + context, + object_type=object_type, ) def detect_shifted_positional_args( @@ -2452,7 +2487,9 @@ def detect_shifted_positional_args( next_idx = i + 1 if next_idx >= len(positional_formal_types): break - if is_subtype(actual_type, positional_formal_types[next_idx], options=self.chk.options): + if is_subtype( + actual_type, positional_formal_types[next_idx], options=self.chk.options + ): shift_position = i break else: @@ -2475,10 +2512,7 @@ def detect_shifted_positional_args( ) def _validate_shift_insertion( - self, - actual_types: list[Type], - formal_types: list[Type], - insert_position: int, + self, actual_types: list[Type], formal_types: list[Type], insert_position: int ) -> bool: """Check if inserting an argument at insert_position would fix type errors.""" for i, actual_type in enumerate(actual_types): From 04a812107ae8023e848bc05c0a7a3d4c15a4526b Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 23 Jan 2026 00:40:34 -0500 Subject: [PATCH 3/8] fallback to preexisting way --- mypy/checkexpr.py | 22 ++++++++++++++-------- test-data/unit/check-functions.test | 11 ++++++++--- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 32a8b8062f93d..8bd49f9167299 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2397,16 +2397,22 @@ def check_call_arguments( code=codes.CALL_ARG, ) else: - self.msg.fail( - f'Incompatible arguments for "{func_name}"; check for missing arguments', - context, - code=codes.CALL_ARG, + self.check_argument_count( + callee, arg_types, arg_kinds, arg_names, formal_to_actual, + context, object_type, callable_name, + ) + self.check_argument_types( + arg_types, arg_kinds, args, callee, formal_to_actual, context, + object_type=object_type ) elif has_type_errors: - self.msg.fail( - f'Incompatible arguments for "{func_name}"; check for missing arguments', - context, - code=codes.CALL_ARG, + self.check_argument_count( + callee, arg_types, arg_kinds, arg_names, formal_to_actual, + context, object_type, callable_name, + ) + self.check_argument_types( + arg_types, arg_kinds, args, callee, formal_to_actual, context, + object_type=object_type ) else: self.check_argument_count( diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index c4dff90283965..5997e98a0a295 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3726,7 +3726,9 @@ def f(x: int, y: str, z: bytes) -> None: ... f("wrong", 123) [builtins fixtures/primitives.pyi] [out] -main:3: error: Incompatible arguments for "f"; check for missing arguments +main:3: error: Missing positional argument "z" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" +main:3: error: Argument 2 to "f" has incompatible type "int"; expected "str" [case testMissingPositionalArgumentShiftedTypesManyArgs] def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... @@ -3742,7 +3744,8 @@ def f(x: int, y: str, z: bytes = b'default') -> None: ... f("hello") [builtins fixtures/primitives.pyi] [out] -main:3: error: Incompatible arguments for "f"; check for missing arguments +main:3: error: Missing positional argument "y" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" [case testMissingPositionalArgumentShiftedWithStarArgs] def f(x: int, y: str, z: bytes, *args: int) -> None: ... @@ -3750,4 +3753,6 @@ def f(x: int, y: str, z: bytes, *args: int) -> None: ... f("hello", b'x') [builtins fixtures/primitives.pyi] [out] -main:3: error: Incompatible arguments for "f"; check for missing arguments +main:3: error: Missing positional argument "z" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" +main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str" From 6c2bfddac99afde8607080a4c5d11b247035707a Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 23 Jan 2026 01:09:26 -0500 Subject: [PATCH 4/8] kept more consistent error messaging --- mypy/checkexpr.py | 17 ++++++++++++----- test-data/unit/check-functions.test | 6 +++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8bd49f9167299..f210aa8f497dc 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -33,7 +33,7 @@ from mypy.maptype import 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 +from mypy.messages import MessageBuilder, format_type, format_type_distinctly from mypy.nodes import ( ARG_NAMED, ARG_POS, @@ -2387,12 +2387,19 @@ def check_call_arguments( has_type_errors = type_error_watcher.has_new_errors() if shift_info is not None: - _, param_name, expected_type, high_confidence = shift_info + shift_position, param_name, expected_type, high_confidence = shift_info if high_confidence and param_name: - type_str = format_type(expected_type, self.chk.options) + positional_arg_types = [ + arg_types[i] for i, k in enumerate(arg_kinds) if k == nodes.ARG_POS + ] + actual_type = positional_arg_types[shift_position - 1] + actual_str, expected_str = format_type_distinctly( + actual_type, expected_type, options=self.chk.options + ) self.msg.fail( - f'Expected {type_str} for parameter "{param_name}"; ' - f'did you forget argument "{param_name}"?', + f'Argument {shift_position} to "{func_name}" has incompatible type ' + f'{actual_str}; expected {expected_str} ' + f'(did you forget argument "{param_name}"?)', context, code=codes.CALL_ARG, ) diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 5997e98a0a295..112952c23e15e 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3710,7 +3710,7 @@ def f(x: int, y: str, z: bytes, aa: int) -> None: ... f(1, b'x', 1) [builtins fixtures/primitives.pyi] [out] -main:3: error: Expected "str" for parameter "y"; did you forget argument "y"? +main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str" (did you forget argument "y"?) [case testMissingPositionalArgumentShiftedTypesFirstArg] def f(x: int, y: str, z: bytes) -> None: ... @@ -3718,7 +3718,7 @@ def f(x: int, y: str, z: bytes) -> None: ... f("hello", b'x') [builtins fixtures/primitives.pyi] [out] -main:3: error: Expected "int" for parameter "x"; did you forget argument "x"? +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" (did you forget argument "x"?) [case testMissingPositionalArgumentNoShift] def f(x: int, y: str, z: bytes) -> None: ... @@ -3736,7 +3736,7 @@ def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... f(1, 1.5, [1, 2, 3], ("a", "b")) [builtins fixtures/list.pyi] [out] -main:3: error: Expected "str" for parameter "b"; did you forget argument "b"? +main:3: error: Argument 2 to "f" has incompatible type "float"; expected "str" (did you forget argument "b"?) [case testMissingPositionalArgumentShiftedWithDefaults] def f(x: int, y: str, z: bytes = b'default') -> None: ... From 8fc27d55954e58cd04757df01c32fd1c562ea7a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 06:13:52 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f210aa8f497dc..c64eabf047fde 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2398,28 +2398,50 @@ def check_call_arguments( ) self.msg.fail( f'Argument {shift_position} to "{func_name}" has incompatible type ' - f'{actual_str}; expected {expected_str} ' + f"{actual_str}; expected {expected_str} " f'(did you forget argument "{param_name}"?)', context, code=codes.CALL_ARG, ) else: self.check_argument_count( - callee, arg_types, arg_kinds, arg_names, formal_to_actual, - context, object_type, callable_name, + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, ) self.check_argument_types( - arg_types, arg_kinds, args, callee, formal_to_actual, context, - object_type=object_type + arg_types, + arg_kinds, + args, + callee, + formal_to_actual, + context, + object_type=object_type, ) elif has_type_errors: self.check_argument_count( - callee, arg_types, arg_kinds, arg_names, formal_to_actual, - context, object_type, callable_name, + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, ) self.check_argument_types( - arg_types, arg_kinds, args, callee, formal_to_actual, context, - object_type=object_type + arg_types, + arg_kinds, + args, + callee, + formal_to_actual, + context, + object_type=object_type, ) else: self.check_argument_count( From f307c28552a524710cd6967df0415b77f7c09572 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 30 Jan 2026 00:35:37 -0500 Subject: [PATCH 6/8] made error messages more consistent and improved test names to not include implementation details --- mypy/checkexpr.py | 13 ++++++------ test-data/unit/check-functions.test | 32 +++++++++++++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index c64eabf047fde..f0addfef4064b 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2398,8 +2398,7 @@ def check_call_arguments( ) self.msg.fail( f'Argument {shift_position} to "{func_name}" has incompatible type ' - f"{actual_str}; expected {expected_str} " - f'(did you forget argument "{param_name}"?)', + f"{actual_str}; expected {expected_str}", context, code=codes.CALL_ARG, ) @@ -2491,13 +2490,15 @@ def detect_shifted_positional_args( if not missing_positional: return None + # Only attempt shift detection when exactly one argument is missing. + # When multiple arguments are missing, we should fall back to the original behavior. + if len(missing_positional) != 1: + return None + has_star_args = any(k == nodes.ARG_STAR for k in callee.arg_kinds) has_star_kwargs = any(k == nodes.ARG_STAR2 for k in callee.arg_kinds) has_defaults = any(k == nodes.ARG_OPT for k in callee.arg_kinds) - single_missing = len(missing_positional) == 1 - high_confidence = ( - single_missing and not has_star_args and not has_star_kwargs and not has_defaults - ) + high_confidence = not has_star_args and not has_star_kwargs and not has_defaults positional_actual_types = [ actual_types[i] for i, k in enumerate(actual_kinds) if k == nodes.ARG_POS diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 112952c23e15e..28412380ff68a 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3704,23 +3704,31 @@ kwargs: dict[str, object] foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P" [builtins fixtures/dict.pyi] -[case testMissingPositionalArgumentShiftedTypes] +[case testMissingPositionalArgumentTypeMismatch] def f(x: int, y: str, z: bytes, aa: int) -> None: ... f(1, b'x', 1) [builtins fixtures/primitives.pyi] [out] -main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str" (did you forget argument "y"?) +main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str" -[case testMissingPositionalArgumentShiftedTypesFirstArg] +[case testMissingPositionalArgumentTypeMismatchFirst] def f(x: int, y: str, z: bytes) -> None: ... f("hello", b'x') [builtins fixtures/primitives.pyi] [out] -main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" (did you forget argument "x"?) +main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" + +[case testMissingPositionalArgumentManyArgs] +def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... + +f(1, 1.5, [1, 2, 3], ("a", "b")) +[builtins fixtures/list.pyi] +[out] +main:3: error: Argument 2 to "f" has incompatible type "float"; expected "str" -[case testMissingPositionalArgumentNoShift] +[case testMissingPositionalArgumentNoPattern] def f(x: int, y: str, z: bytes) -> None: ... f("wrong", 123) @@ -3730,15 +3738,17 @@ main:3: error: Missing positional argument "z" in call to "f" main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" main:3: error: Argument 2 to "f" has incompatible type "int"; expected "str" -[case testMissingPositionalArgumentShiftedTypesManyArgs] -def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... +[case testMissingMultiplePositionalArguments] +def f(a: int, b: str, c: float, d: list[int]) -> None: ... -f(1, 1.5, [1, 2, 3], ("a", "b")) +f(1.5, [1, 2, 3]) [builtins fixtures/list.pyi] [out] -main:3: error: Argument 2 to "f" has incompatible type "float"; expected "str" (did you forget argument "b"?) +main:3: error: Missing positional arguments "c", "d" in call to "f" +main:3: error: Argument 1 to "f" has incompatible type "float"; expected "int" +main:3: error: Argument 2 to "f" has incompatible type "list[int]"; expected "str" -[case testMissingPositionalArgumentShiftedWithDefaults] +[case testMissingPositionalArgumentWithDefaults] def f(x: int, y: str, z: bytes = b'default') -> None: ... f("hello") @@ -3747,7 +3757,7 @@ f("hello") main:3: error: Missing positional argument "y" in call to "f" main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" -[case testMissingPositionalArgumentShiftedWithStarArgs] +[case testMissingPositionalArgumentWithStarArgs] def f(x: int, y: str, z: bytes, *args: int) -> None: ... f("hello", b'x') From c59e4f9796bba6598a7bed983e02cc7949d38f9c Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 6 Feb 2026 03:47:36 -0500 Subject: [PATCH 7/8] refactored code --- mypy/checkexpr.py | 294 ++++++---------------------- test-data/unit/check-columns.test | 8 + test-data/unit/check-functions.test | 28 ++- 3 files changed, 86 insertions(+), 244 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f0addfef4064b..d75a70fbc471d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -33,7 +33,7 @@ from mypy.maptype import 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, format_type_distinctly +from mypy.messages import MessageBuilder, callable_name, format_type from mypy.nodes import ( ARG_NAMED, ARG_POS, @@ -1798,17 +1798,29 @@ def check_callable_call( arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual) - self.check_call_arguments( - callee, - arg_types, - arg_kinds, - arg_names, - args, - formal_to_actual, - context, - callable_name, - object_type, - ) + if not self._detect_missing_positional_arg( + callee, arg_types, arg_kinds, args, context + ): + self.check_argument_count( + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, + ) + + self.check_argument_types( + arg_types, + arg_kinds, + args, + callee, + formal_to_actual, + context, + object_type=object_type, + ) if ( callee.is_type_obj() @@ -2337,232 +2349,51 @@ def apply_inferred_arguments( # arguments. return self.apply_generic_arguments(callee_type, inferred_args, context) - def check_call_arguments( + def _detect_missing_positional_arg( self, callee: CallableType, arg_types: list[Type], arg_kinds: list[ArgKind], - arg_names: Sequence[str | None] | None, args: list[Expression], - formal_to_actual: list[list[int]], context: Context, - callable_name: str | None, - object_type: Type | None, - ) -> None: - """Check argument count and types, consolidating errors for missing positional args.""" - with self.msg.filter_errors(): - _, missing_positional = self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) - - if missing_positional: - func_name = callable_name or callee.name or "function" - if "." in func_name: - func_name = func_name.split(".")[-1] - - shift_info = None - num_positional_args = sum(1 for k in arg_kinds if k == nodes.ARG_POS) - if num_positional_args >= 2: - shift_info = self.detect_shifted_positional_args( - callee, arg_types, arg_kinds, missing_positional - ) - - with self.msg.filter_errors() as type_error_watcher: - self.check_argument_types( - arg_types, - arg_kinds, - args, - callee, - formal_to_actual, - context, - object_type=object_type, - ) - has_type_errors = type_error_watcher.has_new_errors() - - if shift_info is not None: - shift_position, param_name, expected_type, high_confidence = shift_info - if high_confidence and param_name: - positional_arg_types = [ - arg_types[i] for i, k in enumerate(arg_kinds) if k == nodes.ARG_POS - ] - actual_type = positional_arg_types[shift_position - 1] - actual_str, expected_str = format_type_distinctly( - actual_type, expected_type, options=self.chk.options - ) - self.msg.fail( - f'Argument {shift_position} to "{func_name}" has incompatible type ' - f"{actual_str}; expected {expected_str}", - context, - code=codes.CALL_ARG, - ) - else: - self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) - self.check_argument_types( - arg_types, - arg_kinds, - args, - callee, - formal_to_actual, - context, - object_type=object_type, - ) - elif has_type_errors: - self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) - self.check_argument_types( - arg_types, - arg_kinds, - args, - callee, - formal_to_actual, - context, - object_type=object_type, - ) - else: - self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) - else: - self.check_argument_count( - callee, - arg_types, - arg_kinds, - arg_names, - formal_to_actual, - context, - object_type, - callable_name, - ) - self.check_argument_types( - arg_types, - arg_kinds, - args, - callee, - formal_to_actual, - context, - object_type=object_type, - ) - - def detect_shifted_positional_args( - self, - callee: CallableType, - actual_types: list[Type], - actual_kinds: list[ArgKind], - missing_positional: list[int], - ) -> tuple[int, str | None, Type, bool] | None: - """Detect if positional arguments are shifted due to a missing argument. + ) -> bool: + """Try to identify a single missing positional argument using type alignment. - Returns (1-indexed position, param name, expected type, high_confidence) if a - shift pattern is found, None otherwise. High confidence is set when the function - has fixed parameters (no defaults, *args, or **kwargs). + If the caller and callee are just positional arguments and exactly one arg is missing, + we scan left to right to find which argument skipped. If there is an error, report it + and return True, or return False to fall back to normal checking. """ - if not missing_positional: - return None - - # Only attempt shift detection when exactly one argument is missing. - # When multiple arguments are missing, we should fall back to the original behavior. - if len(missing_positional) != 1: - return None - - has_star_args = any(k == nodes.ARG_STAR for k in callee.arg_kinds) - has_star_kwargs = any(k == nodes.ARG_STAR2 for k in callee.arg_kinds) - has_defaults = any(k == nodes.ARG_OPT for k in callee.arg_kinds) - high_confidence = not has_star_args and not has_star_kwargs and not has_defaults - - positional_actual_types = [ - actual_types[i] for i, k in enumerate(actual_kinds) if k == nodes.ARG_POS - ] - if len(positional_actual_types) < 2: - return None + if not all(k == ARG_POS for k in callee.arg_kinds): + return False + if not all(k == ARG_POS for k in arg_kinds): + return False + if len(arg_kinds) != len(callee.arg_kinds) - 1: + return False - positional_formal_types: list[Type] = [] - positional_formal_names: list[str | None] = [] - for i, kind in enumerate(callee.arg_kinds): - if kind.is_positional(): - positional_formal_types.append(callee.arg_types[i]) - positional_formal_names.append(callee.arg_names[i]) - - # Find first position where arg doesn't match but would match next position - shift_position = None - for i, actual_type in enumerate(positional_actual_types): - if i >= len(positional_formal_types): - break - if is_subtype(actual_type, positional_formal_types[i], options=self.chk.options): - continue - next_idx = i + 1 - if next_idx >= len(positional_formal_types): - break - if is_subtype( - actual_type, positional_formal_types[next_idx], options=self.chk.options - ): - shift_position = i + skip_idx: int | None = None + j = 0 + for i in range(len(callee.arg_types)): + if j >= len(arg_types): + skip_idx = i break + if is_subtype(arg_types[j], callee.arg_types[i], options=self.chk.options): + j += 1 + elif skip_idx is None: + skip_idx = i else: - break - - if shift_position is None: - return None + return False - # Validate that all args would match if we inserted one at shift_position - if not self._validate_shift_insertion( - positional_actual_types, positional_formal_types, shift_position - ): - return None + if skip_idx is None or j != len(arg_types): + return False - return ( - shift_position + 1, - positional_formal_names[shift_position], - positional_formal_types[shift_position], - high_confidence, - ) + param_name = callee.arg_names[skip_idx] + callee_name = callable_name(callee) + if param_name is None or callee_name is None: + return False - def _validate_shift_insertion( - self, actual_types: list[Type], formal_types: list[Type], insert_position: int - ) -> bool: - """Check if inserting an argument at insert_position would fix type errors.""" - for i, actual_type in enumerate(actual_types): - if i < insert_position: - if i >= len(formal_types): - return False - expected = formal_types[i] - else: - shifted_idx = i + 1 - if shifted_idx >= len(formal_types): - return False - expected = formal_types[shifted_idx] - if not is_subtype(actual_type, expected, options=self.chk.options): - return False + msg = f'Missing positional argument "{param_name}" in call to {callee_name}' + ctx = args[skip_idx] if skip_idx < len(args) else context + self.msg.fail(msg, ctx, code=codes.CALL_ARG) return True def check_argument_count( @@ -2575,15 +2406,13 @@ def check_argument_count( context: Context | None, object_type: Type | None = None, callable_name: str | None = None, - ) -> tuple[bool, list[int]]: + ) -> bool: """Check that there is a value for all required arguments to a function. Also check that there are no duplicate values for arguments. Report found errors using 'messages' if it's not None. If 'messages' is given, 'context' must also be given. - Return a tuple of: - - False if there were any errors, True otherwise - - List of formal argument indices that are missing positional arguments + Return False if there were any errors. Otherwise return True """ if context is None: # Avoid "is None" checks @@ -2601,15 +2430,12 @@ def check_argument_count( callee, actual_types, actual_kinds, actual_names, all_actuals, context ) - missing_positional: list[int] = [] - # Check for too many or few values for formals. for i, kind in enumerate(callee.arg_kinds): mapped_args = formal_to_actual[i] if kind.is_required() and not mapped_args and not is_unexpected_arg_error: # No actual for a mandatory formal if kind.is_positional(): - missing_positional.append(i) self.msg.too_few_arguments(callee, context, actual_names) if object_type and callable_name and "." in callable_name: self.missing_classvar_callable_note(object_type, callable_name, context) @@ -2648,7 +2474,7 @@ def check_argument_count( if actual_kinds[mapped_args[0]] == nodes.ARG_STAR2 and paramspec_entries > 1: self.msg.fail("ParamSpec.kwargs should only be passed once", context) ok = False - return ok, missing_positional + return ok def check_for_extra_actual_arguments( self, @@ -3108,7 +2934,7 @@ def has_shape(typ: Type) -> bool: matches.append(typ) elif self.check_argument_count( typ, arg_types, arg_kinds, arg_names, formal_to_actual, None - )[0]: + ): if args_have_var_arg and typ.is_var_arg: star_matches.append(typ) elif args_have_kw_arg and typ.is_kw_arg: @@ -3481,7 +3307,7 @@ def erased_signature_similarity( with self.msg.filter_errors(): if not self.check_argument_count( callee, arg_types, arg_kinds, arg_names, formal_to_actual, None - )[0]: + ): # Too few or many arguments -> no match. return False diff --git a/test-data/unit/check-columns.test b/test-data/unit/check-columns.test index 8458f0ac27dcd..13d4ab125e7c7 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -409,6 +409,14 @@ main:2:10:2:17: error: Incompatible types in assignment (expression has type "st main:6:3:7:1: error: Argument 1 to "f" has incompatible type "int"; expected "str" main:8:1:8:4: error: Value of type "int" is not indexable +[case testColumnsMissingPositionalArgShiftDetected] +def f(x: int, y: str, z: bytes, aa: int) -> None: ... +f(1, b'x', 1) # E:6: Missing positional argument "y" in call to "f" +def g(x: int, y: str, z: bytes) -> None: ... +g("hello", b'x') # E:3: Missing positional argument "x" in call to "g" +g(1, "hello") # E:1: Missing positional argument "z" in call to "g" +[builtins fixtures/primitives.pyi] + [case testEndColumnsWithTooManyTypeVars] # flags: --pretty import typing diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 28412380ff68a..444f908c244e1 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3704,31 +3704,39 @@ kwargs: dict[str, object] foo(**kwargs) # E: Argument 1 to "foo" has incompatible type "**dict[str, object]"; expected "P" [builtins fixtures/dict.pyi] -[case testMissingPositionalArgumentTypeMismatch] +[case testMissingPositionalArgShiftDetectedMiddle] def f(x: int, y: str, z: bytes, aa: int) -> None: ... f(1, b'x', 1) [builtins fixtures/primitives.pyi] [out] -main:3: error: Argument 2 to "f" has incompatible type "bytes"; expected "str" +main:3: error: Missing positional argument "y" in call to "f" -[case testMissingPositionalArgumentTypeMismatchFirst] +[case testMissingPositionalArgShiftDetectedFirst] def f(x: int, y: str, z: bytes) -> None: ... f("hello", b'x') [builtins fixtures/primitives.pyi] [out] -main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" +main:3: error: Missing positional argument "x" in call to "f" -[case testMissingPositionalArgumentManyArgs] +[case testMissingPositionalArgShiftDetectedManyArgs] def f(a: int, b: str, c: float, d: list[int], e: tuple[str, ...]) -> None: ... f(1, 1.5, [1, 2, 3], ("a", "b")) [builtins fixtures/list.pyi] [out] -main:3: error: Argument 2 to "f" has incompatible type "float"; expected "str" +main:3: error: Missing positional argument "b" in call to "f" + +[case testMissingPositionalArgShiftDetectedLast] +def f(x: int, y: str, z: bytes) -> None: ... + +f(1, "hello") +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "z" in call to "f" -[case testMissingPositionalArgumentNoPattern] +[case testMissingPositionalArgNoShiftPattern] def f(x: int, y: str, z: bytes) -> None: ... f("wrong", 123) @@ -3738,7 +3746,7 @@ main:3: error: Missing positional argument "z" in call to "f" main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" main:3: error: Argument 2 to "f" has incompatible type "int"; expected "str" -[case testMissingMultiplePositionalArguments] +[case testMissingPositionalArgMultipleMissing] def f(a: int, b: str, c: float, d: list[int]) -> None: ... f(1.5, [1, 2, 3]) @@ -3748,7 +3756,7 @@ main:3: error: Missing positional arguments "c", "d" in call to "f" main:3: error: Argument 1 to "f" has incompatible type "float"; expected "int" main:3: error: Argument 2 to "f" has incompatible type "list[int]"; expected "str" -[case testMissingPositionalArgumentWithDefaults] +[case testMissingPositionalArgWithDefaults] def f(x: int, y: str, z: bytes = b'default') -> None: ... f("hello") @@ -3757,7 +3765,7 @@ f("hello") main:3: error: Missing positional argument "y" in call to "f" main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int" -[case testMissingPositionalArgumentWithStarArgs] +[case testMissingPositionalArgWithStarArgs] def f(x: int, y: str, z: bytes, *args: int) -> None: ... f("hello", b'x') From 3340653d315a761dc0414dbb9f5916e0af41d34a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 08:49:33 +0000 Subject: [PATCH 8/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d75a70fbc471d..cc5e6763bf675 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1798,9 +1798,7 @@ def check_callable_call( arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual) - if not self._detect_missing_positional_arg( - callee, arg_types, arg_kinds, args, context - ): + if not self._detect_missing_positional_arg(callee, arg_types, arg_kinds, args, context): self.check_argument_count( callee, arg_types, @@ -2360,7 +2358,7 @@ def _detect_missing_positional_arg( """Try to identify a single missing positional argument using type alignment. If the caller and callee are just positional arguments and exactly one arg is missing, - we scan left to right to find which argument skipped. If there is an error, report it + we scan left to right to find which argument skipped. If there is an error, report it and return True, or return False to fall back to normal checking. """ if not all(k == ARG_POS for k in callee.arg_kinds):