From a2368f8822b324dac6728f05fb0e51578d9d4284 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 16 Jan 2026 02:53:04 -0500 Subject: [PATCH 01/12] 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 345c6a4646384..630d9f78e7271 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1792,19 +1792,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 ( @@ -2335,6 +2325,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, @@ -2345,13 +2500,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 @@ -2369,12 +2526,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) @@ -2413,7 +2573,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, @@ -2873,7 +3033,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: @@ -3246,7 +3406,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 7eb6723844b4c..73af5d0825a75 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3767,3 +3767,51 @@ class C: def defer() -> int: return 1 + +[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 8ea8ea3c9f13c5fb5855551b5aa493994dad0c5d 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 02/12] [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 630d9f78e7271..9fb6c5ffd6703 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1793,8 +1793,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 ( @@ -2340,8 +2347,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: @@ -2358,8 +2371,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() @@ -2387,17 +2405,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( @@ -2447,7 +2482,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: @@ -2470,10 +2507,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 1c598bd6f186f00932c507086096ed87a27f1ff2 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 23 Jan 2026 00:40:34 -0500 Subject: [PATCH 03/12] 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 9fb6c5ffd6703..534b05931b220 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2392,16 +2392,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 73af5d0825a75..481033e68aee8 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3790,7 +3790,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: ... @@ -3806,7 +3808,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: ... @@ -3814,4 +3817,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 319057c09e14aaa30922d5c36f9faba2064b0789 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 23 Jan 2026 01:09:26 -0500 Subject: [PATCH 04/12] 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 534b05931b220..b490cecdd6611 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, @@ -2382,12 +2382,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 481033e68aee8..32c3a789aaf89 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3774,7 +3774,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: ... @@ -3782,7 +3782,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: ... @@ -3800,7 +3800,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 2c431a51435843e6d131b7de21118f23f1301cfc 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 05/12] [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 b490cecdd6611..e5c3fae3b3dd4 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2393,28 +2393,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 a432f38b058fb15a691f04aafaabb4a85591d895 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 30 Jan 2026 00:35:37 -0500 Subject: [PATCH 06/12] 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 e5c3fae3b3dd4..63973f3519f4f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2393,8 +2393,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, ) @@ -2486,13 +2485,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 32c3a789aaf89..eb103d21fbcef 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3768,23 +3768,31 @@ class C: def defer() -> int: return 1 -[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) @@ -3794,15 +3802,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") @@ -3811,7 +3821,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 f7b108f226dab9d139daf093ca527c82033c3809 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 6 Feb 2026 03:47:36 -0500 Subject: [PATCH 07/12] 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 63973f3519f4f..441e2abdbc56d 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, @@ -1792,17 +1792,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() @@ -2332,232 +2344,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( @@ -2570,15 +2401,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 @@ -2596,15 +2425,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) @@ -2643,7 +2469,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, @@ -3103,7 +2929,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: @@ -3476,7 +3302,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 7a6c5bd5bc081..bf6865bcb6111 100644 --- a/test-data/unit/check-columns.test +++ b/test-data/unit/check-columns.test @@ -408,6 +408,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 eb103d21fbcef..7fe9cd8e883b8 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3768,31 +3768,39 @@ class C: def defer() -> int: return 1 -[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) @@ -3802,7 +3810,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]) @@ -3812,7 +3820,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") @@ -3821,7 +3829,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 c967cdbb4b5b9263557cd8d583e712baf4b27082 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Fri, 13 Feb 2026 00:00:33 -0500 Subject: [PATCH 08/12] handle missing pos args at the end --- mypy/checkexpr.py | 8 ++++++-- test-data/unit/check-functions.test | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 441e2abdbc56d..0d35295842f72 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2355,8 +2355,9 @@ 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 - and return True, or return False to fall back to normal checking. + we scan left to right to find which argument skipped. If only the last argument is missing, + we return False since it's already handled in a desired manner. 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): return False @@ -2381,6 +2382,9 @@ def _detect_missing_positional_arg( if skip_idx is None or j != len(arg_types): return False + if skip_idx == len(callee.arg_types) - 1: + return False + param_name = callee.arg_names[skip_idx] callee_name = callable_name(callee) if param_name is None or callee_name is None: diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test index 7fe9cd8e883b8..62c6accecfe46 100644 --- a/test-data/unit/check-functions.test +++ b/test-data/unit/check-functions.test @@ -3810,6 +3810,22 @@ 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 testMissingPositionalArgNoShiftPatternLast] +def f(x: int, y: str, z: bytes) -> None: ... + +f(123, "wrong") +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "z" in call to "f" + +[case testMissingPositionalArgNoShiftPatternNone] +def f(x: int) -> None: ... + +f() +[builtins fixtures/primitives.pyi] +[out] +main:3: error: Missing positional argument "x" in call to "f" + [case testMissingPositionalArgMultipleMissing] def f(a: int, b: str, c: float, d: list[int]) -> None: ... From 97bce106c1f344744b9843e32cd8638c695ebe3b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:40:14 +0000 Subject: [PATCH 09/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 0d35295842f72..d67dd45f35612 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1792,9 +1792,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, @@ -2355,8 +2353,8 @@ 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 only the last argument is missing, - we return False since it's already handled in a desired manner. If there is an error, + we scan left to right to find which argument skipped. If only the last argument is missing, + we return False since it's already handled in a desired manner. 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): From bebf5de14eec75991191403b2d52008378caa401 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Sun, 15 Mar 2026 23:12:08 -0400 Subject: [PATCH 10/12] refactor pos arg errors to be more performant --- mypy/checkexpr.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d67dd45f35612..8fd0baf761b08 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1792,8 +1792,8 @@ 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): - self.check_argument_count( + with self.msg.filter_errors(save_filtered_errors=True) as w: + ok = self.check_argument_count( callee, arg_types, arg_kinds, @@ -1804,6 +1804,16 @@ def check_callable_call( callable_name, ) + if ( + not ok + and not self.msg.prefer_simple_messages() + and self._detect_missing_positional_arg( + callee, arg_types, arg_kinds, args, context + ) + ): + pass + else: + self.msg.add_errors(w.filtered_errors()) self.check_argument_types( arg_types, arg_kinds, From 1ff910c2d247ba8d1cfdd55ffde7151253e80222 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 03:13:58 +0000 Subject: [PATCH 11/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8fd0baf761b08..6ccac07748530 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1807,9 +1807,7 @@ def check_callable_call( if ( not ok and not self.msg.prefer_simple_messages() - and self._detect_missing_positional_arg( - callee, arg_types, arg_kinds, args, context - ) + and self._detect_missing_positional_arg(callee, arg_types, arg_kinds, args, context) ): pass else: From d92333d893f6b74d991d2b7e64815ec4834c8084 Mon Sep 17 00:00:00 2001 From: KevinRK29 Date: Mon, 16 Mar 2026 03:28:54 -0400 Subject: [PATCH 12/12] add cheap gate to avoid expensive checks on hot path --- mypy/checkexpr.py | 47 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 6ccac07748530..f9afc8586dea9 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1792,8 +1792,42 @@ def check_callable_call( arg_types = self.infer_arg_types_in_context(callee, args, arg_kinds, formal_to_actual) - with self.msg.filter_errors(save_filtered_errors=True) as w: - ok = self.check_argument_count( + might_have_shifted_args = ( + not self.msg.prefer_simple_messages() + and all(k == ARG_POS for k in callee.arg_kinds) + and all(k == ARG_POS for k in arg_kinds) + and len(arg_kinds) == len(callee.arg_kinds) - 1 + ) + + if might_have_shifted_args: + with self.msg.filter_errors(save_filtered_errors=True) as w: + ok = self.check_argument_count( + callee, + arg_types, + arg_kinds, + arg_names, + formal_to_actual, + context, + object_type, + callable_name, + ) + if not ok and self._detect_missing_positional_arg( + callee, arg_types, arg_kinds, args, context + ): + pass + else: + self.msg.add_errors(w.filtered_errors()) + 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, @@ -1803,15 +1837,6 @@ def check_callable_call( object_type, callable_name, ) - - if ( - not ok - and not self.msg.prefer_simple_messages() - and self._detect_missing_positional_arg(callee, arg_types, arg_kinds, args, context) - ): - pass - else: - self.msg.add_errors(w.filtered_errors()) self.check_argument_types( arg_types, arg_kinds,