Skip to content
86 changes: 72 additions & 14 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, callable_name, format_type
from mypy.nodes import (
ARG_NAMED,
ARG_POS,
Expand Down Expand Up @@ -1798,20 +1798,27 @@ 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,
)
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
)
self.check_argument_types(
arg_types,
arg_kinds,
args,
callee,
formal_to_actual,
context,
object_type=object_type,
)

if (
callee.is_type_obj()
Expand Down Expand Up @@ -2340,6 +2347,57 @@ def apply_inferred_arguments(
# arguments.
return self.apply_generic_arguments(callee_type, inferred_args, context)

def _detect_missing_positional_arg(
self,
callee: CallableType,
arg_types: list[Type],
arg_kinds: list[ArgKind],
args: list[Expression],
context: Context,
) -> bool:
"""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,
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
if not all(k == ARG_POS for k in arg_kinds):
return False
if len(arg_kinds) != len(callee.arg_kinds) - 1:
return False

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:
return False

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:
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(
self,
callee: CallableType,
Expand Down
8 changes: 8 additions & 0 deletions test-data/unit/check-columns.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -3703,3 +3703,90 @@ 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 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: Missing positional argument "y" in call to "f"

[case testMissingPositionalArgShiftDetectedFirst]
def f(x: int, y: str, z: bytes) -> None: ...

f("hello", b'x')
[builtins fixtures/primitives.pyi]
[out]
main:3: error: Missing positional argument "x" in call to "f"

[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: 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 testMissingPositionalArgNoShiftPattern]
def f(x: int, y: str, z: bytes) -> None: ...

f("wrong", 123)
[builtins fixtures/primitives.pyi]
[out]
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: ...

f(1.5, [1, 2, 3])
[builtins fixtures/list.pyi]
[out]
main:3: error: Missing positional arguments "c", "d" in call to "f"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there are multiple positional arguments missing, it seems reasonable to not try to align them, so this looks fine, but it would be clearer if the order of the errors would be different (see my other comments).

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 testMissingPositionalArgWithDefaults]
def f(x: int, y: str, z: bytes = b'default') -> None: ...

f("hello")
[builtins fixtures/primitives.pyi]
[out]
main:3: error: Missing positional argument "y" in call to "f"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you report this error after the 'Argument N to ...' errors, so that we'd report the errors in the same order as the arguments are in the call.

main:3: error: Argument 1 to "f" has incompatible type "str"; expected "int"

[case testMissingPositionalArgWithStarArgs]
def f(x: int, y: str, z: bytes, *args: int) -> None: ...

f("hello", b'x')
[builtins fixtures/primitives.pyi]
[out]
main:3: error: Missing positional argument "z" in call to "f"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case it's not clear what the idea error message would be, since the caller accepts *args. One option would be to report missing positional argument x, but maybe it could result in confusing messages in other contexts, so these messages are reasonable. However, I think it would better to show the 'Missing positional argument ...' after the other messages, so that the messages could be shown in a logical order.

Copy link
Collaborator Author

@KevinRK29 KevinRK29 Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I did try this, however I noticed how the errors in mypy work is that it orders them based on column.
"Missing positional argument 'y'" is in column 1 and "Argument 1 has incompatible type" is in column 3. So the "Missing positional" error was always sorted first.

I tried making it so that the context/column for this error to be the last one, but that also affects Too few arguments for "x" since they're in the same code block and moves that to the bottom.

But also the other thing is that it changed the column at which this error occurs, so for

[case testColumnInvalidArguments]
(f()) # E:2: Missing positional arguments "x", "y" in call to "f"
(f(y=1)) # E:6: Missing positional argument "x" in call to "f"

It becomes E:6 instead of E:2, where I feel E:2 might be more correct since it's pointing to f?
Anyways that was what I was seeing, and I wanted more thoughts before committing the change on whether this is fine or not.

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"