Back to Ruff

Call expression

crates/ty_python_semantic/resources/mdtest/call/function.md

0.15.1247.9 KB
Original Source

Call expression

Simple

py
def get_int() -> int:
    return 42

reveal_type(get_int())  # revealed: int

Gradual variadic parameters

py
from typing import Any

def accepts_anything(first: int, *args: Any, **kwargs: Any) -> None: ...
def accepts_only_gradual(*args: Any, **kwargs: Any) -> None: ...

accepts_anything(1, "one", object(), keyword=object())
accepts_anything("not an int")  # error: [invalid-argument-type]
accepts_only_gradual(1, "one", keyword=object())
accepts_only_gradual(**{1: "one"})  # error: [invalid-argument-type]

Async

py
async def get_int_async() -> int:
    return 42

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

Generic

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

reveal_type(get_int())  # revealed: int

Decorated

py
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

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

Potentially unbound function

py
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

<!-- snapshot-diagnostics -->

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 argument’s 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.

py
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. This is flagged with a different error code since (per the typing spec), this is likely a mistake from the user:

py
from typing import overload

# error: [invalid-legacy-positional-parameter]
def g(x: int, __y: str): ...

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

# The earlier `g` definition is shadowed here,
# but we still emit a diagnostic on the earlier definition
def g(): ...

Because the lint is a syntactic check, we emit it for each overload if multiple overloads violate the lint:

py
import tkinter
from typing import Callable, TypeVar, Any

@overload
def g2(x: int, __y: str): ...  # error: [invalid-legacy-positional-parameter]
@overload
def g2(x: str, __y: int): ...  # error: [invalid-legacy-positional-parameter]
def g2(x: str | int, __y: int | str): ...  # error: [invalid-legacy-positional-parameter]

T = TypeVar("T")

def copy_type(f: T) -> Callable[[Any], T]:
    return lambda x: x

# Naively iterating over the overloads using `.iter_overloads_and_implementation()` and/or
# using `.signature()` would cause us to panic on this function, because the overloads
# of this function's public signature are defined in `stdlib/tkinter/__init__.pyi` due to
# the decorator.
@copy_type(tkinter.Text.__init__)
def g3(x, *args: Any, **kwargs: Any) -> None: ...
def new_signature(): ...

# The check is able to "see through" the decorators and examines the original function's
# signature:
@copy_type(new_signature)
def g4(a, __b): ...  # error: [invalid-legacy-positional-parameter]

Parameters are also not understood as positional-only if they both start and end with __:

py
def h(__x__: str): ...

h(__x__="foo")

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

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

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

And self/cls are implicitly positional-only:

py
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: [invalid-legacy-positional-parameter]
    # `__new__` is a staticmethod, but the `cls` parameter works in the same way as the `cls`
    # parameter in a classmethod, and is always passed positionally at runtime,
    # We therefore understand both `cls` and `__x` here as positional-only; we do not
    # emit `[invalid-legacy-positional-parameter]` on the method.
    def __new__(cls, __x: int): ...

# error: [positional-only-parameter-as-kwarg]
C(42).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

py
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

py
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

py
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

toml
[environment]
python-version = "3.11"
py
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

toml
[environment]
python-version = "3.11"
py
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

py
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

py
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

py
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

py
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

toml
[environment]
python-version = "3.11"
py
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:

py
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]

A union of heterogeneous tuples provided to a variadic parameter:

py
# Test inspired by ecosystem code at:
# - <https://github.com/home-assistant/core/blob/bde4eb50111a72f9717fe73ee5929e50eb06911b/homeassistant/components/lovelace/websocket.py#L50-L59>
# - <https://github.com/pydata/xarray/blob/3572f4e70f2b12ef9935c1f8c3c1b74045d2a092/xarray/tests/test_groupby.py#L3058-L3059>

def f2(a: str, b: bool): ...
def f3(coinflip: bool):
    if coinflip:
        args = "foo", True
    else:
        args = "bar", False

    # revealed: tuple[Literal["foo"], Literal[True]] | tuple[Literal["bar"], Literal[False]]
    reveal_type(args)
    f2(*args)  # fine

    if coinflip:
        other_args = "foo", True
    else:
        other_args = "bar", (True,)

    # revealed: tuple[Literal["foo"], Literal[True]] | tuple[Literal["bar"], tuple[Literal[True]]]
    reveal_type(other_args)
    # error: [invalid-argument-type] "Argument to function `f2` is incorrect: Expected `bool`, found `Literal[True] | tuple[Literal[True]]`"
    f2(*other_args)

def f4(a=None, b=None, c=None, d=None, e=None): ...

my_args = ((1, 2), (3, 4), (5, 6))

for tup in my_args:
    f4(*tup, e=None)  # fine

my_other_args = (
    (1, 2, 3, 4, 5),
    (6, 7, 8, 9, 10),
)

for tup in my_other_args:
    # error: [parameter-already-assigned] "Multiple values provided for parameter `e` of function `f4`"
    f4(*tup, e=None)

