Skip to content

Python 3.14: event handler annotations resolve against the owning class namespace, breaking event: dict handlers #6890

Description

@masenf

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 / :1971get_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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions