Skip to content

fix(event): declare LambdaEventCallback.__code__ read-only so bound methods match the protocol - #7397

Draft
benedikt-bartscher wants to merge 8 commits into
reflex-dev:mainfrom
benedikt-bartscher:lambda-callback-readonly-code
Draft

benedikt-bartscher wants to merge 8 commits into
reflex-dev:mainfrom
benedikt-bartscher:lambda-callback-readonly-code

Conversation

@benedikt-bartscher

@benedikt-bartscher benedikt-bartscher commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

LambdaEventCallback declares __code__: types.CodeType. A plain annotation on a
protocol means a settable attribute, but a bound method's __code__ is a read-only
property on types.MethodType that reads its function's. So under the typing spec a
bound method never satisfies the protocol, and passing one to an event trigger, for
example reflex's own ClientStateVar.set_value as on_change, is rejected by
checkers that enforce this (ty does, pyright does not):

import types
from typing import Any, Protocol


class Settable(Protocol):
    __code__: types.CodeType

    def __call__(self, value: int, /) -> Any: ...


class ReadOnly(Protocol):
    @property
    def __code__(self) -> types.CodeType: ...

    def __call__(self, value: int, /) -> Any: ...


class Obj:
    def meth(self, value: int) -> None: ...


a: Settable = Obj().meth  # ty: invalid-assignment
b: ReadOnly = Obj().meth  # ok

Declaring __code__ as a read-only property keeps functions and lambdas matching and
lets bound methods match as well.

ty rejects bound methods passed as event handlers for two other reasons as well:
astral-sh/ty#4098 (__call__ overloads whose self does not fit the specialization
are not skipped) and astral-sh/ty#4746 (protocol methods with a protocol-typed self
reject valid implementations, fix in astral-sh/ruff#29450). With both fixed, this
change is what lets bound methods through. I checked that on ty 0.0.64 and on current
ty with astral-sh/ruff#29450, each with the astral-sh/ty#4098 fix simulated by keeping
only the [V] overload.

pyright still accepts lambdas, functions and bound methods as event handlers, and
tests/units/test_event.py passes.

Review in cubic

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues remain.

Summary

Changes LambdaEventCallback.__code__ to a read-only property so bound methods can meet that part of the callback contract.

  • Event callbacks can now include bound methods.

Reviews (7) · Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." · Reviewed by Greptile

Comment thread packages/reflex-base/src/reflex_base/event/__init__.py
Comment thread packages/reflex-base/src/reflex_base/event/__init__.py
@codspeed

codspeed Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 4.09%

⚠️ 19 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 147 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_isinstance_container[list_typeddict] 108.8 ms 113.4 ms -4.09%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing benedikt-bartscher:lambda-callback-readonly-code (6949923) with main (4be8b4d)

Open in CodSpeed

@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review October 3, 2026 09:34
@benedikt-bartscher
benedikt-bartscher requested a review from a team as a code owner October 3, 2026 09:34

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

FarhanAliRaza
FarhanAliRaza previously approved these changes Oct 8, 2026
@FarhanAliRaza FarhanAliRaza added the on deck PRs lined up to review / merge next label Oct 9, 2026 — with Claude
@FarhanAliRaza FarhanAliRaza removed the on deck PRs lined up to review / merge next label Oct 13, 2026
@FarhanAliRaza

Copy link
Copy Markdown
Contributor

This change has no observable effect today.

I ran the same probe file against main and this branch with pyright 1.1.411, mypy 1.19.1, and ty 0.0.63, 0.0.73 and 0.0.86. Each checker reports the same diagnostics on both. The probe covers functions, lambdas, bound methods, ClientStateVar.set_value, classmethods and staticmethods. It assigns them to LambdaEventCallback[...] and also passes them through rx.input(on_change=...).

The read-only __code__ is correct in principle. When a protocol's __call__ has no self annotation, ty 0.0.86 rejects a bound method with "the member does not accept writes of type CodeType". The property removes that error.

But LambdaEventCallback.__call__ uses self: "LambdaEventCallback[V]" overloads. To simulate a fix for astral-sh/ty#4098, I kept only the overload that fits [str]. With that change, ty 0.0.86 and 0.0.73 accept plain functions. They still reject lambdas and bound methods on __call__, with and without this change:

import types
from typing import Any, Protocol, TypeVar, TypeVarTuple

from reflex.vars.base import Var

P = TypeVarTuple("P")
V = TypeVar("V")


class Settable(Protocol[*P]):
    __code__: types.CodeType

    def __call__(self: "Settable[V]", value: Var[V], /) -> Any: ...


class ReadOnly(Protocol[*P]):
    @property
    def __code__(self) -> types.CodeType: ...

    def __call__(self: "ReadOnly[V]", value: Var[V], /) -> Any: ...


class H:
    def one(self, value: Var[str]) -> None: ...


def fn(value: Var[str]) -> None: ...


a: Settable[str] = fn  # ok
b: ReadOnly[str] = fn  # ok
c: Settable[str] = H().one  # ty: `__call__` is incompatible
d: ReadOnly[str] = H().one  # ty: `__call__` is incompatible

So I could not reproduce "This change covers what is left for bound methods once that is fixed."

Please show a ty build where a bound method passes with this change and fails without it, or narrow that claim. The news fragment says bound methods now match the event handler type. No released checker shows that, so please reword it.

@FarhanAliRaza
FarhanAliRaza dismissed their stale review October 13, 2026 15:24

This change has no observable effect with current type checkers. See #7397 (comment)

No released type checker shows bound methods newly matching the event
handler type yet: ty still rejects them through astral-sh/ty#4098 and
astral-sh/ty#4746. File the fragment as misc and state only that
__code__ is now declared read-only.
@benedikt-bartscher

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough probe. You're right that this has no observable effect today. I reproduced your results, also on ty 0.0.89.

In your astral-sh/ty#4098 simulation, bound methods still fail on __call__ because of a second, unrelated ty regression. Since ty 0.0.65, a protocol method whose self is annotated with the protocol itself rejects valid implementations, including a plain class:

class HasMeth(Protocol):
    def meth(self: "HasMeth", value: int, /) -> Any: ...


class Impl:
    def meth(self, value: int) -> None: ...


x: HasMeth = Impl()  # ty >= 0.0.65: invalid-assignment

For a callback protocol, every callable except a plain def is rejected: bound methods, lambdas, callable instances and partial. I bisected it to astral-sh/ruff#27301, filed astral-sh/ty#4746, and opened a fix in astral-sh/ruff#29450.

The builds you asked for:

  • ty 0.0.63/0.0.64 on your own probe: c: Settable[str] = H().one fails, d: ReadOnly[str] = H().one passes.
  • Reflex's LambdaEventCallback reduced to the [V] overload: bound methods, classmethods and ClientStateVar.set_value fail on main and pass on this branch. That holds as a direct assignment and through rx.input(on_change=...), on ty 0.0.64 and on current ty with [ty] Fix protocol methods whose receiver is annotated with the protocol astral-sh/ruff#29450 applied.

So this change shows once both astral-sh/ty#4098 and astral-sh/ty#4746 are fixed. I've narrowed the description to say that and reworded the fragment.

@benedikt-bartscher
benedikt-bartscher marked this pull request as draft October 17, 2026 16:54

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants