ruff/crates/ty_python_semantic/resources/mdtest/call/function.md
Dhruv Manilawala 35ed55ec8c
[ty] Filter overloads using variadic parameters (#20547)
## Summary

Closes: https://github.com/astral-sh/ty/issues/551

This PR adds support for step 4 of the overload call evaluation
algorithm which states that:

> If the argument list is compatible with two or more overloads,
determine whether one or more of the overloads has a variadic parameter
(either `*args` or `**kwargs`) that maps to a corresponding argument
that supplies an indeterminate number of positional or keyword
arguments. If so, eliminate overloads that do not have a variadic
parameter.

And, with that, the overload call evaluation algorithm has been
implemented completely end to end as stated in the typing spec.

## Test Plan

Expand the overload call test suite.
2025-09-25 14:58:00 +00:00

35 KiB
Raw Permalink Blame History

Call expression

Simple

def get_int() -> int:
    return 42

reveal_type(get_int())  # revealed: int

Async

async def get_int_async() -> int:
    return 42

reveal_type(get_int_async())  # revealed: CoroutineType[Any, Any, int]

Generic

[environment]
python-version = "3.12"
def get_int[T]() -> int:
    return 42

reveal_type(get_int())  # revealed: int

Decorated

from typing import Callable

def foo() -> int:
    return 42

def decorator(func) -> Callable[[], int]:
    return foo

@decorator
def bar() -> str:
    return "bar"

reveal_type(bar())  # revealed: int

Invalid callable

nonsense = 123
x = nonsense()  # error: "Object of type `Literal[123]` is not callable"

Potentially unbound function

def _(flag: bool):
    if flag:
        def foo() -> int:
            return 42
    # error: [possibly-unresolved-reference]
    reveal_type(foo())  # revealed: int

PEP-484 convention for positional-only parameters

PEP 570, introduced in Python 3.8, added dedicated Python syntax for denoting positional-only parameters (the / in a function signature). However, functions implemented in C were able to have positional-only parameters prior to Python 3.8 (there was just no syntax for expressing this at the Python level).

Stub files describing functions implemented in C nonetheless needed a way of expressing that certain parameters were positional-only. In the absence of dedicated Python syntax, PEP 484 described a convention that type checkers were expected to understand:

Some functions are designed to take their arguments only positionally, and expect their callers never to use the arguments name to provide that argument by keyword. All arguments with names beginning with __ are assumed to be positional-only, except if their names also end with __.

While this convention is now redundant (following the implementation of PEP 570), many projects still continue to use the old convention, so it is supported by ty as well.

def f(__x: int): ...

f(1)
# error: [positional-only-parameter-as-kwarg]
f(__x=1)

But not if they follow a non-positional-only parameter:

def g(x: int, __y: str): ...

g(x=1, __y="foo")

And also not if they both start and end with __:

def h(__x__: str): ...

h(__x__="foo")

And if any parameters use the new PEP-570 convention, the old convention does not apply:

def i(x: str, /, __y: int): ...

i("foo", __y=42)  # fine

And self/cls are implicitly positional-only:

class C:
    def method(self, __x: int): ...
    @classmethod
    def class_method(cls, __x: str): ...
    # (the name of the first parameter is irrelevant;
    # a staticmethod works the same as a free function in the global scope)
    @staticmethod
    def static_method(self, __x: int): ...

# error: [positional-only-parameter-as-kwarg]
C().method(__x=1)
# error: [positional-only-parameter-as-kwarg]
C.class_method(__x="1")
C.static_method("x", __x=42)  # fine

Splatted arguments

Unknown argument length

def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...

# Test all of the above with a number of different splatted argument types

def _(args: list[int]) -> None:
    takes_zero(*args)
    takes_one(*args)
    takes_two(*args)
    takes_two(*b"ab")
    takes_two(*b"abc")  # error: [too-many-positional-arguments]
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: tuple[int, ...]) -> None:
    takes_zero(*args)
    takes_one(*args)
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

Fixed-length tuple argument

def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...

# Test all of the above with a number of different splatted argument types

def _(args: tuple[int]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)
    takes_two(*args)  # error: [missing-argument]
    takes_two_positional_only(*args)  # error: [missing-argument]
    takes_two_different(*args)  # error: [missing-argument]
    takes_two_different_positional_only(*args)  # error: [missing-argument]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)  # error: [missing-argument]
    takes_at_least_two_positional_only(*args)  # error: [missing-argument]

def _(args: tuple[int, int]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: tuple[int, str]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)  # error: [invalid-argument-type]
    takes_two_positional_only(*args)  # error: [invalid-argument-type]
    takes_two_different(*args)
    takes_two_different_positional_only(*args)
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)  # error: [invalid-argument-type]
    takes_at_least_two_positional_only(*args)  # error: [invalid-argument-type]

