Repository: pydantic/typing-inspection
Pull request #37: Fix compatibility with latest Python 3.14 release
Author: login: Viicos
Created: 2025-03-22T12:34:36Z | Merged: 2025-03-22T12:38:34Z

Adapt documentation and tests related to the `typing.Union` changes


Diff:
diff --git a/docs/usage.md b/docs/usage.md
index c9ece27..7a538c6 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -4,18 +4,18 @@ The library is divided into two submodules:
 
 - [`typing_inspection.typing_objects`][]: provides functions to check if a variable is a [`typing`][] object:
   ```python
-  from typing_extensions import Union, get_origin
+  from typing_extensions import Literal, get_origin
 
-  from typing_inspection.typing_objects import is_union
+  from typing_inspection.typing_objects import is_literal
 
-  is_union(get_origin(Union[int, str]))  # True
+  is_literal(get_origin(Literal[1, 2]))  # True
   ```
 
     !!! note
         You might be tempted to use a simple identity check:
 
         ```pycon
-        >>> get_origin(Union[int, str]) is typing.Union
+        >>> get_origin(Literal[1, 2]) is typing.Literal
         ```
 
         However, [`typing_extensions`][] might provide a different version of the [`typing`][] objects. Instead,
diff --git a/src/typing_inspection/introspection.py b/src/typing_inspection/introspection.py
index 43cce1e..4f92527 100644
--- a/src/typing_inspection/introspection.py
+++ b/src/typing_inspection/introspection.py
@@ -23,6 +23,40 @@
     'is_union_origin',
 )
 
+if sys.version_info >= (3, 14):
+
+    def is_union_origin(obj: Any, /) -> bool:
+        """Return whether the provided origin is the union form.
+
+        ```pycon
+        >>> is_union_origin(typing.Union)
+        True
+        >>> is_union_origin(get_origin(int | str))
+        True
+        >>> is_union_origin(types.UnionType)
+        True
+        ```
+
+        !!! note
+            Starting in Python 3.14, the [`typing.Union`][] special form [was changed](https://github.com/python/cpython/pull/105511)
+            to be an alias to [`types.UnionType`][]. As such, it is recommended to not use this function
+            anymore (provided that you only support Python 3.14 or greater), and instead perform
+            the check directly:
+
+            ```python
+            import types
+            from typing import Union, get_origin
+
+            typ = Union[int, str]
+            origin = get_origin(typ)
+            if origin is types.UnionType:
+                ...
+            ```
+        """
+        return obj is types.UnionType
+        return typing_objects.is_union(obj) or obj is types.UnionType
+
+
 if sys.version_info >= (3, 10):
 
     def is_union_origin(obj: Any, /) -> bool:
@@ -33,7 +67,25 @@ def is_union_origin(obj: Any, /) -> bool:
         True
         >>> is_union_origin(get_origin(int | str))
         True
+        >>> is_union_origin(types.UnionType)
+        True
         ```
+
+        !!! note
+            Starting in Python 3.14, the [`typing.Union`][] special form [was changed](https://github.com/python/cpython/pull/105511)
+            to be an alias to [`types.UnionType`][]. As such, it is recommended to not use this function
+            anymore (provided that you only support Python 3.14 or greater), and instead perform
+            the check directly:
+
+            ```python
+            import types
+            from typing import Union, get_origin
+
+            typ = Union[int, str]
+            origin = get_origin(typ)
+            if origin is types.UnionType:
+                ...
+            ```
         """
         return typing_objects.is_union(obj) or obj is types.UnionType
 
@@ -47,7 +99,25 @@ def is_union_origin(obj: Any, /) -> bool:
         True
         >>> is_union_origin(get_origin(int | str))
         True
+        >>> is_union_origin(types.UnionType)
+        True
         ```
+
+        !!! note
+            Starting in Python 3.14, the [`typing.Union`][] special form [was changed](https://github.com/python/cpython/pull/105511)
+            to be an alias to [`types.UnionType`][]. As such, it is recommended to not use this function
+            anymore (provided that you only support Python 3.14 or greater), and instead perform
+            the check directly:
+
+            ```python
+            import types
+            from typing import Union, get_origin
+
+            typ = Union[int, str]
+            origin = get_origin(typ)
+            if origin is types.UnionType:
+                ...
+            ```
         """
         return typing_objects.is_union(obj)
 
diff --git a/tests/typing_objects/test_member_checks.py b/tests/typing_objects/test_member_checks.py
index 86d9761..2cc5df0 100644
--- a/tests/typing_objects/test_member_checks.py
+++ b/tests/typing_objects/test_member_checks.py
@@ -189,6 +189,12 @@ def test_is_deprecated(deprecated: deprecated) -> None:
 # Misc. tests:
 
 
-@pytest.mark.skipif(sys.version_info < (3, 10), reason='`types.UnionType` is only available in Python 3.10.')
+@pytest.mark.skipif(
+    sys.version_info < (3, 10) or sys.version_info >= (3, 14),
+    reason=(
+        '`types.UnionType` is only available in Python 3.10. '
+        'In Python 3.14, `typing.Union` is an alias for `types.UnionType`.'
+    ),
+)
 def test_is_union_does_not_match_uniontype() -> None:
     assert not typing_objects.is_union(types.UnionType)


Reviews:
user: login: Darsstar
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-27T11:58:34Z
user: login: Viicos
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T12:26:29Z

Review comments:
diff_hunk: @@ -23,6 +23,40 @@
     'is_union_origin',
 )
 
+if sys.version_info >= (3, 14):
+
+    def is_union_origin(obj: Any, /) -> bool:
+        """Return whether the provided origin is the union form.
+
+        ```pycon
+        >>> is_union_origin(typing.Union)
+        True
+        >>> is_union_origin(get_origin(int | str))
+        True
+        >>> is_union_origin(types.UnionType)
+        True
+        ```
+
+        !!! note
+            Starting in Python 3.14, the [`typing.Union`][] special form [was changed](https://github.com/python/cpython/pull/105511)
+            to be an alias to [`types.UnionType`][]. As such, it is recommended to not use this function
+            anymore (provided that you only support Python 3.14 or greater), and instead perform
+            the check directly:
+
+            ```python
+            import types
+            from typing import Union, get_origin
+
+            typ = Union[int, str]
+            origin = get_origin(typ)
+            if origin is types.UnionType:
+                ...
+            ```
+        """
+        return obj is types.UnionType
+        return typing_objects.is_union(obj) or obj is types.UnionType
+
+
 if sys.version_info >= (3, 10):
path: src/typing_inspection/introspection.py
user: login: Darsstar
body: Shouldn't this have been changed to be an `elif`?


created_at: 2025-03-27T11:36:22Z
author_association: NONE
diff_hunk: @@ -23,6 +23,40 @@
     'is_union_origin',
 )
 
+if sys.version_info >= (3, 14):
+
+    def is_union_origin(obj: Any, /) -> bool:
+        """Return whether the provided origin is the union form.
+
+        ```pycon
+        >>> is_union_origin(typing.Union)
+        True
+        >>> is_union_origin(get_origin(int | str))
+        True
+        >>> is_union_origin(types.UnionType)
+        True
+        ```
+
+        !!! note
+            Starting in Python 3.14, the [`typing.Union`][] special form [was changed](https://github.com/python/cpython/pull/105511)
+            to be an alias to [`types.UnionType`][]. As such, it is recommended to not use this function
+            anymore (provided that you only support Python 3.14 or greater), and instead perform
+            the check directly:
+
+            ```python
+            import types
+            from typing import Union, get_origin
+
+            typ = Union[int, str]
+            origin = get_origin(typ)
+            if origin is types.UnionType:
+                ...
+            ```
+        """
+        return obj is types.UnionType
+        return typing_objects.is_union(obj) or obj is types.UnionType
path: src/typing_inspection/introspection.py
user: login: Darsstar
body: Maybe remove the unreachable code as well?
created_at: 2025-03-27T11:56:25Z
author_association: NONE
diff_hunk: @@ -23,6 +23,40 @@
     'is_union_origin',
 )
 
+if sys.version_info >= (3, 14):
+
+    def is_union_origin(obj: Any, /) -> bool:
+        """Return whether the provided origin is the union form.
+
+        ```pycon
+        >>> is_union_origin(typing.Union)
+        True
+        >>> is_union_origin(get_origin(int | str))
+        True
+        >>> is_union_origin(types.UnionType)
+        True
+        ```
+
+        !!! note
+            Starting in Python 3.14, the [`typing.Union`][] special form [was changed](https://github.com/python/cpython/pull/105511)
+            to be an alias to [`types.UnionType`][]. As such, it is recommended to not use this function
+            anymore (provided that you only support Python 3.14 or greater), and instead perform
+            the check directly:
+
+            ```python
+            import types
+            from typing import Union, get_origin
+
+            typ = Union[int, str]
+            origin = get_origin(typ)
+            if origin is types.UnionType:
+                ...
+            ```
+        """
+        return obj is types.UnionType
+        return typing_objects.is_union(obj) or obj is types.UnionType
+
+
 if sys.version_info >= (3, 10):
path: src/typing_inspection/introspection.py
user: login: Viicos
body: Nice catch. I'm surprised tests passed with this. I also need to actually use `typing.Union` as it is `types.UnionType` that was marked as a alias to `typing.Union`.
created_at: 2025-03-27T12:26:29Z
author_association: MEMBER

PR comments:
user: login: hyperlint-ai-deprecated[bot]
created_at: 2025-03-22T12:34:50Z
body: ## PR Change Summary

Updated documentation and tests to ensure compatibility with the latest Python 3.14 release, specifically addressing changes in the typing module.
- Modified usage examples to reflect changes in typing.Union and typing.Literal
- Updated function imports to align with the latest typing standards
- Adjusted documentation notes to clarify usage of typing features


**Modified Files**

- `docs/usage.md`


 







---

<details>

<summary>How can I customize these reviews?</summary>

Check out the [Hyperlint AI Reviewer docs](https://docs.hyperlint.com/guides/ai-reviewer/) for more information on how to customize the review.

If you just want to ignore it on this PR, you can add the `hyperlint-ignore` label to the PR. Future changes won't trigger a Hyperlint review.

Note specifically for link checks, we only check the first 30 links in a file and we cache the results for several hours (for instance, if you just added a page, you might experience this). Our recommendation is to add `hyperlint-ignore` to the PR to ignore the link check for this PR.
</details>

<details>
<summary>What is Hyperlint?</summary>

[Hyperlint](https://hyperlint.com) is an AI agent that helps you write, edit, and maintain your documentation.

Learn more about the [Hyperlint AI reviewer](https://docs.hyperlint.com/guides/ai-reviewer/) and the checks that we can run on your documentation.

</details>
author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-03-22T12:35:15Z
body: ## Deploying typing-inspection with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>aec589d</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://a30c075f.typing-inspection.pages.dev'>https://a30c075f.typing-inspection.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://3-14-compat-1.typing-inspection.pages.dev'>https://3-14-compat-1.typing-inspection.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/58c2606138e6f7346345b5a882718168/pages/view/typing-inspection/a30c075f-3c70-447a-9c0d-df836d698ee9)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-22T12:38:14Z
body: ## Coverage report


<img title="Coverage for the whole project is 94.52%. Previous coverage rate is not available, cannot report on evolution." src="https://img.shields.io/badge/Coverage-94%25-orange.svg"> <img title="66.66% of the statement lines added by this PR are covered" src="https://img.shields.io/badge/PR%20Coverage-66%25-red.svg"><details><summary>Click to see where and how coverage changed</summary><table><thead>
  <tr><th>File</th><th>Statements</th><th>Missing</th><th>Coverage</th><th>Coverage<br>(new stmts)</th><th>Lines missing</th></tr>
</thead>
<tbody><tr>
<td colspan="6">&nbsp;&nbsp;<b>src/typing_inspection</b></td><tr>
<td>&nbsp;&nbsp;<a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-91fab654e0099d0231aead2ad570ce916e742b63f49eab2a84b16fe70e1bbc00">introspection.py</a></td>
<td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-91fab654e0099d0231aead2ad570ce916e742b63f49eab2a84b16fe70e1bbc00"><img title="This PR adds 209 statements to src/typing_inspection/introspection.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/209-%28%2B209%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-91fab654e0099d0231aead2ad570ce916e742b63f49eab2a84b16fe70e1bbc00"><img title="This PR adds 10 statements missing coverage to src/typing_inspection/introspection.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/10-%28%2B10%29-red.svg"></a></td><td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-91fab654e0099d0231aead2ad570ce916e742b63f49eab2a84b16fe70e1bbc00"><img title="The coverage rate of src/typing_inspection/introspection.py is 94.75% (199/209). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/94%25-%28199/209%29-orange.svg"></a></td><td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-91fab654e0099d0231aead2ad570ce916e742b63f49eab2a84b16fe70e1bbc00"><img title="In this PR, 3 new statements are added to src/typing_inspection/introspection.py, 2 of which are covered (66.66%)." src="https://img.shields.io/badge/66%25-%282/3%29-red.svg"></a></td><td><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-91fab654e0099d0231aead2ad570ce916e742b63f49eab2a84b16fe70e1bbc00R56-R56">56</a></td></tbody>
<tfoot>
<tr>
<td><b>Project Total</b></td>
<td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 333 statements to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/333-%28%2B333%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 14 statements missing coverage to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/14-%28%2B14%29-red.svg"></a></td><td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="The coverage rate of the whole project is 94.52% (319/333). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/94%25-%28319/333%29-orange.svg"></a></td><td align="center"><a href="https://github.com/pydantic/typing-inspection/pull/37/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="In this PR, 3 new statements are added to the whole project, 2 of which are covered (66.66%)." src="https://img.shields.io/badge/66%25-%282/3%29-red.svg"></a></td><td>&nbsp;</td>
</tr>
</tfoot>
</table>

<sub>

This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)

</sub>
</details>



<!-- This comment was produced by python-coverage-comment-action -->
author_association: NONE