✦ For everyone, free.

Practical knowledge for real and everyday life

Home

Type Hints in Python

Type Hints in Python improve code clarity by annotating variables, functions, and classes for better tooling and fewer errors.

Type hints in Python provide a standardized static typing vocabulary for describing expected value types, callable interfaces, generic relationships, structural contracts, structured mappings, narrowing behavior, class-level typing constraints, and separately distributed interface information. These hints serve as annotations to express type expectations without implying automatic runtime enforcement or altering Python’s dynamic execution behavior.


Foundations of Type Hints in Python

Python source annotations are expressions attached to variables, function parameters, and return types. These annotations encode type expressions that static type checkers analyze to verify expected type relationships. The annotations exist as runtime values but are typically inert at runtime unless explicitly inspected or evaluated. Static type checkers use these annotations to reason about values and interfaces before or independently of program execution. Optional runtime introspection can access these annotations but does not imply any automatic type enforcement.

Gradual typing means that Python code can mix statically described portions with dynamically unconstrained portions. This approach allows incremental adoption of type hints without requiring complete annotation of an entire program. Parts of the codebase can remain untyped or loosely typed while others have precise static contracts.

Static type compatibility refers to whether one static type is assignable to or compatible with another according to the typing rules. This is distinct from runtime class membership, which checks actual object types during execution. Nominal subtyping depends on explicit inheritance relationships between types, whereas structural subtyping depends on the presence of compatible attributes and methods regardless of inheritance. Value validation involves runtime checks of actual object values and is separate from static type hinting, which does not assert runtime validation or rejection of incompatible values.

ConceptPrincipal Static-Typing Responsibility
AnnotationAttach type expressions to source names or callables
Type ExpressionDescribe expected static types, including unions, generics, protocols
Type AliasName reusable type expressions without creating new runtime types
Type ParameterRepresent generic type placeholders within parametric relationships
Generic TypeExpress parameterized types preserving relationships across specializations
Callable TypeSpecify callable interfaces with parameter and return types
OverloadDeclare multiple accepted argument-type and return-type signatures
ProtocolDefine structural static types by required attributes and methods
TypedDictDescribe dictionaries with specified keys and value types
Type NarrowingRefine static types based on control-flow conditions
NewTypeCreate distinct static nominal subtypes over existing runtime types
Type-Checker DirectiveCommunicate static analysis hints such as casting, ignoring, or revealing types
Stub FileProvide static type information without runtime implementations

Example of annotations describing intended types but not enforcing them at runtime:

x: int = 5
x = "hello"  # No runtime error, but static type checkers report incompatibility

def greet(name: str) -> str:
    return "Hello " + name

result = greet(42)  # Runtime executes, but a type checker warns of argument mismatch

The following conceptual SVG illustrates annotated Python source feeding two distinct consumers: ordinary runtime execution and a separate static type-checking path. The static checker analyzes type relationships while runtime values proceed under Python’s dynamic semantics.

Annotated Python Source Runtime Execution Static Type Checker: Verifies Type Relationships

Static Typing Semantics in Python

Static types describe sets of possible values and the operations those values support. Type checkers reason about these types before or independently of ordinary program execution, enabling detection of type mismatches without running code.

Subtyping and assignability concern whether values of one static type are acceptable where another is expected. This static relationship is not the same as exact runtime type identity or class membership. For example, a subtype can be substituted for a supertype if it meets the necessary interface or inheritance constraints.

Any is a special gradual typing escape that permits any static type and disables static checking for values or expressions typed as Any. It facilitates interoperability and incremental typing but allows unsound uses if overused. By contrast, object represents the top of the nominal type hierarchy: it can hold any runtime object but restricts statically permitted operations to those available on all objects.

Example contrasting Any and object:

from typing import Any

def process_any(x: Any) -> None:
    x.some_method()  # No static error, even if `x` has no such method at runtime

def process_object(x: object) -> None:
    # x.some_method()  # Static type error: `object` has no attribute 'some_method'
    print(str(x))  # Allowed since `str` is available on all objects

process_any(42)
process_object(42)

Bottom-like types such as Never or NoReturn represent code paths that cannot normally produce a value or return to their caller, like functions that always raise exceptions or loop infinitely. These types help express unreachable code or impossible outcomes in static analysis.

Python’s typing specification standardizes broad semantics for these types and constructs, but individual type checkers differ in diagnostics, inference precision, optional strictness, and supported narrowing behavior.


Type Annotations in Python

Type annotations appear in three main syntactic locations: variable annotations attach type information to local or global names, parameter annotations specify expected argument types, and return annotations declare the function’s result type.

Example with local and module variable annotations, annotated function parameters, return type, and an annotation without immediate value assignment:

age: int = 30  # Module-level variable annotation

def double(x: int) -> int:
    y: int  # Local variable annotation without assignment
    y = x * 2
    return y

name: str  # Module variable annotation without assignment
name = "Alice"

Annotation metadata has a static meaning distinct from its runtime representation or evaluation. How annotations are stored or evaluated can depend on Python version and supported introspection mechanisms such as typing.get_type_hints.

Forward references are annotations referring to types not yet defined in source order. Modern annotation mechanisms allow these to be represented as string literals or deferred evaluation without requiring runtime object construction at annotation time.

Example of recursive type annotations with forward references:

from typing import Optional, get_type_hints

class Node:
    value: int
    next: Optional["Node"]

node_type_hints = get_type_hints(Node)
print(node_type_hints)

Adding annotations to an interface should describe real behavioral expectations and relationships rather than maximizing annotation density without practical semantic meaning.


Python Type Expressions

Type expressions are valid in type positions to describe static types. These include nominal classes, unions, parameterized generics, literal types, annotated types, special typing forms, and type parameters.

Union and Optional Types in Python

A union A | B describes values that may be of either type A or B. Optionality is a special case of union where one of the alternatives is None, written as T | None. This differs from a parameter merely having a default value, which does not imply optionality in the type.

Example with unions and nullable types:

from typing import Optional

def greet(name: Optional[str]) -> str:
    if name is None:
        return "Hello, stranger!"
    else:
        return "Hello, " + name

print(greet("Alice"))
print(greet(None))

The function narrows name from str | None to str after the None check, allowing safe string concatenation.

Parameterized Types in Python

Parameterized types like list[int], dict[str, float], tuple[int, str], and set[bytes] describe collections or containers holding elements of specified types. User-defined generic classes can also be parameterized similarly.

Example with nested parameterized collections and a user-defined generic:

from typing import Generic, TypeVar

T = TypeVar("T")

class Box(Generic[T]):
    content: T

def process_items(items: list[dict[str, Box[int]]]) -> None:
    ...

# Static type error in assignment below:
items: list[dict[str, Box[str]]] = []  # type: ignore  # Incompatible Box[str] assigned to Box[int]

Literal Types in Python

Literal restricts a type to specific statically known values rather than the broader runtime type of those values.

Example with a function accepting Literal values:

from typing import Literal

def move(direction: Literal["up", "down", "left", "right"]) -> None:
    print(f"Moving {direction}")

move("up")     # Valid
# move("forward")  # Static error: invalid literal

Annotated Type Hints in Python

Annotated[T, metadata...] associates additional descriptive metadata with a primary static type T without changing the type-checker’s core interpretation.

Example using Annotated:

from typing import Annotated, get_type_hints

UserId = Annotated[int, "User identifier"]

def get_user(user_id: UserId) -> None:
    pass

hints = get_type_hints(get_user)
print(hints)

The metadata is accessible via introspection but does not influence type-checking or runtime behavior automatically.

Representative special type expressions include type[T] (class objects of type T), None (the singleton NoneType), fixed tuple forms like tuple[int, str], variadic tuples like tuple[int, ...], and unpacking forms used in generics.

Type FormExpresses or Restricts
UnionValues of either type
Nullable (T | None)Values of type T or None
ParameterizedContainer or generic types parameterized by contained types
LiteralSpecific constant values
AnnotatedType plus additional descriptive metadata
AnyUnrestricted, disables static checking
objectAny runtime object but restricts operations statically
NeverNo possible value; represents unreachable code

Python Type Aliases

A type alias is a reusable static name for a type expression, not a distinct runtime subtype with separate assignability semantics.

Modern Python uses the type statement to explicitly define aliases, including generic aliases with type parameters. This differs from ordinary runtime assignments that bind names to types without signaling alias semantics to type checkers.

Example of modern type alias definitions and usage:

from typing import TypeAlias, TypeVar, list

Vector: TypeAlias = list[float]

T = TypeVar("T")
Box: TypeAlias = list[T]

def process_vector(v: Vector) -> float:
    return sum(v)

def process_box(b: Box[int]) -> int:
    return sum(b)

Older forms of aliasing include simple assignment or using the TypeAlias marker to indicate an alias rather than a value assignment, useful in legacy code recognition.

Type aliases differ from NewType (which creates distinct static nominal subtypes), subclasses (runtime types with inheritance), ordinary variables referencing classes (runtime bindings), or aliases used solely for shortening names.


Generic Typing in Python

Generic typing expresses parameterized relationships over one or more types, allowing functions, classes, and aliases to preserve meaningful type relationships across specializations.

Modern inline type-parameter syntax supports generic functions, classes, and aliases in a concise form. This contrasts with traditional APIs using TypeVar, Generic, ParamSpec, and TypeVarTuple for compatibility and expressive power.

Example of a generic identity function and container using both modern and traditional syntax:

# Modern syntax (Python 3.11+)
def identity[T](x: T) -> T:
    return x

class Box[T]:
    content: T

# Traditional syntax
from typing import Generic, TypeVar

T = TypeVar("T")

def identity_old(x: T) -> T:
    return x

class BoxOld(Generic[T]):
    content: T

Python Type Variables

A type variable represents a placeholder for a type selected consistently within a generic relationship. It is not a runtime variable holding type objects.

Example generic function preserving input-output type relationship:

from typing import TypeVar

T = TypeVar("T")

def echo(x: T) -> T:
    return x

value_int = echo(1)       # Inferred type: int
value_str = echo("text")  # Inferred type: str

Upper Bounds on Type Variables

Upper bounds limit admissible type arguments to a type and compatible subtypes. The selected subtype is preserved where the generic relationship permits.

from typing import TypeVar

class Animal:
    pass

class Dog(Animal):
    pass

TAnimal = TypeVar("TAnimal", bound=Animal)

def treat(animal: TAnimal) -> TAnimal:
    return animal

dog = Dog()
treat(dog)  # Valid

Constrained Type Variables

Constrained type variables select among an explicit set of permitted alternatives, distinct from upper bounds that allow subclasses.

from typing import TypeVar

TShape = TypeVar("TShape", "Circle", "Square")  # Only Circle or Square allowed

Type Parameter Bounds, Constraints, and Defaults in Python

Bounds restrict type arguments to a subtype; constraints restrict to an explicit set. Defaults supply fallback type arguments when none are specified. Modern inline syntax forbids a non-default type parameter following a defaulted one to avoid ambiguity.

Examples:

T_bound: TypeVar = TypeVar("T_bound", bound=Animal)
T_constrained: TypeVar = TypeVar("T_constrained", Circle, Square)
T_default: TypeVar = TypeVar("T_default", bound=Animal, default=Dog)

Python Parameter Specifications

ParamSpec represents callable parameter lists, allowing higher-order APIs to preserve callable signatures beyond just return types.

Example of typed decorator preserving callable signature:

from typing import Callable, ParamSpec, TypeVar

P = ParamSpec('P')
R = TypeVar('R')

def decorator(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("Calling decorated function")
        return func(*args, **kwargs)
    return wrapper

Python Type Variable Tuples

TypeVarTuple represents a variable-length tuple of types, distinguishing heterogeneous parameter sequences from homogeneous sequences represented by ordinary type variables.

Example of generic function using a TypeVarTuple:

from typing import Generic, TypeVarTuple, Unpack

Ts = TypeVarTuple('Ts')

def concat_tuples(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
    return args

Defaults for TypeVar, ParamSpec, and TypeVarTuple

Defaults differ conceptually since TypeVar represents a single type, ParamSpec a parameter list, and TypeVarTuple a tuple of types. Each default form corresponds to their parameterized shape.

Parameter KindWhat It ParameterizesModern DeclarationTraditional APIRepresentative UseDefault Shape
Type VariableOne typeTTypeVar("T")Generic container element typeSingle type (e.g., object)
ParamSpecCallable parameter listPParamSpec("P")Higher-order function parametersEmpty parameter list
TypeVarTupleHeterogeneous tuple of typesTsTypeVarTuple("Ts")Tuple-shaped generic functionsEmpty tuple

Variance in Python Generic Types

Variance describes assignability relationships among generic specializations, not mutation or inheritance behavior of the generic class itself. Covariance allows substitution with subtypes, contravariance with supertypes, and invariance forbids substitution.

Examples:

from typing import Generic, TypeVar

T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
T_inv = TypeVar('T_inv')

class Producer(Generic[T_co]):
    def produce(self) -> T_co:
        ...

class Consumer(Generic[T_contra]):
    def consume(self, item: T_contra) -> None:
        ...

class Box(Generic[T_inv]):
    def __init__(self, content: T_inv) -> None:
        self.content = content

Modern inline type parameters infer variance automatically, unlike traditional declarations where variance is explicit.

Generic specialization such as Box[int] creates a static specialization distinct at the type-checking level, not a new runtime class.

Type parameters belong to their generic declaration scope rather than acting as global names.

Example combining generic class, bounded type parameter, default, and generic method:

from typing import Generic, TypeVar

T = TypeVar('T', bound=int, default=int)

class Container(Generic[T]):
    def __init__(self, value: T) -> None:
        self.value = value

    def get_value(self) -> T:
        return self.value

c_int = Container(5)          # Valid, T inferred as int
c_str = Container("hello")    # Static error: str not compatible with bound int

Callable Type Hints in Python

Callable type hints describe objects callable with specified parameter interfaces and producing specified result types.

Callable[[A, B], R] specifies a callable accepting parameters of types A and B and returning R. Less-specific forms like Callable[..., R] allow any parameter list returning R.

Callback protocols can express parameter names, kinds, overloads, or richer structure more precisely than simple Callable.

ParamSpec extends callable typing by representing callable parameter lists, enabling preservation of signatures in higher-order functions.

Example of function accepting a typed callback and higher-order function preserving signature:

from typing import Callable, ParamSpec, TypeVar

P = ParamSpec('P')
R = TypeVar('R')

def call_with_42(callback: Callable[[int], R]) -> R:
    return callback(42)

def wrap_func(func: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        print("Before call")
        return func(*args, **kwargs)
    return wrapper

Function Overloading in Python

Static function overloading via @overload declares multiple accepted argument-type signatures and corresponding return types for a single runtime callable.

In implementation modules, overload declarations precede one compatible runtime implementation. Stub files contain only overload signatures without implementation bodies.

Complete typed function with overloads:

from typing import overload

@overload
def parse(data: str) -> int:
    ...

@overload
def parse(data: bytes) -> float:
    ...

def parse(data):
    if isinstance(data, str):
        return int(data)
    elif isinstance(data, bytes):
        return float(len(data))
    else:
        raise TypeError("Unsupported type")

Overload ordering, overlapping signatures, and implementation consistency ensure clarity and correctness. Overloads should represent genuinely distinct static relationships rather than cosmetic duplicates.

Compared to unions and type variables, overloads precisely express input-output relationships that simpler constructs cannot.


Structural Typing with Python Protocols

Protocol classes define structural static types based on required attributes and methods rather than explicit inheritance.

Protocols are defined with Protocol and can include method signatures, data attributes, extensions, and explicit or implicit implementations.

Example defining a protocol and two unrelated classes satisfying it structurally:

from typing import Protocol

class SupportsClose(Protocol):
    def close(self) -> None:
        ...

class FileLike:
    def close(self) -> None:
        print("File closed")

class SocketLike:
    def close(self) -> None:
        print("Socket closed")

def cleanup(resource: SupportsClose) -> None:
    resource.close()

cleanup(FileLike())
cleanup(SocketLike())

Generic protocols use type parameters similar to other generic abstractions.

@runtime_checkable enables limited runtime isinstance or issubclass checks on protocols but does not perform full static signature verification.

Typing ApproachCompatibility BasisInheritance RequiredStatic-Checker RoleRuntime Behavior
Nominal Class TypingExplicit inheritanceYesValidate assignabilityRuntime type checks available
Protocol-Based TypingStructural attribute presenceNoStructural compatibility checkLimited runtime checks if opted
Duck Typing (No Spec)Informal, by conventionNoNoneFully dynamic
Runtime-Checkable ProtocolsStructural with runtime checksNoStructural compatibilityisinstance supported selectively

Python Typed Dictionaries

TypedDict describes dictionaries with specified string keys and corresponding value types as a structural static type. At runtime, objects remain ordinary dictionaries.

Required and non-required items are distinguished through totality and item-level qualifiers (Required, NotRequired), separating key presence from whether the value type includes None.

Read-only items use a ReadOnly qualifier at the static level, restricting mutation statically but not transforming the runtime dictionary into an immutable mapping.

Example TypedDict with required, non-required, and read-only items:

from typing import TypedDict, NotRequired, Required

class User(TypedDict):
    id: Required[int]
    name: str
    email: NotRequired[str]

# Read-only support may be available in recent typing versions
# class UserRO(TypedDict, total=False):
#     id: int  # read-only qualifier applied here if supported

user: User = {"id": 1, "name": "Alice"}
user["email"] = "alice@example.com"  # Allowed

# user["id"] = 2  # Static error if id is read-only

TypedDict inheritance and structural assignability depend on required keys, item types, mutability, and presence requirements, not dictionary class inheritance.

Using TypedDict with Unpack allows typed keyword arguments representing named dictionary items without treating the TypedDict as a function signature.

Example function with unpacked TypedDict keyword arguments:

from typing import TypedDict, Unpack

class Config(TypedDict):
    verbose: bool
    retries: int

def configure(**kwargs: Unpack[Config]) -> None:
    print(kwargs)

configure(verbose=True, retries=3)  # Valid
# configure(verbose="yes")  # Static error

Type Narrowing in Python

Type narrowing is a static type checker’s refinement of a value’s statically known type within control-flow paths based on evidence from conditions about possible runtime values.

Control-Flow Type Narrowing in Python

Common narrowing conditions include is not None, isinstance, issubclass, callable, literal comparisons, and discriminating unions. Exact inference differs among type checkers.

Example narrowing with unions and literals:

from typing import Union, Literal

def handle(value: Union[int, None]) -> int:
    if value is None:
        return 0
    else:
        # Here, `value` is narrowed to `int`
        return value * 2

def check_shape(shape: Literal["circle", "square"]) -> str:
    if shape == "circle":
        return "Round"
    else:
        # Narrowed to "square"
        return "Angular"

Narrowing is invalidated or limited when mutable state, aliasing, reassignment, concurrency, or intervening calls undermine assumptions about a value’s type.

User-Defined Type Narrowing in Python

TypeIs and TypeGuard are return annotations communicating trusted narrowing from user-defined predicates.

TypeIs narrows to a compatible target type in both true and false branches, requiring the predicate implementation to accurately characterize membership.

TypeGuard allows true-branch narrowing even when the narrowed type is not assignable to the input type; false-branch behavior differs from TypeIs.

Example contrasting TypeIs and TypeGuard:

from typing import TypeGuard, TypeIs, Union

def is_str(val: object) -> TypeIs[str]:
    return isinstance(val, str)

def is_int(val: object) -> TypeGuard[int]:
    return isinstance(val, int)

def unsound_predicate(val: object) -> TypeGuard[str]:
    return False  # Unsound: returns False even if val is str

Class Type Hints in Python

Class and Instance Variable Type Hints in Python

Instance annotations describe per-instance attributes; ClassVar annotations describe attributes intended for class-level state, distinguishable by static checkers but not enforced at runtime.

Example class with instance attributes, ClassVar, and methods:

from typing import ClassVar

class Counter:
    count: ClassVar[int] = 0
    value: int

    def __init__(self, value: int) -> None:
        self.value = value
        Counter.count += 1

    def increment(self) -> None:
        self.value += 1

print(Counter.count)
c1 = Counter(10)
print(c1.value)
print(Counter.count)

type[T] for Class Objects

type[T] represents values that are class objects compatible with type T. This differs from ordinary instance parameters.

Example:

from typing import Type

class Base:
    pass

def factory(cls: Type[Base]) -> Base:
    return cls()

Self Types in Python

Self statically represents the current enclosing class or applicable subclass, useful for fluent methods, alternative constructors, and subtype-preserving return types.

Example using Self:

from typing import Self

class Base:
    def fluent(self) -> Self:
        # Return the instance for chaining
        return self

class Sub(Base):
    def sub_method(self) -> Self:
        return self

b = Base()
s = Sub()

reveal_type(b.fluent())  # Base
reveal_type(s.fluent())  # Sub

Returning concrete base-class types would lose useful subtype information.

Final and Override Constraints in Python

Final marks names or attributes that should not be reassigned or overridden. @final decorates classes or methods to prevent subclassing or overriding.

@override asserts statically that a method overrides compatible behavior from a base class, aiding detection of misspellings or signature mismatches.

Example:

from typing import ClassVar, Final, Self, final, override

class Base:
    MAX_COUNT: ClassVar[int] = 10
    version: Final[str] = "1.0"

    @final
    def method(self) -> None:
        print("Base method")

class Derived(Base):
    @override
    def method(self) -> None:  # Static error: cannot override @final method
        print("Derived method")

Distinct Static Types with NewType in Python

NewType defines a statically distinct nominal subtype-like identity over an existing runtime representation without creating a runtime subclass with independent storage or behavior.

Example defining two distinct identifier types over int:

from typing import NewType

UserId = NewType('UserId', int)
OrderId = NewType('OrderId', int)

def get_user_name(user_id: UserId) -> str:
    return f"User {user_id}"

user_id = UserId(42)
order_id = OrderId(42)

get_user_name(user_id)  # Valid
# get_user_name(order_id)  # Static error: incompatible type

NewType differs from a type alias (which preserves the same static type identity) and an actual subclass (which creates a runtime class relationship).

NewType suits logically distinct values sharing one runtime representation but discourages proliferation of meaningless distinct static identities.


Type Checker Directives in Python

Type-checker directives communicate information to static analyzers without changing runtime behavior.

cast asserts a static interpretation for a value, assert_type checks an expected inferred type, and reveal_type exposes inferred type information for diagnostics.

Targeted ignore comments like # type: ignore suppress specific static diagnostics narrowly.

TYPE_CHECKING is a constant recognized by analyzers to include code for type analysis only, not runtime execution.

Examples:

from typing import cast, TYPE_CHECKING, assert_type, reveal_type

x = cast(int, "123")  # Static cast, no runtime conversion

if TYPE_CHECKING:
    import some_module  # Only imported during static type checking

def func(val: object) -> None:
    reveal_type(val)  # Shows inferred type
    assert_type(val, object)  # Checks inferred type matches

def foo() -> None:
    x = "string"
    # type: ignore[attr-defined]
    print(x.nonexistent_method())  # Ignored type error

Additional directives include @no_type_check, version/platform checks understood by type checkers, and deprecation metadata for static analysis.


Python Stub Files

.pyi stub files provide interface-oriented static type information for Python modules or packages without including runtime implementations.

Example measurements.py:

# measurements.py
PI = 3.14159

class Circle:
    def __init__(self, radius: float) -> None:
        self.radius = radius

def area_circle(c: Circle) -> float:
    return PI * c.radius ** 2

from typing import overload, Union

@overload
def convert(value: int) -> float: ...
@overload
def convert(value: float) -> int: ...

def convert(value: Union[int, float]) -> Union[float, int]:
    if isinstance(value, int):
        return float(value)
    else:
        return int(value)

Corresponding measurements.pyi stub:

PI: float

class Circle:
    radius: float
    def __init__(self, radius: float) -> None: ...

def area_circle(c: Circle) -> float: ...

from typing import overload

@overload
def convert(value: int) -> float: ...
@overload
def convert(value: float) -> int: ...

Ellipsis and annotation-only declarations signal absence of implementation in stubs. Overloaded functions in stubs contain only overload signatures.

Stubs describe externally usable typed interfaces, not every private implementation detail.

Inline annotations, distributed stub files, and markers allow distributing typing information separately from runtime implementations.

Usage example importing the stub API:

from measurements import Circle, area_circle, convert

c = Circle(2.5)
print(area_circle(c))  # Valid call

result1 = convert(5)    # Static type: float
result2 = convert(3.14) # Static type: int

# result3 = convert("string")  # Static error: invalid argument type

Static checking confirms valid and invalid calls, overload return relationships, and class attributes based on the stub interface.