Skip to content

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

@allanlewis

Description

@allanlewis

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions