Skip to content

Fix class methods shadowing annotation names on Python 3.14 (fixes #219) - #258

Open
anyingiit wants to merge 1 commit into
rnag:mainfrom
anyingiit:fix-219-py314-class-method-shadowing
Open

anyingiit wants to merge 1 commit into
rnag:mainfrom
anyingiit:fix-219-py314-class-method-shadowing

Conversation

@anyingiit

Copy link
Copy Markdown

Description

On Python 3.14 (PEP 649/749), a forward reference created by annotationlib is evaluated against the namespace of the class that owns the annotation. So with

@dataclass
class Inv(JSONWizard):
    items: list[str]
    def list(self): ...

dataclasses stores items as ForwardRef('list[str]', owner=Inv). When eval_forward_ref() resolves it with localns=None, ForwardRef.evaluate() falls back to vars(owner), so list becomes the method and loading fails with TypeError: 'function' object is not subscriptable. Both the v1 and v0 loaders are affected. As noted in the issue, typing.List, dict, and similar names hit the same problem when a method of the same name exists.

This PR adds a small _get_typing_locals() helper (used by eval_forward_ref() in both utils/_typing_compat.py and v0/utils/typing_compat.py). On 3.14+, when the forward ref has a class __owner__, the helper passes that class's namespace as locals, minus members that can never be a type: functions, builtin functions, classmethod/staticmethod, property, and cached_property. Names like list/List/dict then resolve from the module globals or builtins again, while real class-level types such as nested classes still resolve. On Python < 3.14 the behavior is unchanged (_TYPING_LOCALS as before).

Scope note: a bare items: list next to a list() method is evaluated by Python itself to the method, and no ForwardRef is involved, so this PR doesn't change that case.

Regression tests were added for v1 (tests/unit/test_loaders.py) and v0 (tests/unit/v0/test_load.py). The v1 test covers a list() method, a List() method, a dict property, and a nested dataclass, with a round-trip. Both tests fail on main under 3.14 with the TypeError from the issue and pass with this change. They also pass on older Pythons, where the method never interfered.

Motivation / disclosure: I had some spare AI-assistant quota (Claude Code) and am using it to try to help projects with open good-first-issues. The change was prepared with Claude Code and verified as listed below. If it doesn't fit, isn't up to your bar, or you'd simply rather not take it — please feel free to close it, no hard feelings at all 🙂

Related issue

Closes #219

Checklist

  • Tests pass locally (pytest --ignore-glob=*integration*): 886 passed, 2 skipped, 6 xfailed, 4 xpassed on CPython 3.14.0rc2, 3.13 and 3.12, and 885 passed on 3.11. The new tests fail on unpatched main under 3.14 and pass with the fix. flake8 reports nothing new in the changed modules.
  • CHANGELOG.md is updated (if applicable) — n/a (HISTORY.rst is written at release time)
  • Documentation is updated (if applicable) — n/a

…ag#219)

On Python 3.14, a forward reference created by annotationlib is
evaluated against the namespace of the class that owns the annotation.
A method such as `def list(self)` then shadows the builtin `list` in
an annotation like `items: list[str]`, and loading the dataclass fails
with "TypeError: 'function' object is not subscriptable".

When evaluating such a forward reference, use the owner's namespace
without members that can never be a type (functions, class/static
methods, properties), so those names resolve from the module globals or
builtins instead, while nested classes are still resolved from the
class namespace. Applied to both the v1 and v0 code paths.
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.

breaking change with 3.14: local method list() interferes with typing

1 participant