Back to Ruff

Function parameter types

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

0.16.24.5 KB
Original Source

Function parameter types

Basic

Within a function scope, the declared type of each parameter is its annotated type (or Unknown if not annotated). The initial inferred type is the annotated type of the parameter, if any:

py
def f(declared: int, unannotated):
    reveal_type(declared)  # revealed: int
    reveal_type(unannotated)  # revealed: Unknown

The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is a dictionary from strings to its annotated type:

py
def g(*args: int, **kwargs: int):
    reveal_type(args)  # revealed: tuple[int, ...]
    reveal_type(kwargs)  # revealed: dict[str, int]

Unannotated parameters with defaults

If there is no annotation but there is a default value, the inferred paramter type is the union of the inferred type of the default value and Unknown:

py
def f(a="foo", b=0, c=True, d=None):
    reveal_type(a)  # revealed: Unknown | Literal["foo"]
    reveal_type(b)  # revealed: Unknown | Literal[0]
    reveal_type(c)  # revealed: Unknown | Literal[True]
    reveal_type(d)  # revealed: Unknown | None

This means that the code in the function body needs to handle the case where the parameter is set to the default value:

py
def g(x=0):
    print(x + 1)

    # error: [unsupported-operator] "Operator `+` is not supported between objects of type `Unknown | Literal[0]` and `Literal["foo"]`"
    x + "foo"

But it still allows callers to pass in arguments of a wider type:

py
g(1.5)  # fine

Parameter kinds

py
from typing import Literal

def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str):
    reveal_type(a)  # revealed: Unknown
    reveal_type(b)  # revealed: int
    reveal_type(c)  # revealed: Unknown | Literal[1]
    reveal_type(d)  # revealed: int
    reveal_type(e)  # revealed: Unknown | Literal[3]
    reveal_type(f)  # revealed: Literal[4]
    reveal_type(g)  # revealed: Unknown | Literal[5]
    reveal_type(h)  # revealed: Literal[6]
    reveal_type(args)  # revealed: tuple[object, ...]
    reveal_type(kwargs)  # revealed: dict[str, str]

Unannotated variadic parameters

...are inferred as tuple of Unknown or dict from string to Unknown.

py
def g(*args, **kwargs):
    reveal_type(args)  # revealed: tuple[Unknown, ...]
    reveal_type(kwargs)  # revealed: dict[str, Unknown]

Annotation is present but not a fully static type

If there is an annotation, we respect it fully and don't union in the default value type.

py
from typing import Any

def f(x: Any = 1):
    reveal_type(x)  # revealed: Any

Default value type must be assignable to annotated type

The default value type must be assignable to the annotated type. If not, we emit a diagnostic, and fall back to inferring the annotated type, ignoring the default value type.

py
# error: [invalid-parameter-default]
def f(x: int = "foo"):
    reveal_type(x)  # revealed: int

# The check is assignable-to, not subtype-of, so this is fine:
from typing import Any

def g(x: Any = "foo"):
    reveal_type(x)  # revealed: Any

TypedDict defaults use annotation context

py
from typing import TypedDict

class Foo(TypedDict):
    x: int

def x(a: Foo = {"x": 42}): ...
def y(a: Foo = dict(x=42)): ...

TypedDict defaults still validate keys and value types

py
from typing import TypedDict

class Foo(TypedDict):
    x: int
    y: int

# error: [missing-typed-dict-key]
def missing_key(a: Foo = {"x": 42}): ...

# error: [invalid-argument-type]
def wrong_type(a: Foo = {"x": "s", "y": 1}): ...

# error: [invalid-key]
def extra_key(a: Foo = {"x": 1, "y": 2, "z": 3}): ...

Stub functions

toml
[environment]
python-version = "3.12"

In Protocol

py
from typing import Protocol

class Foo(Protocol):
    def x(self, y: bool = ...): ...
    def y[T](self, y: T = ...) -> T: ...

class GenericFoo[T](Protocol):
    def x(self, y: bool = ...) -> T: ...

In abstract method

py
from abc import abstractmethod

class Bar:
    @abstractmethod
    def x(self, y: bool = ...): ...
    @abstractmethod
    def y[T](self, y: T = ...) -> T: ...

In function overload

py
from typing import overload

@overload
def x(y: None = ...) -> None: ...
@overload
def x(y: int) -> str: ...
def x(y: int | None = None) -> str | None: ...

In if TYPE_CHECKING blocks

We generally view code in if TYPE_CHECKING blocks as having the same semantics and exemptions to code in stub files:

py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    def foo(x: bool = ...): ...  # fine