Callables¶
Terminology¶
In this section, and throughout this specification, the term “parameter” refers to a named symbol associated with a function that receives the value of an argument (or multiple arguments) passed to the function. The term “argument” refers to a value passed to a function when it is called.
Python supports five kinds of parameters: positional-only, keyword-only,
standard (positional or keyword), variadic positional (*args), and
variadic keyword (**kwargs). Positional-only parameters can accept only
positional arguments, and keyword-only parameters can accept only keyword
arguments. Standard parameters can accept either positional or keyword
arguments. Parameters of the form *args and **kwargs are variadic
and accept zero or more positional or keyword arguments, respectively.
In the example below, a is a positional-only parameter, b is
a standard (positional or keyword) parameter, c is a keyword-only parameter,
args is a variadic parameter that accepts additional positional arguments,
and kwargs is a variadic parameter that accepts additional keyword
arguments:
def func(a: str, /, b, *args, c=0, **kwargs) -> None:
...
A function’s “signature” refers to its list of parameters (including
the name, kind, optional declared type, and whether it has a default
argument value) plus its return type. The signature of the function above is
(a: str, /, b, *args, c=..., **kwargs) -> None. Note that the default
argument value for parameter c is denoted as ... here because the
presence of a default value is considered part of the signature, but the
specific value is not.
The term “input signature” is used to refer to only the parameters of a function.
In the example above, the input signature is (a: str, /, b, *args, c=..., **kwargs).
Positional-only parameters¶
Within a function signature, positional-only parameters are separated from
non-positional-only parameters by a single forward slash (‘/’). This
forward slash does not represent a parameter, but rather a delimiter. In this
example, a is a positional-only parameter and b is a standard
(positional or keyword) parameter:
def func(a: int, /, b: int) -> None:
...
func(1, 2) # OK
func(1, b=2) # OK
func(a=1, b=2) # Error
Support for the / delimiter was introduced in Python 3.8 (PEP 570).
For compatibility with earlier versions of Python, the type system also
supports specifying positional-only parameters using a double leading
underscore.
Default argument values¶
In certain cases, it may be desirable to omit the default argument value for a parameter. Examples include function definitions in stub files or methods within a protocol or abstract base class. In such cases, the default value may be given as an ellipsis. For example:
def func(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...
If a non-ellipsis default value is present and its type can be statically evaluated, a type checker should verify that this type is assignable to the declared parameter’s type:
def func(x: int = 0): ... # OK
def func(x: int | None = None): ... # OK
def func(x: int = 0.0): ... # Error
def func(x: int = None): ... # Error
Annotating *args and **kwargs¶
At runtime, the type of a variadic positional parameter (*args) is a
tuple, and the type of a variadic keyword parameter (**kwargs) is a
dict. However, when annotating these parameters, the type annotation
refers to the type of items within the tuple or dict (unless
Unpack is used).
Therefore, the definition:
def func(*args: str, **kwargs: int): ...
means that the function accepts an arbitrary number of positional arguments
of type str and an arbitrary number of keyword arguments of type int.
For example, all of the following represent function calls with valid
arguments:
func('a', 'b', 'c')
func(x=1, y=2)
func('', z=0)
In the body of function func, the type of parameter args is
tuple[str, ...], and the type of parameter kwargs is dict[str, int].
Unpack for keyword arguments¶
typing.Unpack has two use cases in the type system:
As introduced by PEP 646, a backward-compatible form for certain operations involving variadic generics. See the section on
TypeVarTuplefor details.As introduced by PEP 692, a way to annotate the
**kwargsof a function.
This second usage is described in this section. The following example:
from typing import TypedDict, Unpack
class Movie(TypedDict):
name: str
year: int
def foo(**kwargs: Unpack[Movie]) -> None: ...
means that the **kwargs comprise two keyword arguments specified by
Movie (i.e. a name keyword of type str and a year keyword of
type int). This indicates that the function should be called as follows:
kwargs: Movie = {"name": "Life of Brian", "year": 1979}
foo(**kwargs) # OK!
foo(name="The Meaning of Life", year=1983) # OK!
When Unpack is used, type checkers treat kwargs inside the
function body as a TypedDict:
def foo(**kwargs: Unpack[Movie]) -> None:
assert_type(kwargs, Movie) # OK!
Function calls with standard dictionaries¶
Passing a dictionary of type dict[str, object] as a **kwargs argument
to a function that has **kwargs annotated with Unpack must generate a
type checker error. On the other hand, the behavior for functions using
standard, untyped dictionaries can depend on the type checker. For example:
def func(**kwargs: Unpack[Movie]) -> None: ...
movie: dict[str, object] = {"name": "Life of Brian", "year": 1979}
func(**movie) # WRONG! Movie is of type dict[str, object]
typed_movie: Movie = {"name": "The Meaning of Life", "year": 1983}
func(**typed_movie) # OK!
another_movie = {"name": "Life of Brian", "year": 1979}
func(**another_movie) # Depends on the type checker.
Keyword collisions¶
A TypedDict that is used to type **kwargs could potentially contain
keys that are already defined in the function’s signature. If the duplicate
name is a standard parameter, an error should be reported by type checkers.
If the duplicate name is a positional-only parameter, no errors should be
generated. For example:
def foo(name, **kwargs: Unpack[Movie]) -> None: ... # WRONG! "name" will
# always bind to the
# first parameter.
def foo(name, /, **kwargs: Unpack[Movie]) -> None: ... # OK! "name" is a
# positional-only parameter,
# so **kwargs can contain
# a "name" keyword.
Required and non-required items¶
Items in a TypedDict may be either required or non-required.
When using a TypedDict to type **kwargs all of the required and
non-required keys should correspond to required and non-required function
keyword parameters. Therefore, if a required key is not provided by the
caller, then an error must be reported by type checkers.
Read-only items¶
TypedDict items may also be read-only. Marking one or more of the items of a TypedDict
used to type **kwargs as read-only will have no effect on the type signature of the method.
However, it will prevent the item from being modified in the body of the function:
class Args(TypedDict):
key1: int
key2: str
class ReadOnlyArgs(TypedDict):
key1: ReadOnly[int]
key2: ReadOnly[str]
class Function(Protocol):
def __call__(self, **kwargs: Unpack[Args]) -> None: ...
def impl(**kwargs: Unpack[ReadOnlyArgs]) -> None:
kwargs["key1"] = 3 # Type check error: key1 is readonly
fn: Function = impl # Accepted by type checker: function signatures are identical
Extra items¶
If the TypedDict used for annotating **kwargs is defined to allow
extra items, arbitrary additional keyword arguments of the right
type may be passed to the function:
class MovieNoExtra(TypedDict):
name: str
class MovieExtra(TypedDict, extra_items=int):
name: str
def f(**kwargs: Unpack[MovieNoExtra]) -> None: ...
def g(**kwargs: Unpack[MovieExtra]) -> None: ...
# Should be equivalent to:
def f(*, name: str) -> None: ...
def g(*, name: str, **kwargs: int) -> None: ...
f(name="No Country for Old Men", year=2007) # Not OK. Unrecognized item
g(name="No Country for Old Men", year=2007) # OK
Assignment¶
Assignments of a function typed with **kwargs: Unpack[Movie] and another
callable type should pass type checking only for the scenarios described below.
Source and destination contain **kwargs¶
Both destination and source functions have a **kwargs: Unpack[TypedDict]
parameter and the destination function’s TypedDict is assignable to
the source function’s TypedDict and the rest of the parameters are
assignable:
class Animal(TypedDict):
name: str
class Dog(Animal):
breed: str
def accept_animal(**kwargs: Unpack[Animal]): ...
def accept_dog(**kwargs: Unpack[Dog]): ...
accept_dog = accept_animal # OK! Expression of type Dog can be
# assigned to a variable of type Animal.
accept_animal = accept_dog # WRONG! Expression of type Animal
# cannot be assigned to a variable of type Dog.
Source contains **kwargs and destination doesn’t¶
The destination callable doesn’t contain **kwargs, the source callable
contains **kwargs: Unpack[TypedDict] and the destination function’s keyword
arguments are assignable to the corresponding keys in source function’s
TypedDict. Moreover, not required keys should correspond to optional
function arguments, whereas required keys should correspond to required
function arguments. Again, the rest of the parameters have to be assignable.
Continuing the previous example:
class Example(TypedDict):
animal: Animal
string: str
number: NotRequired[int]
def src(**kwargs: Unpack[Example]): ...
def dest(*, animal: Dog, string: str, number: int = ...): ...
dest = src # OK!
It is worth pointing out that the destination function’s parameters that are to
be assignable to the keys and values from the TypedDict must be keyword
only:
def dest(dog: Dog, string: str, number: int = ...): ...
dog: Dog = {"name": "Daisy", "breed": "labrador"}
dest(dog, "some string") # OK!
dest = src # Type checker error!
dest(dog, "some string") # The same call fails at
# runtime now because 'src' expects
# keyword arguments.
The reverse situation where the destination callable contains
**kwargs: Unpack[TypedDict] and the source callable doesn’t contain
**kwargs should be disallowed. This is because, we cannot be sure that
additional keyword arguments are not being passed in when an instance of a
subclass had been assigned to a variable with a base class type and then
unpacked in the destination callable invocation:
def dest(**kwargs: Unpack[Animal]): ...
def src(name: str): ...
dog: Dog = {"name": "Daisy", "breed": "Labrador"}
animal: Animal = dog
dest = src # WRONG!
dest(**animal) # Fails at runtime.
A similar situation can happen even without inheritance as assignability between TypedDicts is structural.
Source contains untyped **kwargs¶
The destination callable contains **kwargs: Unpack[TypedDict] and the
source callable contains untyped **kwargs:
def src(**kwargs): ...
def dest(**kwargs: Unpack[Movie]): ...
dest = src # OK!
Source contains traditionally typed **kwargs: T¶
The destination callable contains **kwargs: Unpack[TypedDict], the source
callable contains traditionally typed **kwargs: T and each of the
destination function TypedDict’s fields is assignable to a variable
of type T:
class Vehicle:
...
class Car(Vehicle):
...
class Motorcycle(Vehicle):
...
class Vehicles(TypedDict):
car: Car
moto: Motorcycle
def dest(**kwargs: Unpack[Vehicles]): ...
def src(**kwargs: Vehicle): ...
dest = src # OK!
On the other hand, if the destination callable contains either untyped or
traditionally typed **kwargs: T and the source callable is typed using
**kwargs: Unpack[TypedDict] then an error should be generated, because
traditionally typed **kwargs aren’t checked for keyword names.
To summarize, function parameters should behave contravariantly and function return types should behave covariantly.
Passing kwargs inside a function to another function¶
A previous point mentions the problem of possibly passing additional keyword arguments by assigning a subclass instance to a variable that has a base class type. Let’s consider the following example:
class Animal(TypedDict):
name: str
class Dog(Animal):
breed: str
def takes_name(name: str): ...
dog: Dog = {"name": "Daisy", "breed": "Labrador"}
animal: Animal = dog
def foo(**kwargs: Unpack[Animal]):
print(kwargs["name"].capitalize())
def bar(**kwargs: Unpack[Animal]):
takes_name(**kwargs)
def baz(animal: Animal):
takes_name(**animal)
def spam(**kwargs: Unpack[Animal]):
baz(kwargs)
foo(**animal) # OK! foo only expects and uses keywords of 'Animal'.
bar(**animal) # WRONG! This will fail at runtime because 'breed' keyword
# will be passed to 'takes_name' as well.
spam(**animal) # WRONG! Again, 'breed' keyword will be eventually passed
# to 'takes_name'.
In the example above, the call to foo will not cause any issues at
runtime. Even though foo expects kwargs of type Animal it doesn’t
matter if it receives additional arguments because it only reads and uses what
it needs completely ignoring any additional values.
The calls to bar and spam will fail because an unexpected keyword
argument will be passed to the takes_name function.
Therefore, kwargs hinted with an unpacked TypedDict can only be passed
to another function if the function to which unpacked kwargs are being passed
to has **kwargs in its signature as well, because then additional keywords
would not cause errors at runtime during function invocation. Otherwise, the
type checker should generate an error.
In cases similar to the bar function above the problem could be worked
around by explicitly dereferencing desired fields and using them as arguments
to perform the function call:
def bar(**kwargs: Unpack[Animal]):
name = kwargs["name"]
takes_name(name)
Using Unpack with types other than TypedDict¶
TypedDict is the only permitted heterogeneous type for typing **kwargs.
Therefore, in the context of typing **kwargs, using Unpack with types
other than TypedDict should not be allowed and type checkers should
generate errors in such cases.
Callable¶
The Callable special form can be used to specify the signature of
a function within a type expression. The syntax is
Callable[[Param1Type, Param2Type], ReturnType]. For example:
from collections.abc import Callable
def func(cb: Callable[[int], str]) -> None:
...
x: Callable[[], str]
Parameters specified using Callable are assumed to be positional-only.
The Callable form provides no way to specify keyword-only parameters,
variadic parameters, or default argument values. For these use cases, see
the section on Callback protocols.
Meaning of ... in Callable¶
The Callable special form supports the use of ... in place of the list
of parameter types. This is a gradual form indicating that the type is
consistent with any input signature:
cb1: Callable[..., str]
cb1 = lambda x: str(x) # OK
cb1 = lambda : "" # OK
cb2: Callable[[], str] = cb1 # OK
A ... can also be used with Concatenate. In this case, the parameters
prior to the ... are required to be present in the input signature and
be assignable, but any additional parameters are permitted:
cb3: Callable[Concatenate[int, ...], str]
cb3 = lambda x: str(x) # OK
cb3 = lambda a, b, c: str(a) # OK
cb3 = lambda : "" # Error
cb3 = lambda *, a: str(a) # Error
If the input signature in a function definition includes both a *args and
**kwargs parameter and both are typed as Any (explicitly or implicitly
because it has no annotation), a type checker should treat this as the
equivalent of .... Any other parameters in the signature are unaffected
and are retained as part of the signature:
class Proto1(Protocol):
def __call__(self, *args: Any, **kwargs: Any) -> None: ...
class Proto2(Protocol):
def __call__(self, a: int, /, *args, **kwargs) -> None: ...
class Proto3(Protocol):
def __call__(self, a: int, *args: Any, **kwargs: Any) -> None: ...
class Proto4[**P](Protocol):
def __call__(self, a: int, *args: P.args, **kwargs: P.kwargs) -> None: ...
def func(p1: Proto1, p2: Proto2, p3: Proto3):
assert_type(p1, Callable[..., None]) # OK
assert_type(p2, Callable[Concatenate[int, ...], None]) # OK
assert_type(p3, Callable[..., None]) # Error
assert_type(p3, Proto4[...]) # OK
class A:
def method(self, a: int, /, *args: Any, k: str, **kwargs: Any) -> None:
pass
class B(A):
# This override is OK because it is assignable to the parent's method.
def method(self, a: float, /, b: int, *, k: str, m: str) -> None:
pass
The ... syntax can also be used to provide a specialized value for a
ParamSpec in a generic class or type alias.
For example:
type Callback[**P] = Callable[P, str]
def func(cb: Callable[[], str]) -> None:
f: Callback[...] = cb # OK
If ... is used with signature concatenation, the ... portion continues
to be consistent with any input parameters:
type CallbackWithInt[**P] = Callable[Concatenate[int, P], str]
type CallbackWithStr[**P] = Callable[Concatenate[str, P], str]
def func(cb: Callable[[int, str], str]) -> None:
f1: Callable[Concatenate[int, ...], str] = cb # OK
f2: Callable[Concatenate[str, ...], str] = cb # Error
f3: CallbackWithInt[...] = cb # OK
f4: CallbackWithStr[...] = cb # Error
Callback protocols¶
Protocols can be used to define flexible callback types that are impossible to
express using the Callable special form as specified above.
This includes keyword parameters, variadic parameters, default argument values,
and overloads. They can be defined as protocols with a __call__ member:
from typing import Protocol
class Combiner(Protocol):
def __call__(self, *args: bytes, max_len: int | None = None) -> list[bytes]: ...
def good_cb(*args: bytes, max_len: int | None = None) -> list[bytes]:
...
def bad_cb(*args: bytes, max_items: int | None) -> list[bytes]:
...
comb: Combiner = good_cb # OK
comb = bad_cb # Error! Argument 2 is not assignable because of
# different parameter name and kind in the callback
Callback protocols and Callable[...] types can generally be used
interchangeably.
Assignability rules for callables¶
A callable type B is assignable to a callable type A if the
return type of B is assignable to the return type of A and the input
signature of B accepts all possible combinations of arguments that the
input signature of A accepts. All of the specific assignability rules
described below derive from this general rule.
Parameter types¶
Callable types are covariant with respect to their return types but
contravariant with respect to their parameter types. This means a callable
B is assignable to callable A if the types of the parameters of
A are assignable to the parameters of B. For example, (x: float) ->
int is assignable to (x: int) -> float:
def func(cb: Callable[[float], int]):
f1: Callable[[int], float] = cb # OK
Parameter kinds¶
Callable B is assignable to callable A only if all keyword-only
parameters in A are present in B as either keyword-only parameters or
standard (positional or keyword) parameters. For example, (a: int) -> None
is assignable to (*, a: int) -> None, but the converse is not true. The
order of keyword-only parameters is ignored for purposes of assignability:
class KwOnly(Protocol):
def __call__(self, *, b: int, a: int) -> None: ...
class Standard(Protocol):
def __call__(self, a: int, b: int) -> None: ...
def func(standard: Standard, kw_only: KwOnly):
f1: KwOnly = standard # OK
f2: Standard = kw_only # Error
Likewise, callable B is assignable to callable A only if all
positional-only parameters in A are present in B as either
positional-only parameters or standard (positional or keyword) parameters. The
names of positional-only parameters are ignored for purposes of assignability:
class PosOnly(Protocol):
def __call__(self, not_a: int, /) -> None: ...
class Standard(Protocol):
def __call__(self, a: int) -> None: ...
def func(standard: Standard, pos_only: PosOnly):
f1: PosOnly = standard # OK
f2: Standard = pos_only # Error
*args parameters¶
If a callable A has a signature with a *args parameter, callable B
must also have a *args parameter to be assignable to A, and the
type of A’s *args parameter must be assignable to B’s *args
parameter:
class NoArgs(Protocol):
def __call__(self) -> None: ...
class IntArgs(Protocol):
def __call__(self, *args: int) -> None: ...
class FloatArgs(Protocol):
def __call__(self, *args: float) -> None: ...
def func(no_args: NoArgs, int_args: IntArgs, float_args: FloatArgs):
f1: NoArgs = int_args # OK
f2: NoArgs = float_args # OK
f3: IntArgs = no_args # Error: missing *args parameter
f4: IntArgs = float_args # OK
f5: FloatArgs = no_args # Error: missing *args parameter
f6: FloatArgs = int_args # Error: float is not assignable to int
If a callable A has a signature with one or more positional-only
parameters, a callable B is assignable to A only if B has an
*args parameter whose type is assignable from the types of any
otherwise-unmatched positional-only parameters in A:
class PosOnly(Protocol):
def __call__(self, a: int, b: str, /) -> None: ...
class IntArgs(Protocol):
def __call__(self, *args: int) -> None: ...
class IntStrArgs(Protocol):
def __call__(self, *args: int | str) -> None: ...
class StrArgs(Protocol):
def __call__(self, a: int, /, *args: str) -> None: ...
class Standard(Protocol):
def __call__(self, a: int, b: str) -> None: ...
def func(int_args: IntArgs, int_str_args: IntStrArgs, str_args: StrArgs):
f1: PosOnly = int_args # Error: str is not assignable to int
f2: PosOnly = int_str_args # OK
f3: PosOnly = str_args # OK
f4: IntStrArgs = str_args # Error: int | str is not assignable to str
f5: IntStrArgs = int_args # Error: int | str is not assignable to int
f6: StrArgs = int_str_args # OK
f7: StrArgs = int_args # Error: str is not assignable to int
f8: IntArgs = int_str_args # OK
f9: IntArgs = str_args # Error: int is not assignable to str
f10: Standard = int_str_args # Error: keyword parameters a and b missing
f11: Standard = str_args # Error: keyword parameter b missing
**kwargs parameters¶
If a callable A has a signature with a **kwargs parameter (without an
unpacked TypedDict type annotation), callable B must also have a
**kwargs parameter to be assignable to A, and the type of
A’s **kwargs parameter must be assignable to B’s **kwargs
parameter:
class NoKwargs(Protocol):
def __call__(self) -> None: ...
class IntKwargs(Protocol):
def __call__(self, **kwargs: int) -> None: ...
class FloatKwargs(Protocol):
def __call__(self, **kwargs: float) -> None: ...
def func(no_kwargs: NoKwargs, int_kwargs: IntKwargs, float_kwargs: FloatKwargs):
f1: NoKwargs = int_kwargs # OK
f2: NoKwargs = float_kwargs # OK
f3: IntKwargs = no_kwargs # Error: missing **kwargs parameter
f4: IntKwargs = float_kwargs # OK
f5: FloatKwargs = no_kwargs # Error: missing **kwargs parameter
f6: FloatKwargs = int_kwargs # Error: float is not assignable to int
If a callable A has a signature with one or more keyword-only parameters,
a callable B is assignable to A if B has a **kwargs parameter
whose type is assignable from the types of any otherwise-unmatched keyword-only
parameters in A:
class KwOnly(Protocol):
def __call__(self, *, a: int, b: str) -> None: ...
class IntKwargs(Protocol):
def __call__(self, **kwargs: int) -> None: ...
class IntStrKwargs(Protocol):
def __call__(self, **kwargs: int | str) -> None: ...
class StrKwargs(Protocol):
def __call__(self, *, a: int, **kwargs: str) -> None: ...
class Standard(Protocol):
def __call__(self, a: int, b: str) -> None: ...
def func(int_kwargs: IntKwargs, int_str_kwargs: IntStrKwargs, str_kwargs: StrKwargs):
f1: KwOnly = int_kwargs # Error: str is not assignable to int
f2: KwOnly = int_str_kwargs # OK
f3: KwOnly = str_kwargs # OK
f4: IntStrKwargs = str_kwargs # Error: int | str is not assignable to str
f5: IntStrKwargs = int_kwargs # Error: int | str is not assignable to int
f6: StrKwargs = int_str_kwargs # OK
f7: StrKwargs = int_kwargs # Error: str is not assignable to int
f8: IntKwargs = int_str_kwargs # OK
f9: IntKwargs = str_kwargs # Error: int is not assignable to str
f10: Standard = int_str_kwargs # Error: Does not accept positional arguments
f11: Standard = str_kwargs # Error: Does not accept positional arguments
Assignability rules for callable signatures that contain a **kwargs with an
unpacked TypedDict are described in the section above.
Signatures with ParamSpecs¶
A signature that includes *args: P.args, **kwargs: P.kwargs is equivalent
to a Callable parameterized by P:
class ProtocolWithP[**P](Protocol):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> None: ...
type TypeAliasWithP[**P] = Callable[P, None]
def func[**P](proto: ProtocolWithP[P], ta: TypeAliasWithP[P]):
# These two types are equivalent
f1: TypeAliasWithP[P] = proto # OK
f2: ProtocolWithP[P] = ta # OK
Default argument values¶
If a callable C has a parameter x with a default argument value and
A is the same as C except that x has no default argument, then
C is assignable to A. C is also assignable to A if
A is the same as C with parameter x removed:
class DefaultArg(Protocol):
def __call__(self, x: int = 0) -> None: ...
class NoDefaultArg(Protocol):
def __call__(self, x: int) -> None: ...
class NoX(Protocol):
def __call__(self) -> None: ...
def func(default_arg: DefaultArg):
f1: NoDefaultArg = default_arg # OK
f2: NoX = default_arg # OK
Overloads¶
If a callable B is overloaded with two or more signatures, it is
assignable to callable A if at least one of the overloaded
signatures in B is assignable to A:
class Overloaded(Protocol):
@overload
def __call__(self, x: int) -> int: ...
@overload
def __call__(self, x: str) -> str: ...
class IntArg(Protocol):
def __call__(self, x: int) -> int: ...
class StrArg(Protocol):
def __call__(self, x: str) -> str: ...
class FloatArg(Protocol):
def __call__(self, x: float) -> float: ...
def func(overloaded: Overloaded):
f1: IntArg = overloaded # OK
f2: StrArg = overloaded # OK
f3: FloatArg = overloaded # Error
If a callable A is overloaded with two or more signatures, callable B
is assignable to A if B is assignable to all of the signatures in
A:
class Overloaded(Protocol):
@overload
def __call__(self, x: int, y: str) -> float: ...
@overload
def __call__(self, x: str) -> complex: ...
class StrArg(Protocol):
def __call__(self, x: str) -> complex: ...
class IntStrArg(Protocol):
def __call__(self, x: int | str, y: str = "") -> int: ...
def func(int_str_arg: IntStrArg, str_arg: StrArg):
f1: Overloaded = int_str_arg # OK
f2: Overloaded = str_arg # Error