Skip to content

Fix Python 3.14 compatibility for typing.Union annotations#2093

Open
frenzymadness wants to merge 3 commits intodavidhalter:masterfrom
frenzymadness:py314
Open

Fix Python 3.14 compatibility for typing.Union annotations#2093
frenzymadness wants to merge 3 commits intodavidhalter:masterfrom
frenzymadness:py314

Conversation

@frenzymadness
Copy link
Copy Markdown
Contributor

In Python 3.14, typing.Union changed its repr from 'typing.Union[X, Y]' to 'X | Y' (PEP 604), breaking annotation inference.

Changes:

  • Use getattr() instead of safe_getattr() for module retrieval (getattr_static fails on Union types in Python 3.14)
  • Add fallback to typing.get_origin() when regex fails to match
  • Normalize Union display back to 'Union[X, Y]' format for consistency
  • Update test expectations for invalid annotation edge case in 3.14

Fixes: #2064

It works for me on Python 3.12, 3.13, and 3.14.

In Python 3.14, typing.Union changed its repr from 'typing.Union[X, Y]'
to 'X | Y' (PEP 604), breaking annotation inference.

Changes:
- Use getattr() instead of safe_getattr() for __module__ retrieval
  (getattr_static fails on Union types in Python 3.14)
- Add fallback to typing.get_origin() when regex fails to match
- Normalize Union display back to 'Union[X, Y]' format for consistency
- Update test expectations for invalid annotation edge case in 3.14

Fixes: davidhalter#2064
@frenzymadness
Copy link
Copy Markdown
Contributor Author

The problem in the docs job is caused by ModuleNotFoundError: No module named 'pkg_resources' which is very likely caused by a newest setuptools being installed.

@frenzymadness
Copy link
Copy Markdown
Contributor Author

Would you accept fixes for 3.15 here as well?

- Fix getattr_static for Python 3.15 __dict__ GetSetDescriptorType
- Accept abs() parameter name change ('x' → 'number')
- Add Python 3.15 os module constants to test expectations

Fixes instance attribute introspection and stdlib changes in Python 3.15.
Copy link
Copy Markdown
Owner

@davidhalter davidhalter left a comment

Choose a reason for hiding this comment

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

Thanks! Sorry for the review delay. I think the changes are mostly fine, but I want to review something manually.

if safe_getattr(self._obj, '__module__', default='') == 'typing':
# Use getattr instead of safe_getattr for __module__ as getattr_static
# fails on typing types in Python 3.14+
module = getattr(self._obj, '__module__', '')
Copy link
Copy Markdown
Owner

@davidhalter davidhalter Mar 26, 2026

Choose a reason for hiding this comment

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

Hmm I need to look at this one in detail. I want to understand why safe_getattr doesn't work. We might need some fixes. Also safe_getattr solves a few other problems of code executions that I would want to avoid, but since it's only accessing __module__ it might be mostly fine (except for __getattr__ cases).

DO you know why __module__ is not working anymore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I already kinda lost the context here, but AFAIR: in Python 3.14, the typing module's internal implementation changed. The __module__ attribute on Union types is no longer accessible through the static inspection mechanism that getattr_static() uses.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hmm that's fine. I guess I'll take a stab at it then. This function seems cursed anyway. Why are we matching with regex? Nobody seems to know anymore. This probably comes from a time where some of the typing API wasn't available or very different.

@davidhalter
Copy link
Copy Markdown
Owner

I don't mind fixing things for 3.15 in here.

@frenzymadness
Copy link
Copy Markdown
Contributor Author

I've pushed the commit I had prepared for 3.15.

Copy link
Copy Markdown
Owner

@davidhalter davidhalter left a comment

Choose a reason for hiding this comment

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

I guess probably just revert the test change that makes it worse for 3.14 and then I can try to fix that one. I don't think it will take too much time.

{'return': 'typing.Union["str", 1]'},
['str'] if (3, 11) <= sys.version_info < (3, 14) else [],
'',
),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

>>> import typing
>>> x = typing.Union["str", 1]
>>> x.__args__
(ForwardRef('str'), 1)

Also a small nit: I generally prefer to keep the formatting as is. Especially here where all the other cases have this style. I don't think I would have said anything if I didn't look at this in more detail (Might also be an auto formatter, that did this, but still)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There is a job in the CI here running flake8 and that failed with:

test/test_api/test_interpreter.py:667:101: E501 line too long (102 > 100 characters)

I can revert the commit if you are okay with merging PRs with failed tests like this.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

No worries, I'll just build upon this PR

if safe_getattr(self._obj, '__module__', default='') == 'typing':
# Use getattr instead of safe_getattr for __module__ as getattr_static
# fails on typing types in Python 3.14+
module = getattr(self._obj, '__module__', '')
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hmm that's fine. I guess I'll take a stab at it then. This function seems cursed anyway. Why are we matching with regex? Nobody seems to know anymore. This probably comes from a time where some of the typing API wasn't available or very different.

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.

Python 3.14 compatibility

2 participants