Visitar URL original
ci: add ty type checker and fix existing type errors by timsaucer · Pull Request #1786 · apache/datafusion-python · GitHub
Skip to content

ci: add ty type checker and fix existing type errors - #1786

Open
timsaucer wants to merge 1 commit into
apache:mainfrom
timsaucer:ci/ty-type-checker
Open

timsaucer wants to merge 1 commit into
apache:mainfrom
timsaucer:ci/ty-type-checker

Conversation

@timsaucer

@timsaucer timsaucer commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

No issue filed yet; happy to open one if preferred.

Rationale for this change

This PR improves our type checking on all public facing Python APIs by running the ty type checker, both in CI and as part of pre-commit. Ruff's ANN rules only require annotations to exist. Running ty against python/datafusion reported 82 diagnostics on main; after configuring imports, about 50 were real annotation drift, and one was a runtime bug (@udtf()).

ty is from Astral, like the ruff and uv tooling already used here, and is fast enough to run as a pre-commit hook.

What changes are included in this PR?

Tooling:

Additionally we made fixes for the reported findings.

Are there any user-facing changes?

No API changes. Contributors need ty in their environment (uv sync --dev) for the new pre-commit hook.

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) <noreply@anthropic.com>
left_on = on
right_on = on
# The legacy ``(left, right)`` tuple form was consumed above.
left_on = right_on = cast("str | Sequence[str]", on)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a no-op at runtime but will make it so type checkers recognize that we've already verified the type.

Comment thread python/datafusion/expr.py
Comment on lines +414 to +421
@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]: ...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These overloads make it so type checkers understand that if a None if fed in you get a None out, otherwise you get a valid list of Expr

@timsaucer
timsaucer marked this pull request as ready for review October 7, 2026 16:09
@timsaucer
timsaucer requested a balanced review from Copilot October 8, 2026 12:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

6 open findings
What changed in this PR

Adds Astral’s ty type checker to improve static type safety for the published python/datafusion package, and updates Python APIs/CI tooling to satisfy the newly enforced checks.

Changes:

  • Add ty configuration + a pinned ty==0.0.84 dev dependency, and run ty check in CI and pre-commit.
  • Tighten/clarify Python API typing via overloads and minor refactors (e.g., expression/sort list helpers).
  • Fix a few runtime/type issues discovered by ty (e.g., UDF/UDWF constructor validation, join typing).
File Description
python/​datafusion/​user_defined.py Adds overloads + runtime validation for UDF/UDWF construction; refines decorator typings; adjusts table UDF decorator signature.
python/​datafusion/​plan.py Fixes __eq__ signature to accept object (typing-correct equality).
python/​datafusion/​functions/​spark.py Changes _coerce_i32 to disallow None and always return Expr.
python/​datafusion/​functions/​__init__.py Renames intermediate variables (raw_args) for clearer typing and intent.
python/​datafusion/​expr.py Adds overloads for list conversion helpers; adjusts deprecated import logic; adds ty ignores where intended.
python/​datafusion/​dataframe.py Improves join typing with cast; refactors repartition expr conversion; minor cleanup.
python/​datafusion/​context.py Normalizes file_compression_type to a string for internal calls; updates deprecated import logic.
python/​datafusion/​catalog.py Updates deprecated import logic for Python 3.13+ compatibility.
python/​datafusion/​__init__.py Simplifies metadata import now that stdlib importlib.metadata is assumed available.
pyproject.toml Adds ty config + allowed-unresolved-imports; pins ty==0.0.84 in dev.
.pre-commit-config.yaml Adds a local ty pre-commit hook.
.github/​workflows/​build.yml Runs ty check in the lint-python job.

🧠 Review effort: Lite


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment on lines 1384 to 1386
*,
with_session: bool = False,
) -> Callable[[Callable[..., Any]], TableFunction]:

- name: Run ty
run: |
uv run --no-project ty check --output-format github
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 "",
file_extension=file_extension,
table_partition_cols=table_partition_cols,
file_compression_type=file_compression_type,
file_compression_type=file_compression_type or "",
Comment on lines +59 to 65
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.
"""
Comment on lines +59 to 65
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.
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants