You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by ti...@apache.org on 2026/10/08 17:44:33 UTC

(datafusion-python) branch main updated: ci: add ty type checker and fix existing type errors (#1786)

This is an automated email from the ASF dual-hosted git repository.

timsaucer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 5febeb36 ci: add ty type checker and fix existing type errors (#1786)
5febeb36 is described below

commit 5febeb36e2cac4ad15aab51fe1671957a3ff7dbe
Author: Tim Saucer <ti...@gmail.com>
AuthorDate: Thu Oct 8 13:44:29 2026 -0400

    ci: add ty type checker and fix existing type errors (#1786)
    
    Add ty (pinned to 0.0.84, since it is pre-1.0) to the dev dependency
    group, configure it in pyproject.toml to check python/datafusion, and
    run it in the lint-python CI job and as a local pre-commit hook.
    datafusion._internal ships no stubs, and pandas/polars are optional
    TYPE_CHECKING-only imports, so they are allowed to stay unresolved.
    
    Fix the diagnostics ty reported:
    
    - Make the internal udtf decorator helper require `name`, matching the
      public overloads. `@udtf()` previously passed None into Rust and
      failed with "'None' is not an instance of 'str'".
    - Give AggregateUDF.__init__ defaults matching its FFI overload, and add
      the same FFI overload to ScalarUDF and WindowUDF.
    - Add None/non-None overloads to expr_list_to_raw_expr_list and
      sort_list_to_raw_sort_list so callers that unpack the result type
      check.
    - Stop rebinding typed *args and parameters to values of other types.
    - Import warnings.deprecated behind a sys.version_info check and drop
      the unreachable importlib_metadata fallback.
    - Fix smaller annotation mismatches (LogicalPlan.__eq__,
      spark._coerce_i32, CSV file_compression_type).
    
    Co-authored-by: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
 .github/workflows/build.yml             |  4 ++
 .pre-commit-config.yaml                 |  7 +++
 pyproject.toml                          | 12 +++++
 python/datafusion/__init__.py           |  6 +--
 python/datafusion/catalog.py            |  9 ++--
 python/datafusion/context.py            | 14 +++---
 python/datafusion/dataframe.py          | 24 +++++-----
 python/datafusion/expr.py               | 39 +++++++++++++----
 python/datafusion/functions/__init__.py | 24 +++++-----
 python/datafusion/functions/spark.py    |  6 +--
 python/datafusion/plan.py               |  2 +-
 python/datafusion/user_defined.py       | 77 ++++++++++++++++++++++++++-------
 uv.lock                                 | 27 ++++++++++++
 13 files changed, 184 insertions(+), 67 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e3733344..7b926a9e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -81,6 +81,10 @@ jobs:
           uv run --no-project ruff check --output-format=github python/
           uv run --no-project ruff format --check python/
 
+      - name: Run ty
+        run: |
+          uv run --no-project ty check --output-format github
+
       - name: Run codespell
         run: |
           uv run --no-project codespell --toml pyproject.toml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0a212480..9419fc3c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -30,6 +30,13 @@ repos:
           - id: ruff-format
       - repo: local
         hooks:
+          - id: ty
+            name: ty
+            description: Type check python/datafusion with ty.
+            entry: uv run --no-project ty check
+            pass_filenames: false
+            types: [file, python]
+            language: system
           - id: rust-fmt
             name: Rust fmt
             description: Run cargo fmt on files included in the commit. rustfmt should be installed before-hand.
diff --git a/pyproject.toml b/pyproject.toml
index 604d3f84..b115b119 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -181,6 +181,16 @@ extend-allowed-calls = ["datafusion.lit", "lit"]
 
 # CI and pre-commit invoke codespell with different paths, so we have a little
 # redundancy here, and we intentionally drop python in the path.
+[tool.ty.src]
+# Only the published package is type checked for now. Tests and examples
+# can be added once the package itself is clean.
+include = ["python/datafusion"]
+
+[tool.ty.analysis]
+# `_internal` is the compiled PyO3 extension and ships no `.pyi` stubs.
+# pandas and polars are optional and only imported under TYPE_CHECKING.
+allowed-unresolved-imports = ["datafusion._internal", "pandas", "polars"]
+
 [tool.codespell]
 skip = [
   "*/tests/test_functions.py",
@@ -219,6 +229,8 @@ dev = [
   "pyyaml>=6.0.3",
   "ruff>=0.15.1",
   "toml>=0.10.2",
+  # Pinned exactly: ty is pre-1.0 and new releases can add diagnostics.
+  "ty==0.0.84",
 ]
 # Release tooling only. Kept out of `dev` because pygithub pulls in
 # cryptography, which ships no free-threaded wheel and fails to build
diff --git a/python/datafusion/__init__.py b/python/datafusion/__init__.py
index 76f64779..087b2767 100644
--- a/python/datafusion/__init__.py
+++ b/python/datafusion/__init__.py
@@ -57,13 +57,9 @@ https://github.com/apache/datafusion-python/blob/main/skills/datafusion_python/S
 
 from __future__ import annotations
 
+import importlib.metadata as importlib_metadata
 from typing import Any
 
-try:
-    import importlib.metadata as importlib_metadata
-except ImportError:
-    import importlib_metadata  # type: ignore[import]
-
 # Public submodules
 from . import functions, ipc, object_store, substrait, unparser
 
diff --git a/python/datafusion/catalog.py b/python/datafusion/catalog.py
index d1d50117..9161c62f 100644
--- a/python/datafusion/catalog.py
+++ b/python/datafusion/catalog.py
@@ -19,6 +19,7 @@
 
 from __future__ import annotations
 
+import sys
 from abc import ABC, abstractmethod
 from typing import TYPE_CHECKING, Any, Protocol
 
@@ -31,10 +32,10 @@ if TYPE_CHECKING:
     from datafusion.context import TableProviderExportable
     from datafusion.expr import CreateExternalTable
 
-try:
-    from warnings import deprecated  # Python 3.13+
-except ImportError:
-    from typing_extensions import deprecated  # Python 3.12
+if sys.version_info >= (3, 13):
+    from warnings import deprecated
+else:
+    from typing_extensions import deprecated
 
 
 __all__ = [
diff --git a/python/datafusion/context.py b/python/datafusion/context.py
index c663dea2..08dbc440 100644
--- a/python/datafusion/context.py
+++ b/python/datafusion/context.py
@@ -44,14 +44,15 @@ execution model.
 
 from __future__ import annotations
 
+import sys
 import uuid
 import warnings
 from typing import TYPE_CHECKING, Any, Protocol
 
-try:
-    from warnings import deprecated  # Python 3.13+
-except ImportError:
-    from typing_extensions import deprecated  # Python 3.12
+if sys.version_info >= (3, 13):
+    from warnings import deprecated
+else:
+    from typing_extensions import deprecated
 
 
 from urllib.parse import urlparse
@@ -85,7 +86,6 @@ from ._internal import expr as expr_internal
 
 if TYPE_CHECKING:
     import pathlib
-    import sys
     from collections.abc import Iterable, Sequence
 
     import pandas as pd
@@ -1292,7 +1292,7 @@ class SessionContext:
                 delimiter=delimiter,
                 schema_infer_max_records=schema_infer_max_records,
                 file_extension=file_extension,
-                file_compression_type=file_compression_type,
+                file_compression_type=file_compression_type or "",
             )
         )
 
@@ -2196,7 +2196,7 @@ class SessionContext:
                 schema_infer_max_records=schema_infer_max_records,
                 file_extension=file_extension,
                 table_partition_cols=table_partition_cols,
-                file_compression_type=file_compression_type,
+                file_compression_type=file_compression_type or "",
             )
         )
 
diff --git a/python/datafusion/dataframe.py b/python/datafusion/dataframe.py
index 8a867a8c..12ab0a3c 100644
--- a/python/datafusion/dataframe.py
+++ b/python/datafusion/dataframe.py
@@ -44,19 +44,21 @@ overview of the execution model.
 
 from __future__ import annotations
 
+import sys
 import warnings
 from collections.abc import AsyncIterator, Iterable, Iterator, Sequence
 from typing import (
     TYPE_CHECKING,
     Any,
     Literal,
+    cast,
     overload,
 )
 
-try:
-    from warnings import deprecated  # Python 3.13+
-except ImportError:
-    from typing_extensions import deprecated  # Python 3.12
+if sys.version_info >= (3, 13):
+    from warnings import deprecated
+else:
+    from typing_extensions import deprecated
 
 from datafusion._internal import DataFrame as DataFrameInternal
 from datafusion._internal import DataFrameWriteOptions as DataFrameWriteOptionsInternal
@@ -1158,8 +1160,8 @@ class DataFrame:
             if left_on is not None or right_on is not None:
                 error_msg = "`left_on` or `right_on` should not provided with `on`"
                 raise ValueError(error_msg)
-            left_on = on
-            right_on = on
+            # The legacy ``(left, right)`` tuple form was consumed above.
+            left_on = right_on = cast("str | Sequence[str]", on)
         elif left_on is not None or right_on is not None:
             if left_on is None or right_on is None:
                 error_msg = "`left_on` and `right_on` should both be provided."
@@ -1344,10 +1346,11 @@ class DataFrame:
         Returns:
             Repartitioned DataFrame.
         """
-        exprs = [self.parse_sql_expr(e) if isinstance(e, str) else e for e in exprs]
-        exprs = expr_list_to_raw_expr_list(exprs)
+        raw_exprs = expr_list_to_raw_expr_list(
+            [self.parse_sql_expr(e) if isinstance(e, str) else e for e in exprs]
+        )
 
-        return DataFrame(self.df.repartition_by_hash(*exprs, num=num))
+        return DataFrame(self.df.repartition_by_hash(*raw_exprs, num=num))
 
     def union(self, other: DataFrame, distinct: bool = False) -> DataFrame:
         """Calculate the union of two :py:class:`DataFrame`.
@@ -1833,10 +1836,9 @@ class DataFrame:
             >>> df.unnest_columns("a", recursions=[("a", "a", 1)]).to_pydict()
             {'a': [1, 2, 3], 'b': ['x', 'x', 'y']}
         """
-        columns = list(columns)
         return DataFrame(
             self.df.unnest_columns(
-                columns, preserve_nulls=preserve_nulls, recursions=recursions
+                list(columns), preserve_nulls=preserve_nulls, recursions=recursions
             )
         )
 
diff --git a/python/datafusion/expr.py b/python/datafusion/expr.py
index bfa124ff..d6cf85bc 100644
--- a/python/datafusion/expr.py
+++ b/python/datafusion/expr.py
@@ -46,13 +46,14 @@ operators and helpers.
 
 from __future__ import annotations
 
+import sys
 from collections.abc import Callable, Iterable, Sequence
-from typing import TYPE_CHECKING, Any, ClassVar
+from typing import TYPE_CHECKING, Any, ClassVar, overload
 
-try:
-    from warnings import deprecated  # Python 3.13+
-except ImportError:
-    from typing_extensions import deprecated  # Python 3.12
+if sys.version_info >= (3, 13):
+    from warnings import deprecated
+else:
+    from typing_extensions import deprecated
 
 import pyarrow as pa
 
@@ -114,7 +115,7 @@ def _create_external_table_location(self: Any) -> str:
     return locations[0] if locations else ""
 
 
-CreateExternalTable.location = _create_external_table_location
+CreateExternalTable.location = _create_external_table_location  # ty: ignore[deprecated]
 
 CreateFunction = expr_internal.CreateFunction
 CreateFunctionBody = expr_internal.CreateFunctionBody
@@ -410,8 +411,18 @@ def _to_raw_expr(value: Expr | str) -> expr_internal.Expr:
     raise TypeError(error)
 
 
+@overload
+def expr_list_to_raw_expr_list(expr_list: None) -> None: ...
+
+
+@overload
+def expr_list_to_raw_expr_list(
+    expr_list: Sequence[Expr | str] | Expr | str,
+) -> list[expr_internal.Expr]: ...
+
+
 def expr_list_to_raw_expr_list(
-    expr_list: list[Expr] | Expr | None,
+    expr_list: Sequence[Expr | str] | Expr | str | None,
 ) -> list[expr_internal.Expr] | None:
     """Convert a sequence of expressions or column names to raw expressions."""
     if isinstance(expr_list, Expr | str):
@@ -428,6 +439,16 @@ def sort_or_default(e: Expr | SortExpr) -> expr_internal.SortExpr:
     return SortExpr(e, ascending=True, nulls_first=False).raw_sort
 
 
+@overload
+def sort_list_to_raw_sort_list(sort_list: None) -> None: ...
+
+
+@overload
+def sort_list_to_raw_sort_list(
+    sort_list: Sequence[SortKey] | SortKey,
+) -> list[expr_internal.SortExpr]: ...
+
+
 def sort_list_to_raw_sort_list(
     sort_list: Sequence[SortKey] | SortKey | None,
 ) -> list[expr_internal.SortExpr] | None:
@@ -771,7 +792,7 @@ class Expr:  # noqa: PLW1641
             return Expr(functions_internal.array_slice(self.expr, start, stop, step))
         return Expr(self.expr.__getitem__(key))
 
-    def __eq__(self, rhs: object) -> Expr:
+    def __eq__(self, rhs: object) -> Expr:  # ty: ignore[invalid-method-override]
         """Equal to.
 
         Accepts either an expression or any valid PyArrow scalar literal value.
@@ -782,7 +803,7 @@ class Expr:  # noqa: PLW1641
             rhs = Expr.literal(rhs)
         return Expr(self.expr.__eq__(rhs.expr))
 
-    def __ne__(self, rhs: object) -> Expr:
+    def __ne__(self, rhs: object) -> Expr:  # ty: ignore[invalid-method-override]
         """Not equal to.
 
         Accepts either an expression or any valid PyArrow scalar literal value.
diff --git a/python/datafusion/functions/__init__.py b/python/datafusion/functions/__init__.py
index d5e146bd..c7d99f78 100644
--- a/python/datafusion/functions/__init__.py
+++ b/python/datafusion/functions/__init__.py
@@ -871,8 +871,8 @@ def concat(*args: Expr) -> Expr:
         >>> result.collect_column("c")[0].as_py()
         'hello world'
     """
-    args = [arg.expr for arg in args]
-    return Expr(f.concat(args))
+    raw_args = [arg.expr for arg in args]
+    return Expr(f.concat(raw_args))
 
 
 def concat_ws(separator: str, *args: Expr) -> Expr:
@@ -888,8 +888,8 @@ def concat_ws(separator: str, *args: Expr) -> Expr:
         >>> result.collect_column("c")[0].as_py()
         'hello-world'
     """
-    args = [arg.expr for arg in args]
-    return Expr(f.concat_ws(separator, args))
+    raw_args = [arg.expr for arg in args]
+    return Expr(f.concat_ws(separator, raw_args))
 
 
 def order_by(expr: Expr, ascending: bool = True, nulls_first: bool = False) -> SortExpr:
@@ -1272,8 +1272,8 @@ def coalesce(*args: Expr) -> Expr:
         >>> result.collect_column("c")[0].as_py()
         2
     """
-    args = [arg.expr for arg in args]
-    return Expr(f.coalesce(*args))
+    raw_args = [arg.expr for arg in args]
+    return Expr(f.coalesce(*raw_args))
 
 
 def cos(arg: Expr) -> Expr:
@@ -3105,8 +3105,8 @@ def make_array(*args: Expr) -> Expr:
         >>> result.collect_column("arr")[0].as_py()
         [1, 2, 3]
     """
-    args = [arg.expr for arg in args]
-    return Expr(f.make_array(args))
+    raw_args = [arg.expr for arg in args]
+    return Expr(f.make_array(raw_args))
 
 
 def make_list(*args: Expr) -> Expr:
@@ -3240,8 +3240,8 @@ def struct(*args: Expr) -> Expr:
         >>> result.collect_column("s")[0].as_py() == {"c0": 1, "c1": 2}
         True
     """
-    args = [arg.expr for arg in args]
-    return Expr(f.struct(*args))
+    raw_args = [arg.expr for arg in args]
+    return Expr(f.struct(*raw_args))
 
 
 def named_struct(name_pairs: list[tuple[str, Expr]]) -> Expr:
@@ -3762,8 +3762,8 @@ def array_concat(*args: Expr) -> Expr:
         >>> result.collect_column("result")[0].as_py()
         [1, 2, 3, 4]
     """
-    args = [arg.expr for arg in args]
-    return Expr(f.array_concat(args))
+    raw_args = [arg.expr for arg in args]
+    return Expr(f.array_concat(raw_args))
 
 
 def array_cat(*args: Expr) -> Expr:
diff --git a/python/datafusion/functions/spark.py b/python/datafusion/functions/spark.py
index c4002670..74315fb4 100644
--- a/python/datafusion/functions/spark.py
+++ b/python/datafusion/functions/spark.py
@@ -56,14 +56,14 @@ def _filter_raw(filter: Expr | None) -> Any:
     return filter.expr if filter is not None else None
 
 
-def _coerce_i32(value: Expr | int | None) -> Expr | None:
-    """Coerce a native ``int`` to an int32 literal, passing ``Expr``/``None`` through.
+def _coerce_i32(value: Expr | int) -> Expr:
+    """Coerce a native ``int`` to an int32 literal, passing ``Expr`` through.
 
     Several Spark datetime and interval builders require 32-bit integer
     inputs, so a bare ``int`` must become an int32 literal rather than the
     int64 default that :meth:`Expr.literal` would produce.
     """
-    if value is None or isinstance(value, Expr):
+    if isinstance(value, Expr):
         return value
     return Expr.literal(pa.scalar(value, type=pa.int32()))
 
diff --git a/python/datafusion/plan.py b/python/datafusion/plan.py
index 8b61c097..ffad3862 100644
--- a/python/datafusion/plan.py
+++ b/python/datafusion/plan.py
@@ -143,7 +143,7 @@ class LogicalPlan:  # noqa: PLW1641
         )
         return self.to_bytes()
 
