Skip to content

[Type Enhance] The improvement of static check of ray.method #59303

Open

Description

@HAOCHENYE

Description

I'm new to Ray, but while reading the docs and source code I noticed that the typing story for actor methods could be improved.
The official doc recommends decorating actor methods with @ray.method so that static checkers give better auto-completion / type checking for .remote() calls. This works for the simplest cases, yet breaks as soon as the method has default arguments.

Minimal reproducer

import ray

@ray.method
def f2(self, b: int = 1) -> bool:
    return False

fn = f2          # pyright / mypy infer the type here

Pyright infers fn as _RemoteMethodNoArgs, i.e. a remote method that accepts no arguments (see actor.py#L93).
Consequently fn.remote(5) is flagged by the type checker although it is perfectly valid at runtime.

Root cause
The current stubs use overloads on plain Callable types:

@overload
def method(__method: Callable[[Any, T0], Ret]) -> bool: ...
@overload
def method(__method: Callable[[Any], Ret]) -> float: ...

A function with a default argument (self, b: int = 1) matches both overloads; which one is chosen depends only on declaration order, so the no-argument version wins and the information about the optional parameter is lost.

Possible improvement for modern Python (≥3.10)
We can keep the same runtime behaviour while giving exact static types:

from typing import TypeVar, Generic, Concatenate, ParamSpec, Callable

P = ParamSpec("P")
T = TypeVar("T")
C = TypeVar("C")          # the actor class

class RemoteMethod(Generic[P, T]):
    def remote(self, *args: P.args, **kwargs: P.kwargs) -> T: ...
    def bind(self,  *args: P.args, **kwargs: P.kwargs) -> Any: ...

def ray_method(f: Callable[Concatenate[C, P], T]) -> RemoteMethod[P, T]:
    import ray
    return ray.method(f)          # runtime delegate

This preserves the signature of the original method (including defaults) inside the RemoteMethod instance, so type checkers know exactly which arguments .remote() accepts.

Suggestion
Would the team accept a PR that adds ParamSpec-based overloads for @ray.method when Python version is satisfied?, while keeping the current overloads for older versions?

Use case

No response

Activity

  1. added
    enhancementRequest for new feature and/or capability
    triageNeeds triage (eg: priority, bug/not-bug, and owning component)
    on Dec 9, 2025
  2. added
    P2Important issue, but not time-critical
    contribution-welcomeCommunity contributions for this issue will be welcomed and prioritized for review.
    and removed
    triageNeeds triage (eg: priority, bug/not-bug, and owning component)
    on Dec 12, 2025
  3. edoakes commented on Dec 12, 2025

    @edoakes
    Collaborator

    Contributions in this area are definitely welcome! Note that we just stopped releasing Python 3.9 wheels, so Python >= 3.10 changes can be done safely by default.

    I'll defer to @SolitaryThinker on the details of your proposed change

  4. moved this from Todo to In progress in Ray Data OSSon Feb 5, 2026
  5. hg1112 commented on Jul 30, 2026

    @hg1112

    Thanks for the clear repro and root-cause analysis — I dug into this before starting a PR and wanted to share findings, since they change the shape of the fix a bit.

    Confirmed the bug end-to-end, both statically and at runtime:

    • mypy and pyright both reject fn.remote(5) for a method with a default arg, matching the _RemoteMethodNoArgs inference described above.
    • A real @ray.remote actor with the equivalent method executes the call correctly at runtime — confirmed false positive, not a behavioral bug.
    • Verified against the current ray/actor.py: method()'s stub is ~20 hand-written @overloads, one pair (sync + Awaitable) per fixed argument count, smallest arity declared first. A method with a default arg is structurally callable with fewer args, so it matches multiple buckets and the checker silently picks the smaller one, discarding the optional parameter.

    Also validated the proposed ParamSpec direction — and it holds up. I built an isolated, minimal repro comparing three approaches side by side on the same test functions, checked with both mypy and pyright:

    # 1. ParamSpec + Generic — works
    class RemoteMethod(Generic[P, R]):
        def __init__(self, func: Callable[P, R]) -> None: self._func = func
        def remote(self, *args: P.args, **kwargs: P.kwargs) -> R: return self._func(*args, **kwargs)
    
    def method(func: Callable[P, R]) -> RemoteMethod[P, R]:
        return RemoteMethod[P, R](func)
    
    @method
    def f2(x: int, y: int = 5) -> bool: ...
    f2.remote(1)      # correctly accepted — default preserved
    
    # 2. @overload, fixed-arity buckets (mirrors the CURRENT approach) — reproduces the bug
    @overload
    def method_overload(func: Callable[[T0], Ret]) -> _RemoteMethod0[T0, Ret]: ...
    @overload
    def method_overload(func: Callable[[T0, T1], Ret]) -> _RemoteMethod1[T0, T1, Ret]: ...
    
    @method_overload
    def f4(x: int, y: int = 5) -> bool: ...
    f4.remote(1, 2)   # incorrectly REJECTED — "too many arguments" — reproduces the real bug
    
    # 3. Concatenate — strips `self`, matching real actor methods — also works
    def actor_method(func: Callable[Concatenate[Any, P], R]) -> RemoteMethod[P, R]:
        def bound(*args: P.args, **kwargs: P.kwargs) -> R: return func(None, *args, **kwargs)
        return RemoteMethod(bound)
    
    class MyActor:
        @actor_method
        def f6(self, x: int, y: int = 5) -> bool: ...
    MyActor.f6.remote(1)   # correctly accepted, even with `self` stripped

    Section 2 reproduces the exact failure mode from this issue in isolation; sections 1 and 3 both type-check clean on mypy and pyright, including with self stripped via Concatenate the way an actual actor method needs. So the ParamSpec-based direction looks solid — happy to draft a PR replacing the fixed-arity overload buckets with this approach (gated to Python ≥3.10 per @edoakes's note above, keeping the current overloads for older versions).

    One implementation detail worth flagging early: binding self away needs to happen with a real runtime wrapper (a closure/functools.partial, matching how RemoteMethod-style objects would need to be constructed per actor instance) — annotating it away with Concatenate alone isn't enough, the checker correctly catches that mismatch if you try.

    Does this direction sound right before I put together the PR, or is there a preference for a different structure?

    cc @edoakes @SolitaryThinker

  6. dyami-andrews-e3 commented on Sep 9, 2026

    @dyami-andrews-e3

    Hello All! Just bumping this as I would love to see this included in a future ray release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issue, but not time-criticalcommunity-backlogcontribution-welcomeCommunity contributions for this issue will be welcomed and prioritized for review.coreIssues that should be addressed in Ray CoreenhancementRequest for new feature and/or capabilityusability

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions