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):
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.
Summary
invoke.tasks.Taskis generic over the wrapped callable (T = TypeVar("T", bound=Callable)), andTask.__call__is annotated to returnT:At runtime,
__call__returnsresult— i.e. the return value of the wrapped body — but the annotation says it returnsT, the callable itself. As a result, any code that calls aTask[...]with a concreteTand 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@taskdecorator currently erases toAny, which happens to mask this__call__issue; it surfaces as soon asTis bound to a concrete callable.)Steps to reproduce
repro.py:Run
mypy repro.py:Run
python repro.py(sameTask, calling it):Expected behavior
Calling a task should type-check as returning the body's return value. For the example above,
resultshould beint, andn: 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 orAny.Suggested direction
Because
Taskis parameterised over the callable type rather than its parameters/return, there's no way to express the real return type today. Parameterising over aParamSpecand a separate returnTypeVarwould allow__call__to be typed correctly, e.g.:(This would be a typing-signature change and likely needs to be coordinated with the related typing work in #1061 / #1067.)
Environment
This issue was investigated and drafted by Claude, and reviewed and filed by me.