Skip to content

fix(core): compare Ellipsis callable parameters by return type - #12905

Merged
sjrl merged 2 commits into
deepset-ai:mainfrom
Lesereingrape:fix/callable-ellipsis-type-compat
Sep 24, 2026
Merged

sjrl merged 2 commits into
deepset-ai:mainfrom
Lesereingrape:fix/callable-ellipsis-type-compat

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

Related Issues

Proposed Changes:

_check_callable_compatibility compared the two elements of get_args(Callable[...]) — the
parameter list and the return type — but for Callable[..., T] the first element is not a parameter
list, it is the Ellipsis sentinel. Two len() calls then hit that sentinel directly (and a third
builds a stand-in parameter list from it for a bare Callable sender), so every comparison involving
Callable[..., T] raised TypeError: object of type 'ellipsis' has no len() instead of answering.
Users see it as a crash out of Pipeline.connect() when a component exposes or consumes a
Callable[..., T] socket.

The fix keeps the existing structure and treats Ellipsis as what it means — "parameters of any
signature":

  • the return type is still compared first, so Callable[..., str] remains incompatible with
    Callable[[int], int];
  • when either side spells its parameters as Ellipsis, there are no positions to compare, so the
    result is decided by the return type alone, in both directions;
  • a bare Callable sender no longer expands to len(Ellipsis).

Nested cases follow from the recursion in the last line: Callable[[Callable[..., int]], str] is now
compatible with Callable[[Callable[[int], int]], str].

Impact for users: pipelines that annotate a callback socket as Callable[..., T] — the natural way to
write "some callable, signature not pinned here" — can be built again instead of failing at
construction time with an unrelated-looking standard-library error.

How did you test it?

Five cases added to test/core/test_type_utils.py, next to the existing Callable tables, asserting
both directions plus two return-type controls. The commands behind hatch run test:unit / fmt /
test:types were run directly against a Python 3.12 venv with haystack installed from this branch
(see "Notes for the reviewer" for the exact scope):

test on main on this branch
..._is_compatible[ellipsis-callable-to-typed-callable] fail (TypeError: object of type 'ellipsis' has no len(), type_utils.py:214) pass
..._is_compatible[typed-callable-to-ellipsis-callable] fail (same TypeError) pass
..._is_compatible[nested-ellipsis-callable-to-nested-typed-callable] fail (same TypeError) pass
..._incompatible_return_type[ellipsis-callable-to-callable-wrong-return-type] (control) pass pass
..._incompatible_return_type[typed-callable-to-ellipsis-wrong-return-type] (control) pass pass
# branch
pytest test/core/test_type_utils.py                       -> 1085 passed   (1080 baseline + 5 new)
# same test file, haystack/core/type_utils.py reverted to origin/main
pytest test/core/test_type_utils.py -k ellipsis           -> 3 failed, 2 passed

pytest test/core -m "not integration"                     -> 1874 passed, 2 failed
ruff check haystack/core/type_utils.py test/core/test_type_utils.py     -> All checks passed
ruff format --check <same two files>                      -> 2 files already formatted
mypy haystack/core/type_utils.py test/core/test_type_utils.py           -> Success: no issues found
python scripts/release_note_backticks.py --check <note>    -> clean

The two test/core failures (test_show_in_notebook, and one case in
test_serialization_security.py) reproduce identically with both changed files reverted to
origin/main on this machine, so they are pre-existing environment failures rather than fallout from
this change.

End-to-end check of the reported symptom, with the two-component pipeline from the issue:

# main    -> TypeError: object of type 'ellipsis' has no len()
# branch  -> connect() succeeds

Notes for the reviewer

The semantic choice is the one the surrounding code already makes for bare Callable: an unspecified
parameter list is not a constraint to violate, only a set of positions that cannot be compared. The
Ellipsis short-circuit sits after the return-type check on purpose, so return types stay enforced.

Environment note, stated plainly: hatch is not available on this machine, so I did not run
hatch run test:unit / hatch run fmt / hatch run test:types. I ran the tools behind them directly
against a Python 3.12 venv with this checkout installed editable: pytest, the repository's pinned
ruff (v0.16.0, the rev in .pre-commit-config.yaml), mypy on the two changed files, and
scripts/release_note_backticks.py on the new release note. codespell was not run, and pre-commit
hooks are not installed in this clone, so CI will be the first full hook pass.

Scope: only haystack/core/type_utils.py:205-216 changes (6 added lines, 1 modified), plus 5 test
params and the release note. I scanned the 72 open PRs and the recent history of this file for
overlap and found none — the last commits touching it are #12739 and #12737, both in unrelated
functions.

This contribution was made by an AI agent operating this account: the agent located the defect, wrote
the reproduction, the tests and the release note, and measured the before/after numbers quoted above.
No human has read the diff yet, so please review it with that in mind — it can be amended or closed on
request.

Checklist

  • I have read the contributors guidelines and the code of conduct.
  • I have updated the related issue with new insights and changes.
  • I have added unit tests and updated the docstrings. (Tests added; the two comments in the function already describe the fixed behavior, so no docstring change was needed.)
  • I've used one of the conventional commit types for my PR title: fix:, feat:, build:, chore:, ci:, docs:, style:, refactor:, perf:, test: and added ! in case the PR includes breaking changes.
  • I have documented my code.
  • I have added a release note file, following the contributors guidelines.
  • I ran the tools behind the pre-commit hooks that apply to my files (repo-pinned ruff check + ruff format --check, release_note_backticks.py) plus mypy and pytest, all clean; I could not run hatch, codespell or the full hook suite locally — see "How did you test it?" for the exact scope.

`_check_callable_compatibility` applied `len()` to the first element of
`get_args(Callable[...])`, which is `Ellipsis` rather than a parameter list,
so comparing any `Callable[..., T]` raised TypeError instead of answering.
An Ellipsis parameter list now matches parameters of any signature, while
return types are still compared.
@Lesereingrape
Lesereingrape requested a review from a team as a code owner September 24, 2026 01:03
@Lesereingrape
Lesereingrape requested review from sjrl and removed request for a team September 24, 2026 01:03
@vercel

vercel Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

@Lesereingrape is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  haystack/core
  type_utils.py 211
Project Total  

This report was generated by python-coverage-comment-action

Comment thread test/core/test_type_utils.py
Comment thread releasenotes/notes/callable-ellipsis-type-compatibility-4b7c1e9a52d38f60.yaml Outdated
@github-actions github-actions Bot added the type:documentation Improvements on the docs label Sep 24, 2026

@sjrl sjrl 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.

Thanks!

@sjrl
sjrl merged commit cba3db2 into deepset-ai:main Sep 24, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pipeline.connect() raises TypeError: object of type 'ellipsis' has no len() for Callable[..., T] sockets

2 participants