Task.__call__ is typed to return the wrapped callable T, not its return value
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 52/100
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from pyinvoke/invoke
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Similar issues
-
documentation help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
simonw/sqlite-utils#872 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100