Summary
On Python 3.14, reflex_base resolves an event handler's parameter annotations lazily via get_type_hints(). Under PEP 649, a method's deferred annotations are evaluated against its owning class's own namespace, so any attribute assigned onto that class shadows a same-named builtin or global inside its annotations.
The practical result: a handler annotated event: dict on a state class that has a dict attribute resolves dict to that attribute — a function object — instead of the builtin type. Event-arg checking then blows up:
TypeError: Could not compare types <class 'dict'> and
<function install_delta_filter.<locals>.filtered_dict> for argument event of ChartState.on_point
This passes on 3.10–3.13 and fails on 3.14 with no source change.
Minimal reproduction (no Reflex required)
This is pure CPython behavior — the four arrangements below isolate exactly which one changed:
import typing
def patched(self, *a, **k): return {}
class Base:
def dict(self, *a, **k): return {}
class Sub(Base):
def on_point(self, event: dict): ...
Sub.dict = patched # runtime assignment onto the owning class
print(typing.get_type_hints(Sub.on_point)['event'])
| arrangement |
3.11 |
3.14 |
dict method in the same class body |
A.dict |
A.dict |
| runtime-patched onto the class owning the handler |
<class 'dict'> |
patched ← changed |
| runtime-patched onto the base class |
<class 'dict'> |
<class 'dict'> |
class-body dict, then patched |
original fn |
patched |
Only the owning class's namespace is consulted; patching a base class is unaffected. On ≤3.13 the annotation was evaluated eagerly at def time, so later assignment was invisible.
Where it fails in reflex_base
Observed with reflex-base==0.9.6 on CPython 3.14 (ubuntu-24.04 and windows-2022 alike):
reflex_base/event/__init__.py:1933 / :1971 — get_type_hints(event_callback.handler.fn) builds callback_param_name_to_type, which comes back as {'event': <function ...>}.
reflex_base/event/__init__.py:1846 — _check_event_args_subclass_of_callback wraps the failure into Could not compare types ....
reflex_base/utils/types.py:1028 — the underlying raise is issubclass() arg 2 must be a class, a tuple of classes, or a union from typehint_issubclass.
Impact
Any state class that gets an attribute assigned onto it at runtime will mis-resolve annotations naming that attribute. dict is the sharp edge, since it is both a builtin type and a BaseState method name, and event: dict is an extremely common handler signature.
We hit this in reflex-enterprise, where the auth plugin installs a delta filter with state_cls.dict = filtered_dict; every event: dict handler on a swept state then fails to compile. Anything else that assigns onto a state class will trip the same wire.
Suggested direction
The shadowing is baked into the function's __annotate__ closure, so passing localns/globalns at resolution time cannot undo it. Options that would work:
- Resolve and cache handler annotations eagerly, at
@rx.event decoration / EventHandler construction time, before anything can assign onto the class. This also removes the ordering sensitivity entirely.
- Degrade gracefully in
typehint_issubclass: when a resolved "type" is not a class/union, treat it as unknown rather than raising, so a mis-resolution warns instead of breaking compilation.
(1) is the real fix; (2) is worth having regardless, since the current failure surfaces as an opaque TypeError far from the cause.
Related
Same underlying class of bug as #2848 and #918, which were about from __future__ import annotations. PEP 649 effectively makes every module behave that way on 3.14, so this will resurface broadly as people move to 3.14.
Summary
On Python 3.14,
reflex_baseresolves an event handler's parameter annotations lazily viaget_type_hints(). Under PEP 649, a method's deferred annotations are evaluated against its owning class's own namespace, so any attribute assigned onto that class shadows a same-named builtin or global inside its annotations.The practical result: a handler annotated
event: dicton a state class that has adictattribute resolvesdictto that attribute — a function object — instead of the builtin type. Event-arg checking then blows up:This passes on 3.10–3.13 and fails on 3.14 with no source change.
Minimal reproduction (no Reflex required)
This is pure CPython behavior — the four arrangements below isolate exactly which one changed:
dictmethod in the same class bodyA.dictA.dict<class 'dict'>patched← changed<class 'dict'><class 'dict'>dict, then patchedpatchedOnly the owning class's namespace is consulted; patching a base class is unaffected. On ≤3.13 the annotation was evaluated eagerly at
deftime, so later assignment was invisible.Where it fails in reflex_base
Observed with
reflex-base==0.9.6on CPython 3.14 (ubuntu-24.04 and windows-2022 alike):reflex_base/event/__init__.py:1933/:1971—get_type_hints(event_callback.handler.fn)buildscallback_param_name_to_type, which comes back as{'event': <function ...>}.reflex_base/event/__init__.py:1846—_check_event_args_subclass_of_callbackwraps the failure intoCould not compare types ....reflex_base/utils/types.py:1028— the underlying raise isissubclass() arg 2 must be a class, a tuple of classes, or a unionfromtypehint_issubclass.Impact
Any state class that gets an attribute assigned onto it at runtime will mis-resolve annotations naming that attribute.
dictis the sharp edge, since it is both a builtin type and aBaseStatemethod name, andevent: dictis an extremely common handler signature.We hit this in
reflex-enterprise, where the auth plugin installs a delta filter withstate_cls.dict = filtered_dict; everyevent: dicthandler on a swept state then fails to compile. Anything else that assigns onto a state class will trip the same wire.Suggested direction
The shadowing is baked into the function's
__annotate__closure, so passinglocalns/globalnsat resolution time cannot undo it. Options that would work:@rx.eventdecoration /EventHandlerconstruction time, before anything can assign onto the class. This also removes the ordering sensitivity entirely.typehint_issubclass: when a resolved "type" is not a class/union, treat it as unknown rather than raising, so a mis-resolution warns instead of breaking compilation.(1) is the real fix; (2) is worth having regardless, since the current failure surfaces as an opaque
TypeErrorfar from the cause.Related
Same underlying class of bug as #2848 and #918, which were about
from __future__ import annotations. PEP 649 effectively makes every module behave that way on 3.14, so this will resurface broadly as people move to 3.14.