Subclass of fixed-length tuple argument

def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...

# Test all of the above with a number of different splatted argument types

class SingleElementTuple(tuple[int]): ...

def _(args: SingleElementTuple) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]

    takes_one(*args)

    takes_two(*args)  # error: [missing-argument]
    takes_two_positional_only(*args)  # error: [missing-argument]

    takes_two_different(*args)  # error: [missing-argument]
    takes_two_different_positional_only(*args)  # error: [missing-argument]

    takes_at_least_zero(*args)
    takes_at_least_one(*args)

    takes_at_least_two(*args)  # error: [missing-argument]
    takes_at_least_two_positional_only(*args)  # error: [missing-argument]

class TwoElementIntTuple(tuple[int, int]): ...

def _(args: TwoElementIntTuple) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

class IntStrTuple(tuple[int, str]): ...

def _(args: IntStrTuple) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]

    takes_one(*args)  # error: [too-many-positional-arguments]

    # error: [invalid-argument-type]
    takes_two(*args)
    # error: [invalid-argument-type]
    takes_two_positional_only(*args)

    takes_two_different(*args)
    takes_two_different_positional_only(*args)
    takes_at_least_zero(*args)
    takes_at_least_one(*args)

    # error: [invalid-argument-type]
    takes_at_least_two(*args)
    # error: [invalid-argument-type]
    takes_at_least_two_positional_only(*args)

Mixed tuple argument

[environment]
python-version = "3.11"
def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...

# Test all of the above with a number of different splatted argument types

def _(args: tuple[int, *tuple[int, ...]]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: tuple[int, *tuple[str, ...]]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)
    takes_two(*args)  # error: [invalid-argument-type]
    takes_two_positional_only(*args)  # error: [invalid-argument-type]
    takes_two_different(*args)
    takes_two_different_positional_only(*args)
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)  # error: [invalid-argument-type]
    takes_at_least_two_positional_only(*args)  # error: [invalid-argument-type]

def _(args: tuple[int, int, *tuple[int, ...]]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: tuple[int, int, *tuple[str, ...]]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: tuple[int, *tuple[int, ...], int]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: tuple[int, *tuple[str, ...], int]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)  # error: [invalid-argument-type]
    takes_two_positional_only(*args)  # error: [invalid-argument-type]
    takes_two_different(*args)
    takes_two_different_positional_only(*args)
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)  # error: [invalid-argument-type]
    takes_at_least_two_positional_only(*args)  # error: [invalid-argument-type]

Subclass of mixed tuple argument

[environment]
python-version = "3.11"
def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...

# Test all of the above with a number of different splatted argument types

class IntStarInt(tuple[int, *tuple[int, ...]]): ...

def _(args: IntStarInt) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

class IntStarStr(tuple[int, *tuple[str, ...]]): ...

def _(args: IntStarStr) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]

    takes_one(*args)

    # error: [invalid-argument-type]
    takes_two(*args)
    # error: [invalid-argument-type]
    takes_two_positional_only(*args)

    takes_two_different(*args)
    takes_two_different_positional_only(*args)

    takes_at_least_zero(*args)

    takes_at_least_one(*args)

    # error: [invalid-argument-type]
    takes_at_least_two(*args)
    # error: [invalid-argument-type]
    takes_at_least_two_positional_only(*args)

class IntIntStarInt(tuple[int, int, *tuple[int, ...]]): ...

def _(args: IntIntStarInt) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

class IntIntStarStr(tuple[int, int, *tuple[str, ...]]): ...

def _(args: IntIntStarStr) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]

    takes_one(*args)  # error: [too-many-positional-arguments]

    takes_two(*args)
    takes_two_positional_only(*args)

    # error: [invalid-argument-type]
    takes_two_different(*args)
    # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)

    takes_at_least_zero(*args)

    takes_at_least_one(*args)

    takes_at_least_two(*args)

    takes_at_least_two_positional_only(*args)

class IntStarIntInt(tuple[int, *tuple[int, ...], int]): ...

def _(args: IntStarIntInt) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

class IntStarStrInt(tuple[int, *tuple[str, ...], int]): ...

def _(args: IntStarStrInt) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]

    takes_one(*args)  # error: [too-many-positional-arguments]

    # error: [invalid-argument-type]
    takes_two(*args)
    # error: [invalid-argument-type]
    takes_two_positional_only(*args)

    takes_two_different(*args)
    takes_two_different_positional_only(*args)

    takes_at_least_zero(*args)

    takes_at_least_one(*args)

    # error: [invalid-argument-type]
    takes_at_least_two(*args)

    # error: [invalid-argument-type]
    takes_at_least_two_positional_only(*args)

String argument

from typing import Literal

def takes_zero() -> None: ...
def takes_one(x: str) -> None: ...
def takes_two(x: str, y: str) -> None: ...
def takes_two_positional_only(x: str, y: str, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: str, *args) -> None: ...
def takes_at_least_two(x: str, y: str, *args) -> None: ...
def takes_at_least_two_positional_only(x: str, y: str, /, *args) -> None: ...

# Test all of the above with a number of different splatted argument types

def _(args: Literal["a"]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)
    takes_two(*args)  # error: [missing-argument]
    takes_two_positional_only(*args)  # error: [missing-argument]
    # error: [invalid-argument-type]
    # error: [missing-argument]
    takes_two_different(*args)
    # error: [invalid-argument-type]
    # error: [missing-argument]
    takes_two_different_positional_only(*args)
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)  # error: [missing-argument]
    takes_at_least_two_positional_only(*args)  # error: [missing-argument]

def _(args: Literal["ab"]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: Literal["abc"]) -> None:
    takes_zero(*args)  # error: [too-many-positional-arguments]
    takes_one(*args)  # error: [too-many-positional-arguments]
    takes_two(*args)  # error: [too-many-positional-arguments]
    takes_two_positional_only(*args)  # error: [too-many-positional-arguments]
    # error: [invalid-argument-type]
    # error: [too-many-positional-arguments]
    takes_two_different(*args)
    # error: [invalid-argument-type]
    # error: [too-many-positional-arguments]
    takes_two_different_positional_only(*args)
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

def _(args: str) -> None:
    takes_zero(*args)
    takes_one(*args)
    takes_two(*args)
    takes_two_positional_only(*args)
    takes_two_different(*args)  # error: [invalid-argument-type]
    takes_two_different_positional_only(*args)  # error: [invalid-argument-type]
    takes_at_least_zero(*args)
    takes_at_least_one(*args)
    takes_at_least_two(*args)
    takes_at_least_two_positional_only(*args)

Wrong argument type

Positional argument, positional-or-keyword parameter

def f(x: int) -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo"))  # revealed: int

Positional argument, positional-only parameter

def f(x: int, /) -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo"))  # revealed: int

Positional argument, variadic parameter

def f(*args: int) -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo"))  # revealed: int

Variadic argument, variadic parameter

[environment]
python-version = "3.11"
def f(*args: int) -> int:
    return 1

def _(args: list[str]) -> None:
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
    reveal_type(f(*args))  # revealed: int

Considering a few different shapes of tuple for the splatted argument:

def f1(*args: str): ...
def _(
    args1: tuple[str, ...],
    args2: tuple[str, *tuple[str, ...]],
    args3: tuple[str, *tuple[str, ...], str],
    args4: tuple[int, *tuple[str, ...]],
    args5: tuple[int, *tuple[str, ...], str],
    args6: tuple[*tuple[str, ...], str],
    args7: tuple[*tuple[str, ...], int],
    args8: tuple[int, *tuple[str, ...], int],
    args9: tuple[str, *tuple[str, ...], int],
    args10: tuple[str, *tuple[int, ...], str],
):
    f1(*args1)
    f1(*args2)
    f1(*args3)
    f1(*args4)  # error: [invalid-argument-type]
    f1(*args5)  # error: [invalid-argument-type]
    f1(*args6)
    f1(*args7)  # error: [invalid-argument-type]

    # The reason for two errors here is because of the two fixed elements in the tuple of `args8`
    # which are both `int`
    # error: [invalid-argument-type]
    # error: [invalid-argument-type]
    f1(*args8)

    f1(*args9)  # error: [invalid-argument-type]
    f1(*args10)  # error: [invalid-argument-type]

Mixed argument and parameter containing variadic

[environment]
python-version = "3.11"
def f(x: int, *args: str) -> int:
    return 1

def _(
    args1: list[int],
    args2: tuple[int],
    args3: tuple[int, int],
    args4: tuple[int, ...],
    args5: tuple[int, *tuple[str, ...]],
    args6: tuple[int, int, *tuple[str, ...]],
) -> None:
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    reveal_type(f(*args1))  # revealed: int

    # This shouldn't raise an error because the unpacking doesn't match the variadic parameter.
    reveal_type(f(*args2))  # revealed: int

    # But, this should because the second tuple element is not assignable.
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    reveal_type(f(*args3))  # revealed: int

    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    reveal_type(f(*args4))  # revealed: int

    # The first element of the tuple matches the required argument;
    # all subsequent elements match the variadic argument
    reveal_type(f(*args5))  # revealed: int

    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    reveal_type(f(*args6))  # revealed: int

Keyword argument, positional-or-keyword parameter

def f(x: int) -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo"))  # revealed: int

Keyword argument, keyword-only parameter

def f(*, x: int) -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo"))  # revealed: int

Keyword argument, keywords parameter

def f(**kwargs: int) -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo"))  # revealed: int

Correctly match keyword out-of-order

def f(x: int = 1, y: str = "foo") -> int:
    return 1

# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `Literal[2]`"
# error: 20 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["bar"]`"
reveal_type(f(y=2, x="bar"))  # revealed: int

Too many positional arguments

One too many

def f() -> int:
    return 1

# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
reveal_type(f("foo"))  # revealed: int

Two too many

def f() -> int:
    return 1

# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
reveal_type(f("foo", "bar"))  # revealed: int

No too-many-positional if variadic is taken

def f(*args: int) -> int:
    return 1

reveal_type(f(1, 2, 3))  # revealed: int

Multiple keyword arguments map to keyword variadic parameter

def f(**kwargs: int) -> int:
    return 1

reveal_type(f(foo=1, bar=2))  # revealed: int

Missing arguments

No defaults or variadic

def f(x: int) -> int:
    return 1

# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f())  # revealed: int

With default

def f(x: int, y: str = "foo") -> int:
    return 1

# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f())  # revealed: int

Defaulted argument is not required

def f(x: int = 1) -> int:
    return 1

reveal_type(f())  # revealed: int

With variadic

def f(x: int, *y: str) -> int:
    return 1

# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f())  # revealed: int

Variadic argument is not required

def f(*args: int) -> int:
    return 1

reveal_type(f())  # revealed: int

Keywords argument is not required

def f(**kwargs: int) -> int:
    return 1

reveal_type(f())  # revealed: int

Multiple

def f(x: int, y: int) -> int:
    return 1

# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
reveal_type(f())  # revealed: int

Unknown argument

def f(x: int) -> int:
    return 1

# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
reveal_type(f(x=1, y=2))  # revealed: int

Parameter already assigned

def f(x: int) -> int:
    return 1

# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2))  # revealed: int

Special functions

Some functions require special handling in type inference. Here, we make sure that we still emit proper diagnostics in case of missing or superfluous arguments.

reveal_type

from typing_extensions import reveal_type

# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type()

# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2)

static_assert

from ty_extensions import static_assert

# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
static_assert()

# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
static_assert(True, 2, 3)

len

# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
len()

# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
len([], 1)

Type property predicates

from ty_extensions import is_subtype_of

# error: [missing-argument]
is_subtype_of()

# error: [missing-argument]
is_subtype_of(int)

# error: [too-many-positional-arguments]
is_subtype_of(int, int, int)

# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)

Keywords argument

A double-starred argument (**kwargs) can be used to pass an argument that implements the mapping protocol. This is matched against any of the unmatched standard (positional or keyword), keyword-only, and keywords (**kwargs) parameters.

Empty

def empty() -> None: ...
def _(kwargs: dict[str, int]) -> None:
    empty(**kwargs)

empty(**{})
empty(**dict())

Single parameter

from typing_extensions import TypedDict

def f(**kwargs: int) -> None: ...

class Foo(TypedDict):
    a: int
    b: int

def _(kwargs: dict[str, int]) -> None:
    f(**kwargs)

f(**{"foo": 1})
f(**dict(foo=1))
f(**Foo(a=1, b=2))

Positional-only and variadic parameters

def f1(a: int, b: int, /) -> None: ...
def f2(*args: int) -> None: ...
def _(kwargs: dict[str, int]) -> None:
    # error: [missing-argument] "No arguments provided for required parameters `a`, `b` of function `f1`"
    f1(**kwargs)

    # This doesn't raise an error because `*args` is an optional parameter and `**kwargs` can be empty.
    f2(**kwargs)

Standard parameters

from typing_extensions import TypedDict

class Foo(TypedDict):
    a: int
    b: int

def f(a: int, b: int) -> None: ...
def _(kwargs: dict[str, int]) -> None:
    f(**kwargs)

f(**{"a": 1, "b": 2})
f(**dict(a=1, b=2))
f(**Foo(a=1, b=2))

Keyword-only parameters

from typing_extensions import TypedDict

class Foo(TypedDict):
    a: int
    b: int

def f(*, a: int, b: int) -> None: ...
def _(kwargs: dict[str, int]) -> None:
    f(**kwargs)

