Task.__call__ is typed to return the wrapped callable T, not its return value

Open
#1,073 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
52/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet
Tech stack
python
Domain
tooling

Research direction

Start with invoke.tasks.Task.call and run mypy repro.py to reproduce the incorrect revealed and assigned types. Review the related typing work in #1061 and #1067 before changing the signature; done means the example's result is inferred as int and the integer assignment type-checks without masking the issue with Any.

Written by the indexing model from the issue text.

Description

Summary

invoke.tasks.Task is generic over the wrapped callable (T = TypeVar("T", bound=Callable)), and Task.__call__ is annotated to return T:

class Task(Generic[T]):
    def __call__(self, *args: Any, **kwargs: Any) -> T:
        ...
        result = self.body(*args, **kwargs)
        self.times_called += 1
        return result

At runtime, __call__ returns result — i.e. the return value of the wrapped body — but the annotation says it returns T, the callable itself. As a result, any code that calls a Task[...] with a concrete T and uses the return value is mistyped: the call site sees the function type instead of the function's return type.

(This is distinct from #1067, which is about the task() decorator's own return type being underspecified, and from #1061. The bare @task decorator currently erases to Any, which happens to mask this __call__ issue; it surfaces as soon as T is bound to a concrete callable.)

Steps to reproduce

repro.py:

from collections.abc import Callable

from invoke.context import Context
from invoke.tasks import Task


def add(c: Context, x: int, y: int) -> int:
    return x + y


t: Task[Callable[[Context, int, int], int]] = Task(add)
reveal_type(t.__call__)
result = t(Context(), 1, 2)
reveal_type(result)
n: int = t(Context(), 1, 2)

Run mypy repro.py:

repro.py:12: note: Revealed type is "def (*args: Any, **kwargs: Any) -> def (invoke.context.Context, int, int) -> int"
repro.py:14: note: Revealed type is "def (invoke.context.Context, int, int) -> int"
repro.py:15: error: Incompatible types in assignment (expression has type "Callable[[Context, int, int], int]", variable has type "int")  [assignment]

Run python repro.py (same Task, calling it):

3 int
Expected behavior

Calling a task should type-check as returning the body's return value. For the example above, result should be int, and n: int = t(Context(), 1, 2) should be accepted.

Actual behavior

t(...) is typed as returning the wrapped callable (Callable[[Context, int, int], int]), so the result can't be used as its real (runtime) type without a cast or Any.

Suggested direction

Because Task is parameterised over the callable type rather than its parameters/return, there's no way to express the real return type today. Parameterising over a ParamSpec and a separate return TypeVar would allow __call__ to be typed correctly, e.g.:

P = ParamSpec("P")
R = TypeVar("R")

class Task(Generic[P, R]):
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ...

(This would be a typing-signature change and likely needs to be coordinated with the related typing work in #1061 / #1067.)

Environment
  • invoke: 2.2.1 (installed via pip into a virtualenv)
  • Python: 3.13.13
  • mypy: 2.1.0
  • OS: macOS (arm64) — but this is platform-independent; it's a static-typing issue in the stubs/annotations.

This issue was investigated and drafted by Claude, and reviewed and filed by me.

Dominant language
Python
Stars
4.8k
Forks
412
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from pyinvoke/invoke

All issues in pyinvoke/invoke

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.