[Runtime][Python] Add pyright checks and fix errors (#24561)

Signed-off-by: Florian Walbroel <walbroel@roofline.ai>
diff --git a/build_tools/github_actions/ci_requirements.txt b/build_tools/github_actions/ci_requirements.txt
index 5648db8..81570e4 100644
--- a/build_tools/github_actions/ci_requirements.txt
+++ b/build_tools/github_actions/ci_requirements.txt
@@ -3,3 +3,4 @@
 cmake>=3.26
 ninja
 packaging
+pyright==1.1.410
diff --git a/runtime/bindings/python/CMakeLists.txt b/runtime/bindings/python/CMakeLists.txt
index 087a0c4..3f4e357 100644
--- a/runtime/bindings/python/CMakeLists.txt
+++ b/runtime/bindings/python/CMakeLists.txt
@@ -295,6 +295,21 @@
 # Tests
 ################################################################################
 
+if(IREE_BUILD_PYTHON_BINDINGS AND IREE_BUILD_TESTS)
+  find_program(PYRIGHT_EXECUTABLE NAMES pyright REQUIRED)
+  add_test(
+    NAME iree/runtime/bindings/pyright
+    COMMAND
+      "${PYRIGHT_EXECUTABLE}"
+      "-p"
+      "${CMAKE_CURRENT_SOURCE_DIR}/pyrightconfig.json"
+    WORKING_DIRECTORY
+      "${CMAKE_CURRENT_SOURCE_DIR}"
+  )
+  set_property(TEST iree/runtime/bindings/pyright PROPERTY LABELS "runtime;lint")
+  set_property(TEST iree/runtime/bindings/pyright PROPERTY TIMEOUT 120)
+endif()
+
 iree_py_test(
   NAME
     array_interop_test
diff --git a/runtime/bindings/python/iree/_runtime/libs.pyi b/runtime/bindings/python/iree/_runtime/libs.pyi
new file mode 100644
index 0000000..de17185
--- /dev/null
+++ b/runtime/bindings/python/iree/_runtime/libs.pyi
@@ -0,0 +1,5 @@
+from typing import Any
+
+_runtime: Any
+library_path: str
+name: str
diff --git a/runtime/bindings/python/iree/runtime/_binding.pyi b/runtime/bindings/python/iree/runtime/_binding.pyi
index f2013e0..05abddd 100644
--- a/runtime/bindings/python/iree/runtime/_binding.pyi
+++ b/runtime/bindings/python/iree/runtime/_binding.pyi
@@ -1,4 +1,5 @@
 import os
+import numpy as np
 from typing import (
     Any,
     Callable,
@@ -7,6 +8,7 @@
     Optional,
     Sequence,
     Tuple,
+    TypeVar,
     Union,
     overload,
 )
@@ -14,6 +16,8 @@
 
 from .typing import HalModuleBufferViewTraceCallback
 
+_T = TypeVar("_T")
+
 def create_hal_module(
     instance: VmInstance,
     device: Optional[HalDevice] = None,
@@ -67,31 +71,49 @@
     def __or__(self, other: BufferUsage) -> int: ...
 
 class ExternalTimepointType(int):
-    NONE = ClassVar[ExternalTimepointType] = ...
-    ASYNC_PRIMITIVE = ClassVar[ExternalTimepointType] = ...
-    CUDA_EVENT = ClassVar[ExternalTimepointType] = ...
-    HIP_EVENT = ClassVar[ExternalTimepointType] = ...
+    NONE: ClassVar[ExternalTimepointType] = ...
+    ASYNC_PRIMITIVE: ClassVar[ExternalTimepointType] = ...
+    CUDA_EVENT: ClassVar[ExternalTimepointType] = ...
+    HIP_EVENT: ClassVar[ExternalTimepointType] = ...
     def __int__(self) -> int: ...
 
 class SemaphoreCompatibility(int):
-    NONE = ClassVar[SemaphoreCompatibility] = ...
-    HOST_WAIT = ClassVar[SemaphoreCompatibility] = ...
-    DEVICE_WAIT = ClassVar[SemaphoreCompatibility] = ...
-    HOST_SIGNAL = ClassVar[SemaphoreCompatibility] = ...
-    DEVICE_SIGNAL = ClassVar[SemaphoreCompatibility] = ...
-    HOST_ONLY = ClassVar[SemaphoreCompatibility] = ...
-    DEVICE_ONLY = ClassVar[SemaphoreCompatibility] = ...
-    ALL = ClassVar[SemaphoreCompatibility] = ...
+    NONE: ClassVar[SemaphoreCompatibility] = ...
+    HOST_WAIT: ClassVar[SemaphoreCompatibility] = ...
+    DEVICE_WAIT: ClassVar[SemaphoreCompatibility] = ...
+    HOST_SIGNAL: ClassVar[SemaphoreCompatibility] = ...
+    DEVICE_SIGNAL: ClassVar[SemaphoreCompatibility] = ...
+    HOST_ONLY: ClassVar[SemaphoreCompatibility] = ...
+    DEVICE_ONLY: ClassVar[SemaphoreCompatibility] = ...
+    ALL: ClassVar[SemaphoreCompatibility] = ...
     def __and__(self, other: SemaphoreCompatibility) -> int: ...
     def __or__(self, other: SemaphoreCompatibility) -> int: ...
     def __int__(self) -> int: ...
 
 class ExternalTimepointFlags(int):
-    NONE = ClassVar[ExternalTimepointFlags] = ...
+    NONE: ClassVar[ExternalTimepointFlags] = ...
     def __and__(self, other: ExternalTimepointFlags) -> int: ...
     def __or__(self, other: ExternalTimepointFlags) -> int: ...
     def __int__(self) -> int: ...
 
+class _InvokeStatics: ...
+
+_invoke_statics: _InvokeStatics
+
+class InvokeContext:
+    def __init__(self, device: HalDevice) -> None: ...
+
+class ArgumentPacker:
+    def __init__(
+        self, statics: _InvokeStatics, arg_descs: Optional[list[Any]] = None
+    ) -> None: ...
+    def pack(
+        self,
+        invoke_context: InvokeContext,
+        pos_args: Sequence[Any],
+        kwargs: dict[str, Any],
+    ) -> VmVariantList: ...
+
 class FileHandle:
     @staticmethod
     def wrap_memory(
@@ -129,6 +151,12 @@
     def allocate_host_staging_buffer_copy(
         self, device: HalDevice, initial_contents: object
     ) -> HalBuffer: ...
+    def import_host_allocation(
+        self,
+        buffer_obj: Any,
+        memory_type: Union[MemoryType, int],
+        allowed_usage: Union[BufferUsage, int],
+    ) -> HalBuffer: ...
     def query_buffer_compatibility(
         self,
         memory_type: Union[MemoryType, int],
@@ -202,7 +230,7 @@
     @property
     def __iree_vm_ref__(self) -> VmRef: ...
 
-HalSemaphoreList = List[Tuple[HalSemaphore, int]]
+HalSemaphoreList = List[Tuple[HalSemaphore, int]] | HalFence
 
 class HalDevice:
     def begin_profiling(self, mode: Optional[str] = None) -> None: ...
@@ -345,7 +373,7 @@
 
 class HalFence:
     @staticmethod
-    def create_at(value: int) -> HalFence: ...
+    def create_at(sem: HalSemaphore, value: int) -> HalFence: ...
     @staticmethod
     def join(fences: Sequence[HalFence]) -> HalFence: ...
     def __init__(self, capacity: int) -> None: ...
@@ -422,7 +450,9 @@
 
 class MappedMemory:
     def __init__(self, *args, **kwargs) -> None: ...
-    def asarray(self, shape: Sequence[int], numpy_dtype_descr: object) -> object: ...
+    def asarray(
+        self, shape: Sequence[int], numpy_dtype_descr: object
+    ) -> np.ndarray[Any, np.dtype[Any]]: ...
 
 class MemoryAccess(int):
     ALL: ClassVar[MemoryAccess] = ...
@@ -481,7 +511,7 @@
         ...
 
 class ParameterIndex:
-    def __init__() -> None: ...
+    def __init__(self) -> None: ...
     def __len__(self) -> int: ...
     def __getitem__(self, i) -> ParameterIndexEntry: ...
     def reserve(self, new_capacity: int) -> None: ...
@@ -564,7 +594,7 @@
 
 class VmContext:
     def __init__(
-        self, instance: VmInstance, modules: Optional[list[VmModule]] = None
+        self, instance: VmInstance, modules: Optional[Sequence[VmModule]] = None
     ) -> None: ...
     def invoke(
         self, function: VmFunction, inputs: VmVariantList, outputs: VmVariantList
@@ -624,12 +654,12 @@
     @property
     def name(self) -> str: ...
     @property
-    def stashed_flatbuffer_blob(self) -> object: ...
+    def stashed_flatbuffer_blob(self) -> bytes | None: ...
     @property
     def version(self) -> int: ...
 
 class VmRef:
-    def deref(self, value: Any, optional: bool = False) -> object: ...
+    def deref(self, value: type[_T], optional: bool = False) -> _T | None: ...
     def isinstance(self, ref_type: type) -> bool: ...
     def __eq__(self, other) -> bool: ...
     @property
@@ -638,7 +668,10 @@
 class VmVariantList:
     def __init__(self, capacity: int) -> None: ...
     def get_as_list(self, index: int) -> VmVariantList: ...
+    @overload
     def get_as_object(self, index: int) -> object: ...
+    @overload
+    def get_as_object(self, index: int, clazz: type[_T]) -> _T: ...
     def get_as_ref(self, index: int) -> VmRef: ...
     def get_serialized_trace_value(self, index: int) -> dict: ...
     def get_variant(self, index: int) -> Any: ...
diff --git a/runtime/bindings/python/iree/runtime/array_interop.py b/runtime/bindings/python/iree/runtime/array_interop.py
index 63deb86..da2b21c 100644
--- a/runtime/bindings/python/iree/runtime/array_interop.py
+++ b/runtime/bindings/python/iree/runtime/array_interop.py
@@ -5,7 +5,7 @@
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 """BufferView and Python Array Protocol interop."""
 
-from typing import Optional, Tuple
+from typing import Literal, Optional, Tuple, cast
 import logging
 import ml_dtypes
 import numpy as np
@@ -99,6 +99,18 @@
             func, types, args, kwargs
         )  # pytype: disable=attribute-error
 
+    def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
+        def transfer_input(inp):
+            if isinstance(inp, DeviceArray):
+                return inp._transfer_to_host(True)
+            return inp
+
+        host_inputs = [transfer_input(inp) for inp in inputs]
+        if kwargs.get("out") is not None:
+            kwargs = dict(kwargs)
+            kwargs["out"] = tuple(transfer_input(inp) for inp in kwargs["out"])
+        return getattr(ufunc, method)(*host_inputs, **kwargs)
+
     def __repr__(self):
         return f"<IREE DeviceArray: shape={np.shape(self)}, dtype={self.dtype}>"
 
@@ -142,7 +154,9 @@
         # TODO: When synchronization is enabled, need to block here.
         raw_dtype = self._get_raw_dtype()
         mapped_memory = self._buffer_view.map()
-        host_array = mapped_memory.asarray(self._buffer_view.shape, raw_dtype)
+        host_array = cast(
+            np.ndarray, mapped_memory.asarray(self._buffer_view.shape, raw_dtype)
+        )
         # Detect if we need to force an explicit conversion. This happens when
         # we were requested to pretend that the array is in a specific dtype,
         # even if that is not representable on the device. You guessed it:
@@ -216,7 +230,12 @@
     def shape(self):
         return np.shape(self)
 
-    def astype(self, dtype, casting="unsafe", copy=True):
+    def astype(
+        self,
+        dtype,
+        casting: Literal["no", "equiv", "safe", "same_kind", "unsafe"] = "unsafe",
+        copy=True,
+    ):
         if self.dtype == dtype and not copy:
             return self
         host_ary = self.to_host()
@@ -305,6 +324,8 @@
         buffer=a,
         element_type=element_type,
     )
+    if not isinstance(buffer_view, HalBufferView):
+        raise TypeError("Expected allocate_buffer_copy to return a HalBufferView")
     return DeviceArray(
         device,
         buffer_view,
diff --git a/runtime/bindings/python/iree/runtime/benchmark.py b/runtime/bindings/python/iree/runtime/benchmark.py
index e1598dc..ecccde0 100644
--- a/runtime/bindings/python/iree/runtime/benchmark.py
+++ b/runtime/bindings/python/iree/runtime/benchmark.py
@@ -19,7 +19,6 @@
 from typing import Union
 from os import PathLike
 
-import iree.runtime
 from . import VmModule
 import numpy
 import os
@@ -105,7 +104,7 @@
         v = kwargs[k]
         args.append(f"--{k}={v}")
 
-    for inp in inputs:
+    for inp in inputs or []:
         if isinstance(inp, str):
             args.append(f"--input={inp}")
             continue
diff --git a/runtime/bindings/python/iree/runtime/build_requirements.txt b/runtime/bindings/python/iree/runtime/build_requirements.txt
index 3700a42..81384ac 100644
--- a/runtime/bindings/python/iree/runtime/build_requirements.txt
+++ b/runtime/bindings/python/iree/runtime/build_requirements.txt
@@ -10,3 +10,4 @@
 wheel>=0.36.2
 sympy>=1.14.0
 ml_dtypes>=0.5.4
+pyright==1.1.410
diff --git a/runtime/bindings/python/iree/runtime/function.py b/runtime/bindings/python/iree/runtime/function.py
index 0de8e94..c104d24 100644
--- a/runtime/bindings/python/iree/runtime/function.py
+++ b/runtime/bindings/python/iree/runtime/function.py
@@ -8,6 +8,7 @@
 
 import json
 import logging
+from typing import cast
 
 import numpy as np
 
@@ -364,7 +365,9 @@
             # since this is higher level and it preserves layering. Note that
             # the reflection case also does this conversion.
             if isinstance(converted, VmRef):
-                converted_buffer_view = converted.deref(HalBufferView, True)
+                converted_buffer_view = cast(
+                    HalBufferView | None, converted.deref(HalBufferView, True)
+                )
                 if converted_buffer_view:
                     converted = DeviceArray(
                         inv.device, converted_buffer_view, implicit_host_transfer=True
@@ -382,7 +385,7 @@
                 raise
             except Exception as e:
                 _raise_return_error(
-                    inv, f"exception converting from VM type to Python", e
+                    inv, "exception converting from VM type to Python", e
                 )
         results.append(converted)
     return results
diff --git a/runtime/bindings/python/iree/runtime/io.py b/runtime/bindings/python/iree/runtime/io.py
index e9d54da..dcd5ce8 100644
--- a/runtime/bindings/python/iree/runtime/io.py
+++ b/runtime/bindings/python/iree/runtime/io.py
@@ -26,20 +26,22 @@
 class SplatValue:
     def __init__(
         self,
-        pattern: Union[array.array, np.ndarray],
+        pattern: Union[array.array, np.ndarray, np.generic],
         count: Union[Sequence[int], int],
     ):
-        if hasattr(pattern, "shape"):
+        if isinstance(pattern, np.ndarray):
             shape = pattern.shape
             if not shape:
                 total_elements = 1
             else:
-                total_elements = reduce(lambda x, y: x * y, pattern.shape)
+                total_elements = reduce(lambda x, y: x * y, shape)
+        elif isinstance(pattern, np.generic):
+            total_elements = 1
         else:
             total_elements = len(pattern)
         item_size = pattern.itemsize
         if total_elements != 1:
-            raise ValueError(f"SplatValue requires an array of a single element")
+            raise ValueError("SplatValue requires an array of a single element")
         self.pattern = pattern
         if isinstance(count, int):
             logical_length = count
@@ -162,23 +164,23 @@
 
 
 _DTYPE_TO_NAME = (
-    (np.float16, "float16"),
-    (np.float32, "float32"),
-    (np.float64, "float64"),
-    (np.int32, "int32"),
-    (np.int64, "int64"),
-    (np.int16, "int16"),
-    (np.int8, "int8"),
-    (np.uint32, "uint32"),
-    (np.uint64, "uint64"),
-    (np.uint16, "uint16"),
-    (np.uint8, "uint8"),
-    (np.bool_, "bool"),
-    (np.complex64, "complex64"),
-    (np.complex128, "complex128"),
+    (np.dtype(np.float16), "float16"),
+    (np.dtype(np.float32), "float32"),
+    (np.dtype(np.float64), "float64"),
+    (np.dtype(np.int32), "int32"),
+    (np.dtype(np.int64), "int64"),
+    (np.dtype(np.int16), "int16"),
+    (np.dtype(np.int8), "int8"),
+    (np.dtype(np.uint32), "uint32"),
+    (np.dtype(np.uint64), "uint64"),
+    (np.dtype(np.uint16), "uint16"),
+    (np.dtype(np.uint8), "uint8"),
+    (np.dtype(np.bool_), "bool"),
+    (np.dtype(np.complex64), "complex64"),
+    (np.dtype(np.complex128), "complex128"),
 )
 
-_NAME_TO_DTYPE: dict[str, np.dtype] = {
+_NAME_TO_DTYPE: dict[str, np.dtype[Any]] = {
     name: np_dtype for np_dtype, name in _DTYPE_TO_NAME
 }
 
diff --git a/runtime/bindings/python/iree/runtime/system_api.py b/runtime/bindings/python/iree/runtime/system_api.py
index c41251c..6bd0bad 100644
--- a/runtime/bindings/python/iree/runtime/system_api.py
+++ b/runtime/bindings/python/iree/runtime/system_api.py
@@ -31,7 +31,7 @@
 import os
 import sys
 
-from typing import Any, List, Mapping, Optional, Sequence, Tuple, Union
+from typing import Any, List, MutableMapping, Optional, Sequence, Tuple, Union
 
 from . import _binding
 from .function import FunctionInvoker
@@ -77,7 +77,9 @@
         self.default_vm_modules = (hal_module,)
 
 
-def _bool_to_int8(array: Any) -> Optional[Union[np.ndarray, List[Any], Tuple[Any]]]:
+def _bool_to_int8(
+    array: Any,
+) -> Optional[Union[np.ndarray, List[Any], Tuple[Any], dict[Any, Any]]]:
     if not isinstance(array, np.ndarray):
         return array
 
@@ -88,7 +90,9 @@
     return array
 
 
-def normalize_value(value: Any) -> Optional[Union[np.ndarray, List[Any], Tuple[Any]]]:
+def normalize_value(
+    value: Any,
+) -> Optional[Union[np.ndarray, List[Any], Tuple[Any], dict[Any, Any]]]:
     """Normalizes the given value for input to (or comparison with) IREE."""
     if value is None:
         # Exclude None from falling through to blanket np.asarray conversion.
@@ -112,7 +116,7 @@
 def _convert_lists_to_tuples(pytree):
     if isinstance(pytree, Sequence):
         return tuple(_convert_lists_to_tuples(leaf) for leaf in pytree)
-    elif isinstance(pytree, Mapping):
+    elif isinstance(pytree, MutableMapping):
         for key in pytree:
             pytree[key] = _convert_lists_to_tuples(pytree[key])
         return pytree
@@ -180,12 +184,17 @@
 class SystemContext:
     """Global system."""
 
-    def __init__(self, vm_modules=None, config: Optional[Config] = None):
+    def __init__(
+        self,
+        vm_modules: Optional[Sequence[_binding.VmModule]] = None,
+        config: Optional[Config] = None,
+    ):
         self._config = config if config is not None else Config()
         self._is_dynamic = vm_modules is None
         if self._is_dynamic:
             init_vm_modules = None
         else:
+            assert vm_modules is not None
             init_vm_modules = self._config.default_vm_modules + tuple(vm_modules)
 
         self._vm_context = _binding.VmContext(
@@ -201,6 +210,7 @@
                 ]
             )
         else:
+            assert init_vm_modules is not None
             self._bound_modules = BoundModules(
                 [(m.name, BoundModule(self, m)) for m in init_vm_modules]
             )
diff --git a/runtime/bindings/python/iree/runtime/system_setup.py b/runtime/bindings/python/iree/runtime/system_setup.py
index c592e46..029af5f 100644
--- a/runtime/bindings/python/iree/runtime/system_setup.py
+++ b/runtime/bindings/python/iree/runtime/system_setup.py
@@ -49,6 +49,8 @@
         if cache:
             existing = _GLOBAL_DEVICES_BY_URI.get(device_uri)
             if existing is not None:
+                if isinstance(existing, Exception):
+                    raise existing
                 return existing
 
         driver = get_driver(device_uri)
diff --git a/runtime/bindings/python/iree/runtime/version.py b/runtime/bindings/python/iree/runtime/version.py
index 65f20b6..71ce247 100644
--- a/runtime/bindings/python/iree/runtime/version.py
+++ b/runtime/bindings/python/iree/runtime/version.py
@@ -9,4 +9,4 @@
 package.
 """
 
-from .._runtime_libs.version import *
+from .._runtime_libs.version import *  # pyright: ignore[reportMissingImports]
diff --git a/runtime/bindings/python/iree/runtime/version.pyi b/runtime/bindings/python/iree/runtime/version.pyi
new file mode 100644
index 0000000..dad2728
--- /dev/null
+++ b/runtime/bindings/python/iree/runtime/version.pyi
@@ -0,0 +1,3 @@
+PACKAGE_SUFFIX: str
+REVISIONS: dict[str, str]
+VERSION: str
diff --git a/runtime/bindings/python/pyrightconfig.json b/runtime/bindings/python/pyrightconfig.json
new file mode 100644
index 0000000..8382457
--- /dev/null
+++ b/runtime/bindings/python/pyrightconfig.json
@@ -0,0 +1,17 @@
+{
+  "typeCheckingMode": "basic",
+  "pythonVersion": "3.10",
+  "include": [
+    "iree"
+  ],
+  "stubPath": "typings",
+  "reportMissingModuleSource": "none",
+  "extraPaths": [
+    "."
+  ],
+  "exclude": [
+    "**/__pycache__",
+    "**/.venv",
+    "**/build"
+  ]
+}
diff --git a/runtime/bindings/python/tests/array_interop_test.py b/runtime/bindings/python/tests/array_interop_test.py
index 77bc132..0d12c1d 100644
--- a/runtime/bindings/python/tests/array_interop_test.py
+++ b/runtime/bindings/python/tests/array_interop_test.py
@@ -102,6 +102,14 @@
         with self.assertRaises(ValueError):
             _ = np.asarray(ary)
 
+    def testIllegalImplicitHostTransferForUfunc(self):
+        init_ary = np.zeros([3, 4], dtype=np.int32) + 2
+        ary = iree.runtime.asdevicearray(self.device, init_ary)
+        with self.assertRaises(ValueError):
+            _ = ary + 1
+        with self.assertRaises(ValueError):
+            _ = np.add(ary, 1)
+
     def testImplicitHostArithmetic(self):
         init_ary = np.zeros([3, 4], dtype=np.int32) + 2
         ary = iree.runtime.asdevicearray(
diff --git a/runtime/bindings/python/typings/iree/_runtime/libs.pyi b/runtime/bindings/python/typings/iree/_runtime/libs.pyi
new file mode 100644
index 0000000..de17185
--- /dev/null
+++ b/runtime/bindings/python/typings/iree/_runtime/libs.pyi
@@ -0,0 +1,5 @@
+from typing import Any
+
+_runtime: Any
+library_path: str
+name: str
diff --git a/runtime/bindings/python/typings/iree/_runtime_libs/__init__.pyi b/runtime/bindings/python/typings/iree/_runtime_libs/__init__.pyi
new file mode 100644
index 0000000..dbf2dc4
--- /dev/null
+++ b/runtime/bindings/python/typings/iree/_runtime_libs/__init__.pyi
@@ -0,0 +1,6 @@
+from typing import Any
+
+__path__: list[str]
+_runtime: Any
+library_path: str
+name: str
diff --git a/runtime/bindings/python/typings/iree/_runtime_libs/version.pyi b/runtime/bindings/python/typings/iree/_runtime_libs/version.pyi
new file mode 100644
index 0000000..dad2728
--- /dev/null
+++ b/runtime/bindings/python/typings/iree/_runtime_libs/version.pyi
@@ -0,0 +1,3 @@
+PACKAGE_SUFFIX: str
+REVISIONS: dict[str, str]
+VERSION: str
diff --git a/runtime/bindings/python/typings/iree/_runtime_libs_tracy/__init__.pyi b/runtime/bindings/python/typings/iree/_runtime_libs_tracy/__init__.pyi
new file mode 100644
index 0000000..dbf2dc4
--- /dev/null
+++ b/runtime/bindings/python/typings/iree/_runtime_libs_tracy/__init__.pyi
@@ -0,0 +1,6 @@
+from typing import Any
+
+__path__: list[str]
+_runtime: Any
+library_path: str
+name: str
diff --git a/runtime/bindings/python/typings/iree/runtime/version.pyi b/runtime/bindings/python/typings/iree/runtime/version.pyi
new file mode 100644
index 0000000..dad2728
--- /dev/null
+++ b/runtime/bindings/python/typings/iree/runtime/version.pyi
@@ -0,0 +1,3 @@
+PACKAGE_SUFFIX: str
+REVISIONS: dict[str, str]
+VERSION: str
diff --git a/runtime/bindings/python/typings/render/cli.pyi b/runtime/bindings/python/typings/render/cli.pyi
new file mode 100644
index 0000000..df152a5
--- /dev/null
+++ b/runtime/bindings/python/typings/render/cli.pyi
@@ -0,0 +1 @@
+def main(argv: list[str] | None = None) -> int: ...