-    def __eq__(self, other: LogicalPlan) -> bool:
+    def __eq__(self, other: object) -> bool:
         """Test equality."""
         if not isinstance(other, LogicalPlan):
             return False
diff --git a/python/datafusion/user_defined.py b/python/datafusion/user_defined.py
index 1d8ccbe3..c2624f61 100644
--- a/python/datafusion/user_defined.py
+++ b/python/datafusion/user_defined.py
@@ -197,6 +197,7 @@ class ScalarUDF:
     operating on a group of rows.
     """
 
+    @overload
     def __init__(
         self,
         name: str,
@@ -204,6 +205,25 @@ class ScalarUDF:
         input_fields: list[pa.Field],
         return_field: pa.Field,
         volatility: Volatility | str,
+    ) -> None: ...
+
+    @overload
+    def __init__(
+        self,
+        name: str,
+        func: ScalarUDFExportable,
+        input_fields: None = ...,
+        return_field: None = ...,
+        volatility: None = ...,
+    ) -> None: ...
+
+    def __init__(
+        self,
+        name: str,
+        func: Callable[..., _R] | ScalarUDFExportable,
+        input_fields: list[pa.Field] | None = None,
+        return_field: pa.Field | None = None,
+        volatility: Volatility | str | None = None,
     ) -> None:
         """Instantiate a scalar user-defined function (UDF).
 
@@ -212,6 +232,12 @@ class ScalarUDF:
         if hasattr(func, "__datafusion_scalar_udf__"):
             self._udf = df_internal.ScalarUDF.from_pycapsule(func)
             return
+        if input_fields is None or return_field is None or volatility is None:
+            msg = (
+                "`input_fields`, `return_field`, and `volatility` "
+                "must be provided when `func` is callable."
+            )
+            raise TypeError(msg)
         if isinstance(input_fields, pa.DataType):
             input_fields = [input_fields]
         self._udf = df_internal.ScalarUDF(
@@ -294,7 +320,7 @@ class ScalarUDF:
     def udf(func: _PyCapsule) -> ScalarUDF: ...
 
     @staticmethod
-    def udf(*args: Any, **kwargs: Any):  # noqa: D417, C901
+    def udf(*args: Any, **kwargs: Any):  # noqa: D417
         """Create a new User-Defined Function (UDF).
 
         This class can be used both as either a function or a decorator.
@@ -365,10 +391,7 @@ class ScalarUDF:
                 msg = "`func` argument must be callable"
                 raise TypeError(msg)
             if name is None:
-                if hasattr(func, "__qualname__"):
-                    name = func.__qualname__.lower()
-                else:
-                    name = func.__class__.__name__.lower()
+                name = getattr(func, "__qualname__", func.__class__.__name__).lower()
             input_fields = data_types_or_fields_to_field_list(input_fields)
             return_field = data_type_or_field_to_field(return_field, "value")
             return ScalarUDF(
@@ -391,7 +414,7 @@ class ScalarUDF:
                 )
 
                 @functools.wraps(func)
-                def wrapper(*args: Any, **kwargs: Any) -> Callable:
+                def wrapper(*args: Any, **kwargs: Any) -> Expr:
                     return udf_caller(*args, **kwargs)
 
                 return wrapper
@@ -509,10 +532,10 @@ class AggregateUDF:
         self,
         name: str,
         accumulator: Callable[[], Accumulator] | AggregateUDFExportable,
-        input_types: list[pa.DataType] | None,
-        return_type: pa.DataType | None,
-        state_type: list[pa.DataType] | None,
-        volatility: Volatility | str | None,
+        input_types: list[pa.DataType] | None = None,
+        return_type: pa.DataType | None = None,
+        state_type: list[pa.DataType] | None = None,
+        volatility: Volatility | str | None = None,
     ) -> None:
         """Instantiate a user-defined aggregate function (UDAF).
 
@@ -959,6 +982,7 @@ class WindowUDF:
     also :py:class:`ScalarUDF` for operating on a row by row basis.
     """
 
+    @overload
     def __init__(
         self,
         name: str,
@@ -966,6 +990,25 @@ class WindowUDF:
         input_types: list[pa.DataType],
         return_type: pa.DataType,
         volatility: Volatility | str,
+    ) -> None: ...
+
+    @overload
+    def __init__(
+        self,
+        name: str,
+        func: WindowUDFExportable,
+        input_types: None = ...,
+        return_type: None = ...,
+        volatility: None = ...,
+    ) -> None: ...
+
+    def __init__(
+        self,
+        name: str,
+        func: Callable[[], WindowEvaluator] | WindowUDFExportable,
+        input_types: list[pa.DataType] | None = None,
+        return_type: pa.DataType | None = None,
+        volatility: Volatility | str | None = None,
     ) -> None:
         """Instantiate a user-defined window function (UDWF).
 
@@ -975,6 +1018,12 @@ class WindowUDF:
         if hasattr(func, "__datafusion_window_udf__"):
             self._udwf = df_internal.WindowUDF.from_pycapsule(func)
             return
+        if input_types is None or return_type is None or volatility is None:
+            msg = (
+                "`input_types`, `return_type`, and `volatility` "
+                "must be provided when `func` is callable."
+            )
+            raise TypeError(msg)
         self._udwf = df_internal.WindowUDF(
             name, func, input_types, return_type, str(volatility)
         )
@@ -1134,7 +1183,7 @@ class WindowUDF:
             msg = "`func` must implement the abstract base class WindowEvaluator"
             raise TypeError(msg)
 
-        name = name or func.__qualname__.lower()
+        name = name or WindowUDF._get_default_name(func)
         input_types = (
             [input_types] if isinstance(input_types, pa.DataType) else input_types
         )
@@ -1144,9 +1193,7 @@ class WindowUDF:
     @staticmethod
     def _get_default_name(func: Callable) -> str:
         """Get the default name for a function based on its attributes."""
-        if hasattr(func, "__qualname__"):
-            return func.__qualname__.lower()
-        return func.__class__.__name__.lower()
+        return getattr(func, "__qualname__", func.__class__.__name__).lower()
 
     @staticmethod
     def _normalize_input_types(
@@ -1333,7 +1380,7 @@ class TableFunction:
 
     @staticmethod
     def _create_table_udf_decorator(
-        name: str | None = None,
+        name: str,
         *,
         with_session: bool = False,
     ) -> Callable[[Callable[..., Any]], TableFunction]:
diff --git a/uv.lock b/uv.lock
index 49982971..b1bf3c5e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -505,6 +505,7 @@ dev = [
     { name = "pyyaml" },
     { name = "ruff" },
     { name = "toml" },
+    { name = "ty" },
 ]
 docs = [
     { name = "jinja2" },
@@ -549,6 +550,7 @@ dev = [
     { name = "pyyaml", specifier = ">=6.0.3" },
     { name = "ruff", specifier = ">=0.15.1" },
     { name = "toml", specifier = ">=0.10.2" },
+    { name = "ty", specifier = "==0.0.84" },
 ]
 docs = [
     { name = "jinja2", specifier = ">=3.1.5" },
@@ -2937,6 +2939,31 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" },
 ]
 
+[[package]]
+name = "ty"
+version = "0.0.84"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" },
+    { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" },
+    { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" },
+    { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" },
+    { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" },
+    { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" },
+    { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" },
+    { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" },
+    { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" },
+    { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" },
+    { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" },
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.15.0"


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org