f(**{"a": 1, "b": 2})
f(**dict(a=1, b=2))
f(**Foo(a=1, b=2))

Multiple keywords argument

def f(**kwargs: int) -> None: ...
def _(kwargs1: dict[str, int], kwargs2: dict[str, int], kwargs3: dict[str, str], kwargs4: dict[int, list]) -> None:
    f(**kwargs1, **kwargs2)
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
    f(**kwargs1, **kwargs3)
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
    # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `list[Unknown]`"
    f(**kwargs3, **kwargs4)

Keyword-only after keywords

class B: ...

def f(*, a: int, b: B, **kwargs: int) -> None: ...
def _(kwargs: dict[str, int]):
    # Make sure that the `b` argument is not being matched against `kwargs` by passing an integer
    # instead of the annotated type which should raise an
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `B`, found `Literal[2]`"
    f(a=1, **kwargs, b=2)

Mixed parameter kind

def f1(*, a: int, b: int, **kwargs: int) -> None: ...
def f2(a: int, *, b: int, **kwargs: int) -> None: ...
def f3(a: int, /, *args: int, b: int, **kwargs: int) -> None: ...
def _(kwargs1: dict[str, int], kwargs2: dict[str, str]):
    f1(**kwargs1)
    f2(**kwargs1)
    f3(1, **kwargs1)

TypedDict

from typing_extensions import NotRequired, TypedDict

class Foo1(TypedDict):
    a: int
    b: str

class Foo2(TypedDict):
    a: int
    b: NotRequired[str]

def f(**kwargs: int) -> None: ...

# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
f(**Foo1(a=1, b="b"))
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `str`"
f(**Foo2(a=1))

Keys must be strings

The keys of the mapping passed to a double-starred argument must be strings.

from collections.abc import Mapping

def f(**kwargs: int) -> None: ...

class DictSubclass(dict[int, int]): ...
class MappingSubclass(Mapping[int, int]): ...

class MappingProtocol:
    def keys(self) -> list[int]:
        return [1]

    def __getitem__(self, key: int) -> int:
        return 1

def _(kwargs: dict[int, int]) -> None:
    # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
    f(**kwargs)

# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**DictSubclass())
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**MappingSubclass())
# error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `int`"
f(**MappingProtocol())

The key can also be a custom type that inherits from str.

class SubStr(str): ...
class SubInt(int): ...

def _(kwargs1: dict[SubStr, int], kwargs2: dict[SubInt, int]) -> None:
    f(**kwargs1)
    # error: [invalid-argument-type] "Argument expression after ** must be a mapping with `str` key type: Found `SubInt`"
    f(**kwargs2)

Or, it can be a type that is assignable to str.

from typing import Any
from ty_extensions import Unknown

def _(kwargs1: dict[Any, int], kwargs2: dict[Unknown, int]) -> None:
    f(**kwargs1)
    f(**kwargs2)

Invalid value type

from collections.abc import Mapping

def f(**kwargs: str) -> None: ...

class DictSubclass(dict[str, int]): ...
class MappingSubclass(Mapping[str, int]): ...

class MappingProtocol:
    def keys(self) -> list[str]:
        return ["foo"]

    def __getitem__(self, key: str) -> int:
        return 1

def _(kwargs: dict[str, int]) -> None:
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    f(**kwargs)
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    f(**DictSubclass())
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    f(**MappingSubclass())
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `int`"
    f(**MappingProtocol())

Unknown type

from ty_extensions import Unknown

def f(**kwargs: int) -> None: ...
def _(kwargs: Unknown):
    f(**kwargs)

Not a mapping

def f(**kwargs: int) -> None: ...

class A: ...

class InvalidMapping:
    def keys(self) -> A:
        return A()

    def __getitem__(self, key: str) -> int:
        return 1

def _(kwargs: dict[str, int] | int):
    # error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `dict[str, int] | int`"
    f(**kwargs)
    # error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `InvalidMapping`"
    f(**InvalidMapping())

Generic

For a generic keywords parameter, the type variable should be specialized to the value type of the mapping.

from typing import TypeVar

_T = TypeVar("_T")

def f(**kwargs: _T) -> _T:
    return kwargs["a"]

def _(kwargs: dict[str, int]) -> None:
    reveal_type(f(**kwargs))  # revealed: int

For a TypedDict, the type variable should be specialized to the union of all value types.

from typing import TypeVar
from typing_extensions import TypedDict

_T = TypeVar("_T")

class Foo(TypedDict):
    a: int
    b: str

def f(**kwargs: _T) -> _T:
    return kwargs["a"]

reveal_type(f(**Foo(a=1, b="b")))  # revealed: int | str