Regression test for https://github.com/astral-sh/ty/issues/2734.

py
def f5(x: int | None = None, y: str = "") -> None: ...
def f6(flag: bool) -> None:
    args = () if flag else (1,)
    f5(*args)

def f7(x: int | None = None, y: str = "") -> None: ...
def f8(flag: bool) -> None:
    args = () if flag else ("bad",)
    f7(*args)  # error: [invalid-argument-type]

def f11(*args: int) -> None: ...
def f12(args: tuple[int] | int) -> None:
    f11(*args)  # error: [not-iterable]

def f13(a: int, b: int, c: str) -> None: ...
def f14(a: int, b: int, c: str, d: list[float], e: list[float]) -> None: ...
def f15(profile: bool, line: str) -> None:
    matcher = f13
    timings = []
    if profile:
        matcher = f14
        timings = [[0.0], [1.0], [2.0], [3.0]]
    matcher(1, 2, line, *timings[:2])

def f9(x: int = 0, y: str = "") -> None: ...
def f10(args: tuple[int, ...] | tuple[int, str]) -> None:
    # The variable-length element `int` from `tuple[int, ...]` unions with `str`
    # from `tuple[int, str]` at position 1, giving `int | str` for `y: str`.
    f9(*args)  # error: [invalid-argument-type]

def f18(x: int = 0, y: int = 0) -> None: ...
def f19(args: tuple[int, ...] | tuple[int, int]) -> None:
    f18(*args)

# Union variadic unpacking also works when the non-defaulted parameters are covered by
# the shortest union element, even if not all remaining parameters are defaulted.
def f16(a: int, b: int = 0, c: str = "") -> None: ...
def f17(x: tuple[int] | tuple[int, int]) -> None:
    f16(*x)

# Longer union elements must still be rejected when they would contribute
# extra positional arguments.
def f20(a: int, b: int) -> None: ...
def f21(x: tuple[int, int] | tuple[int, int, int]) -> None:
    f20(*x)  # error: [too-many-positional-arguments]

# Shorter union elements must also be rejected when they cannot provide a required
# positional argument.
def f22(a: int, b: int, c: int) -> None: ...
def f23(x: tuple[int, int] | tuple[int, int, int]) -> None:
    f22(*x)  # error: [missing-argument]

# Later positional arguments must not be allowed to "slide left" when a longer
# union member would still bind an incompatible tuple element. We currently
# handle this conservatively, so this still reports the broader iterator-based
# family of errors.
def f24(a: int, b: int, c: int = 0) -> None: ...
def f25(x: tuple[int] | tuple[int, str]) -> None:
    # error: [invalid-argument-type]
    # error: [invalid-argument-type]
    # error: [too-many-positional-arguments]
    f24(*x, 1)  # error: [invalid-argument-type]

Mixed argument and parameter containing variadic

toml
[environment]
python-version = "3.11"
py
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

Variable-length unpacking with explicit keyword arguments

When a variable-length iterable (like a list) is unpacked and followed by explicit keyword arguments, the variadic unpacking should not greedily consume parameters that have explicit keyword bindings. This prevents false positive "parameter already assigned" errors.

Regression test for https://github.com/astral-sh/ty/issues/1584.

py
from typing import TypedDict

def f(a: str, b: str, c: float) -> None: ...

# Explicit keyword argument takes precedence over variable-length variadic expansion.
# The list unpacking should only fill `a` and `b`, leaving `c` for the keyword argument.
def _(args: list[str]) -> None:
    f(*args, c=1.0)

# Fixed-length tuple unpacking with keyword argument also works correctly.
def _(args: tuple[str, str]) -> None:
    f(*args, c=1.0)

# But, with a fixed-length tuple that is too long, we get the expected error.
def _(args: tuple[str, str, str]) -> None:
    # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int | float`, found `str`"
    # error: [parameter-already-assigned] "Multiple values provided for parameter `c` of function `f`"
    f(*args, c=1.0)

However, when there's no explicit keyword argument, the behavior remains conservative:

py
# Positional argument after variable-length variadic is ambiguous
def _(args: list[str]) -> None:
    # error: [invalid-argument-type]
    # error: [too-many-positional-arguments]
    f(*args, 1.0)

# Multiple variable-length variadics are also ambiguous
def _(args1: list[str], args2: list[float]) -> None:
    # error: [invalid-argument-type]
    f(*args1, *args2)

# Keyword variadic (**dict) with unknown keys is also ambiguous
def _(args: list[str], kwargs: dict[str, float]) -> None:
    # error: [invalid-argument-type]
    f(*args, **kwargs)

# Keyword variadic with TypedDict has known keys but is still handled conservatively
# but we could possibly improve this in the future.
class CKwargs(TypedDict):
    c: float

def _(args: list[str]) -> None:
    # error: [invalid-argument-type]
    # error: [parameter-already-assigned]
    f(*args, **CKwargs(c=1.0))

Keyword argument, positional-or-keyword parameter

py
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

py
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

py
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

py
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

Diagnostics for union types where the union is not assignable

<!-- snapshot-diagnostics -->
py
from typing import Sized

class Foo: ...
class Bar: ...
class Baz: ...

def f(x: Sized): ...
def g(
    a: str | Foo,
    b: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo,
    c: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar,
    d: list[str] | str | dict[str, str] | tuple[str, ...] | bytes | frozenset[str] | set[str] | Foo | Bar | Baz,
):
    f(a)  # error: [invalid-argument-type]
    f(b)  # error: [invalid-argument-type]
    f(c)  # error: [invalid-argument-type]
    f(d)  # error: [invalid-argument-type]

Too many positional arguments

One too many

py
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

py
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

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

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

Multiple keyword arguments map to keyword variadic parameter

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

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

Missing arguments

No defaults or variadic

py
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

py
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

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

reveal_type(f())  # revealed: int

With variadic

py
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

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

reveal_type(f())  # revealed: int

Keywords argument is not required

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

reveal_type(f())  # revealed: int

Multiple

py
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

py
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

py
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

py
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

py
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"
# error: [invalid-argument-type] "Argument to function `static_assert` is incorrect: Expected `LiteralString | None`, found `Literal[2]`"
static_assert(True, 2, 3)

len

py
# 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

py
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

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

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

Single parameter

py
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

py
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

py
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

py
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

py
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

py
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

py
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

py
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.

py
from collections.abc import Mapping

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

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

class MappingSubclass(Mapping[int, int]):
    def __iter__(self): ...
    def __len__(self): ...
    def __getitem__(self, key): ...

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.

py
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.

py
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

py
from collections.abc import Mapping

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

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

class MappingSubclass(Mapping[str, int]):
    def __iter__(self): ...
    def __len__(self): ...
    def __getitem__(self, key) -> int:
        return 42

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

py
from ty_extensions import Unknown

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

Not a mapping

py
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())

Not a mapping with overloaded function

When **kwargs with a non-mapping type is passed to an overloaded function, the error should report the specific mapping type error.

overloaded.pyi:

pyi
from typing import overload

@overload
def f(x: int, **kwargs: int) -> int: ...
@overload
def f(x: str, **kwargs: str) -> str: ...
py
from overloaded import f

# error: [invalid-argument-type] "Argument expression after ** must be a mapping type: Found `None`"
f(1, **None)

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

Generic

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

py
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.

py
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

Non-iterable variadic argument

A starred argument must be iterable. If it is not, an error should be reported.

py
def some_fn(a: int):
    pass

# error: [not-iterable] "Object of type `None` is not iterable"
some_fn(*None)

This also applies when the type might not be iterable:

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

def _(x: int | list[int]):
    # error: [not-iterable] "Object of type `int | list[int]` may not be iterable"
    f(*x)

Non-iterable variadic argument with overloaded functions

overloaded.pyi:

pyi
from typing import overload

@overload
def foo(a: int) -> tuple[int]: ...
@overload
def foo(a: int, b: int) -> tuple[int, int]: ...
py
from overloaded import foo

# error: [not-iterable] "Object of type `None` is not iterable"
foo(*None)

def _(arg: int):
    # error: [not-iterable] "Object of type `int` is not iterable"
    foo(*arg)

Union variadic unpacking with explicit keyword arguments

When a union type containing variable-length elements (like Unknown) is unpacked as *args, the variadic expansion should not greedily consume optional positional parameters that are also provided as explicit keyword arguments.

py
from ty_extensions import Unknown

def f(a: int = 0, b: int = 0, c: int = 0, fmt: str | None = None) -> None: ...
def _(args: "Unknown | tuple[int, int, int]"):
    f(*args, fmt="{key}")  # fine

def g(a: int, b: int = 0, c: int = 0) -> None: ...
def _(args: tuple[int, int] | tuple[int, int, int]):
    g(*args, c=1)  # error: [parameter-already-assigned]

Variadic unpacking should stop at max known arity

When unpacking (a union of) fixed-length tuples, variadic matching should stop once the known positions are exhausted. Otherwise, optional positional parameters can be incorrectly treated as already assigned, causing false positives for **kwargs.

(This test uses **kwargs unpacking of a TypedDict instead of the simpler c=1 keyword argument, because c=1 is a known keyword argument and we always prevent unpacking *args over an explicitly-provided keyword argument. The case shown here, without the explicit keyword argument, requires instead that we use our knowledge of the tuple length to prevent over-unpacking.)

py
from typing import TypedDict

class CKwargs(TypedDict):
    c: int

def f(a: int = 0, b: int = 0, c: int = 0) -> None: ...
def _(args_tuple: tuple[int, int], args_union: tuple[int] | tuple[int, int], kwargs: CKwargs) -> None:
    f(*args_tuple, **kwargs)  # fine
    f(*args_union, **kwargs)  # fine