]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🎨 Upgrade typing syntax for Python 3.10 (#14932)
authorSebastián Ramírez <tiangolo@gmail.com>
Tue, 17 Feb 2026 09:59:14 +0000 (01:59 -0800)
committerGitHub <noreply@github.com>
Tue, 17 Feb 2026 09:59:14 +0000 (09:59 +0000)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tiangolo <1326112+tiangolo@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
77 files changed:
docs_src/extra_models/tutorial003_py310.py
scripts/doc_parsing_utils.py
scripts/mkdocs_hooks.py
scripts/notify_translations.py
scripts/people.py
tests/main.py
tests/test_additional_properties_bool.py
tests/test_additional_responses_union_duplicate_anyof.py
tests/test_callable_endpoint.py
tests/test_compat.py
tests/test_custom_middleware_exception.py
tests/test_custom_schema_fields.py
tests/test_dependency_contextvars.py
tests/test_dependency_overrides.py
tests/test_dependency_paramless.py
tests/test_enforce_once_required_parameter.py
tests/test_extra_routes.py
tests/test_filter_pydantic_sub_model_pv2.py
tests/test_form_default.py
tests/test_forms_single_model.py
tests/test_infer_param_optionality.py
tests/test_invalid_sequence_param.py
tests/test_jsonable_encoder.py
tests/test_openapi_examples.py
tests/test_openapi_query_parameter_extension.py
tests/test_openapi_schema_type.py
tests/test_openapi_separate_input_output_schemas.py
tests/test_optional_file_list.py
tests/test_param_class.py
tests/test_param_include_in_schema.py
tests/test_pydantic_v1_error.py
tests/test_pydanticv2_dataclasses_uuid_stringified_annotations.py
tests/test_request_params/test_body/test_list.py
tests/test_request_params/test_body/test_optional_list.py
tests/test_request_params/test_body/test_optional_str.py
tests/test_request_params/test_body/test_required_str.py
tests/test_request_params/test_cookie/test_optional_str.py
tests/test_request_params/test_file/test_optional.py
tests/test_request_params/test_file/test_optional_list.py
tests/test_request_params/test_form/test_optional_list.py
tests/test_request_params/test_form/test_optional_str.py
tests/test_request_params/test_header/test_optional_list.py
tests/test_request_params/test_header/test_optional_str.py
tests/test_request_params/test_query/test_optional_list.py
tests/test_request_params/test_query/test_optional_str.py
tests/test_required_noneable.py
tests/test_response_model_as_return_annotation.py
tests/test_router_events.py
tests/test_schema_extra_examples.py
tests/test_security_api_key_cookie_optional.py
tests/test_security_api_key_header_optional.py
tests/test_security_api_key_query_optional.py
tests/test_security_http_base_optional.py
tests/test_security_http_basic_optional.py
tests/test_security_http_bearer_optional.py
tests/test_security_http_digest_optional.py
tests/test_security_oauth2_authorization_code_bearer.py
tests/test_security_oauth2_authorization_code_bearer_description.py
tests/test_security_oauth2_authorization_code_bearer_scopes_openapi.py
tests/test_security_oauth2_optional.py
tests/test_security_oauth2_optional_description.py
tests/test_security_oauth2_password_bearer_optional.py
tests/test_security_oauth2_password_bearer_optional_description.py
tests/test_security_openid_connect_optional.py
tests/test_serialize_response.py
tests/test_serialize_response_dataclass.py
tests/test_serialize_response_model.py
tests/test_skip_defaults.py
tests/test_sub_callbacks.py
tests/test_union_body.py
tests/test_union_body_discriminator.py
tests/test_union_body_discriminator_annotated.py
tests/test_union_forms.py
tests/test_union_inherited_body.py
tests/test_validate_response.py
tests/test_validate_response_dataclass.py
tests/utils.py

index 06675cbc09808a020cdbc0f6d1b1efc2793de7a8..8fe6f7136e17f98f06ff5ef7b4b7ad93639c699c 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 from fastapi import FastAPI
 from pydantic import BaseModel
 
@@ -30,6 +28,6 @@ items = {
 }
 
 
-@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
+@app.get("/items/{item_id}", response_model=PlaneItem | CarItem)
 async def read_item(item_id: str):
     return items[item_id]
index 79f2e9ec0a558adb0fc80491cb44e1934be011bb..1cd2299e66933d1a4f767c58db7bf590cdcfdf66 100644 (file)
@@ -1,5 +1,5 @@
 import re
-from typing import TypedDict, Union
+from typing import TypedDict
 
 CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$")
 CODE_INCLUDE_PLACEHOLDER = "<CODE_INCLUDE>"
@@ -50,8 +50,8 @@ class MarkdownLinkInfo(TypedDict):
     line_no: int
     url: str
     text: str
-    title: Union[str, None]
-    attributes: Union[str, None]
+    title: str | None
+    attributes: str | None
     full_match: str
 
 
@@ -287,8 +287,8 @@ def _add_lang_code_to_url(url: str, lang_code: str) -> str:
 def _construct_markdown_link(
     url: str,
     text: str,
-    title: Union[str, None],
-    attributes: Union[str, None],
+    title: str | None,
+    attributes: str | None,
     lang_code: str,
 ) -> str:
     """
@@ -549,7 +549,7 @@ def extract_multiline_code_blocks(text: list[str]) -> list[MultilineCodeBlockInf
     return blocks
 
 
-def _split_hash_comment(line: str) -> tuple[str, Union[str, None]]:
+def _split_hash_comment(line: str) -> tuple[str, str | None]:
     match = HASH_COMMENT_RE.match(line)
     if match:
         code = match.group("code").rstrip()
@@ -558,7 +558,7 @@ def _split_hash_comment(line: str) -> tuple[str, Union[str, None]]:
     return line.rstrip(), None
 
 
-def _split_slashes_comment(line: str) -> tuple[str, Union[str, None]]:
+def _split_slashes_comment(line: str) -> tuple[str, str | None]:
     match = SLASHES_COMMENT_RE.match(line)
     if match:
         code = match.group("code").rstrip()
@@ -603,9 +603,9 @@ def replace_multiline_code_block(
         return block_a["content"].copy()  # We don't handle mermaid code blocks for now
 
     code_block: list[str] = []
-    for line_a, line_b in zip(block_a["content"], block_b["content"]):
-        line_a_comment: Union[str, None] = None
-        line_b_comment: Union[str, None] = None
+    for line_a, line_b in zip(block_a["content"], block_b["content"], strict=False):
+        line_a_comment: str | None = None
+        line_b_comment: str | None = None
 
         # Handle comments based on language
         if block_language in {
@@ -659,7 +659,7 @@ def replace_multiline_code_blocks_in_text(
         )
 
     modified_text = text.copy()
-    for block, original_block in zip(code_blocks, original_code_blocks):
+    for block, original_block in zip(code_blocks, original_code_blocks, strict=True):
         updated_content = replace_multiline_code_block(block, original_block)
 
         start_line_index = block["start_line_no"] - 1
index 567c0111dc8a7258a76f4056598bf7049864a0a5..97006953d6d372bfb46dc3eb17f7ff1395632026 100644 (file)
@@ -1,6 +1,6 @@
 from functools import lru_cache
 from pathlib import Path
-from typing import Any, Union
+from typing import Any
 
 import material
 from mkdocs.config.defaults import MkDocsConfig
@@ -105,9 +105,9 @@ def on_files(files: Files, *, config: MkDocsConfig) -> Files:
 
 
 def generate_renamed_section_items(
-    items: list[Union[Page, Section, Link]], *, config: MkDocsConfig
-) -> list[Union[Page, Section, Link]]:
-    new_items: list[Union[Page, Section, Link]] = []
+    items: list[Page | Section | Link], *, config: MkDocsConfig
+) -> list[Page | Section | Link]:
+    new_items: list[Page | Section | Link] = []
     for item in items:
         if isinstance(item, Section):
             new_title = item.title
index 74cdf0dffefd0321178d473c62ea4a39fc66c6a2..3484b69c7046cf80d5a404ed762d7f6ddc34335e 100644 (file)
@@ -3,7 +3,7 @@ import random
 import sys
 import time
 from pathlib import Path
-from typing import Any, Union, cast
+from typing import Any, cast
 
 import httpx
 from github import Github
@@ -181,9 +181,9 @@ class Settings(BaseSettings):
     github_repository: str
     github_token: SecretStr
     github_event_path: Path
-    github_event_name: Union[str, None] = None
+    github_event_name: str | None = None
     httpx_timeout: int = 30
-    debug: Union[bool, None] = False
+    debug: bool | None = False
     number: int | None = None
 
 
@@ -199,12 +199,12 @@ def get_graphql_response(
     *,
     settings: Settings,
     query: str,
-    after: Union[str, None] = None,
-    category_id: Union[str, None] = None,
-    discussion_number: Union[int, None] = None,
-    discussion_id: Union[str, None] = None,
-    comment_id: Union[str, None] = None,
-    body: Union[str, None] = None,
+    after: str | None = None,
+    category_id: str | None = None,
+    discussion_number: int | None = None,
+    discussion_id: str | None = None,
+    comment_id: str | None = None,
+    body: str | None = None,
 ) -> dict[str, Any]:
     headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"}
     variables = {
@@ -249,7 +249,7 @@ def get_graphql_translation_discussions(
 
 
 def get_graphql_translation_discussion_comments_edges(
-    *, settings: Settings, discussion_number: int, after: Union[str, None] = None
+    *, settings: Settings, discussion_number: int, after: str | None = None
 ) -> list[CommentsEdge]:
     data = get_graphql_response(
         settings=settings,
@@ -372,8 +372,8 @@ def main() -> None:
             f"Found a translation discussion for language: {lang} in discussion: #{discussion.number}"
         )
 
-        already_notified_comment: Union[Comment, None] = None
-        already_done_comment: Union[Comment, None] = None
+        already_notified_comment: Comment | None = None
+        already_done_comment: Comment | None = None
 
         logging.info(
             f"Checking current comments in discussion: #{discussion.number} to see if already notified about this PR: #{pr.number}"
index 207ab4649335b7bbeac4aa425ce7c6c97e75f8af..f3254ab606111e74389ebb2991d85246233a3522 100644 (file)
@@ -6,7 +6,7 @@ from collections import Counter
 from collections.abc import Container
 from datetime import datetime, timedelta, timezone
 from pathlib import Path
-from typing import Any, Union
+from typing import Any
 
 import httpx
 import yaml
@@ -70,7 +70,7 @@ class Author(BaseModel):
 
 class CommentsNode(BaseModel):
     createdAt: datetime
-    author: Union[Author, None] = None
+    author: Author | None = None
 
 
 class Replies(BaseModel):
@@ -89,7 +89,7 @@ class DiscussionsComments(BaseModel):
 
 class DiscussionsNode(BaseModel):
     number: int
-    author: Union[Author, None] = None
+    author: Author | None = None
     title: str | None = None
     createdAt: datetime
     comments: DiscussionsComments
@@ -127,8 +127,8 @@ def get_graphql_response(
     *,
     settings: Settings,
     query: str,
-    after: Union[str, None] = None,
-    category_id: Union[str, None] = None,
+    after: str | None = None,
+    category_id: str | None = None,
 ) -> dict[str, Any]:
     headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"}
     variables = {"after": after, "category_id": category_id}
@@ -156,7 +156,7 @@ def get_graphql_response(
 def get_graphql_question_discussion_edges(
     *,
     settings: Settings,
-    after: Union[str, None] = None,
+    after: str | None = None,
 ) -> list[DiscussionsEdge]:
     data = get_graphql_response(
         settings=settings,
index 7edb16c61565721d12994099da051de9b2d8fa48..d2fbbe6153268a34a90bace9663e5228b5ee87d4 100644 (file)
@@ -1,5 +1,4 @@
 import http
-from typing import Optional
 
 from fastapi import FastAPI, Path, Query
 
@@ -54,7 +53,7 @@ def get_bool_id(item_id: bool):
 
 
 @app.get("/path/param/{item_id}")
-def get_path_param_id(item_id: Optional[str] = Path()):
+def get_path_param_id(item_id: str | None = Path()):
     return item_id
 
 
@@ -161,7 +160,7 @@ def get_query_type(query: int):
 
 
 @app.get("/query/int/optional")
-def get_query_type_optional(query: Optional[int] = None):
+def get_query_type_optional(query: int | None = None):
     if query is None:
         return "foo bar"
     return f"foo bar {query}"
index c02841cde19bcb3ee1e094cf9d7f0599214618ff..9a1e139eaaf9cafbdce08adc0435cd8e95fb896f 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -19,7 +17,7 @@ app = FastAPI()
 
 @app.post("/")
 async def post(
-    foo: Union[Foo, None] = None,
+    foo: Foo | None = None,
 ):
     return foo
 
index 5d833fce4a4cf838b0491162d1235a357dcf15e5..401bc0a7446c7a5b2d6f913a02e97b3d1c95b215 100644 (file)
@@ -4,8 +4,6 @@ don't accumulate duplicate $ref entries in anyOf arrays.
 See https://github.com/fastapi/fastapi/pull/14463
 """
 
-from typing import Union
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -23,7 +21,7 @@ class ModelB(BaseModel):
 app = FastAPI(
     responses={
         500: {
-            "model": Union[ModelA, ModelB],
+            "model": ModelA | ModelB,
             "content": {"application/json": {"examples": {"Case A": {"value": "a"}}}},
         }
     }
index 1882e9053a21344dc26539560807bae0b3c08d97..28999d3833a14ddfeac906d49281847e7ff6fae1 100644 (file)
@@ -1,11 +1,10 @@
 from functools import partial
-from typing import Optional
 
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 
 
-def main(some_arg, q: Optional[str] = None):
+def main(some_arg, q: str | None = None):
     return {"some_arg": some_arg, "q": q}
 
 
index 0b5600f8f59dace5a2f99ac18ffaab3fcaab1fef..772bd305eb41559f722f15e1abf0ecbeae2273a1 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 from fastapi import FastAPI, UploadFile
 from fastapi._compat import (
     Undefined,
@@ -10,8 +8,6 @@ from fastapi.testclient import TestClient
 from pydantic import BaseModel, ConfigDict
 from pydantic.fields import FieldInfo
 
-from .utils import needs_py310
-
 
 def test_model_field_default_required():
     from fastapi._compat import v2
@@ -26,7 +22,7 @@ def test_complex():
     app = FastAPI()
 
     @app.post("/")
-    def foo(foo: Union[str, list[int]]):
+    def foo(foo: str | list[int]):
         return foo
 
     client = TestClient(app)
@@ -49,17 +45,17 @@ def test_propagates_pydantic2_model_config():
 
     class EmbeddedModel(BaseModel):
         model_config = ConfigDict(arbitrary_types_allowed=True)
-        value: Union[str, Missing] = Missing()
+        value: str | Missing = Missing()
 
     class Model(BaseModel):
         model_config = ConfigDict(
             arbitrary_types_allowed=True,
         )
-        value: Union[str, Missing] = Missing()
+        value: str | Missing = Missing()
         embedded_model: EmbeddedModel = EmbeddedModel()
 
     @app.post("/")
-    def foo(req: Model) -> dict[str, Union[str, None]]:
+    def foo(req: Model) -> dict[str, str | None]:
         return {
             "value": req.value or None,
             "embedded_value": req.embedded_model.value or None,
@@ -89,7 +85,7 @@ def test_is_bytes_sequence_annotation_union():
     # TODO: in theory this would allow declaring types that could be lists of bytes
     # to be read from files and other types, but I'm not even sure it's a good idea
     # to support it as a first class "feature"
-    assert is_bytes_sequence_annotation(Union[list[str], list[bytes]])
+    assert is_bytes_sequence_annotation(list[str] | list[bytes])
 
 
 def test_is_uploadfile_sequence_annotation():
@@ -97,21 +93,20 @@ def test_is_uploadfile_sequence_annotation():
     # TODO: in theory this would allow declaring types that could be lists of UploadFile
     # and other types, but I'm not even sure it's a good idea to support it as a first
     # class "feature"
-    assert is_uploadfile_sequence_annotation(Union[list[str], list[UploadFile]])
+    assert is_uploadfile_sequence_annotation(list[str] | list[UploadFile])
 
 
 def test_serialize_sequence_value_with_optional_list():
     """Test that serialize_sequence_value handles optional lists correctly."""
     from fastapi._compat import v2
 
-    field_info = FieldInfo(annotation=Union[list[str], None])
+    field_info = FieldInfo(annotation=list[str] | None)
     field = v2.ModelField(name="items", field_info=field_info)
     result = v2.serialize_sequence_value(field=field, value=["a", "b", "c"])
     assert result == ["a", "b", "c"]
     assert isinstance(result, list)
 
 
-@needs_py310
 def test_serialize_sequence_value_with_optional_list_pipe_union():
     """Test that serialize_sequence_value handles optional lists correctly (with new syntax)."""
     from fastapi._compat import v2
@@ -125,9 +120,12 @@ def test_serialize_sequence_value_with_optional_list_pipe_union():
 
 def test_serialize_sequence_value_with_none_first_in_union():
     """Test that serialize_sequence_value handles Union[None, List[...]] correctly."""
+    from typing import Union
+
     from fastapi._compat import v2
 
-    field_info = FieldInfo(annotation=Union[None, list[str]])
+    # Use Union[None, list[str]] to ensure None comes first in the union args
+    field_info = FieldInfo(annotation=Union[None, list[str]])  # noqa: UP007
     field = v2.ModelField(name="items", field_info=field_info)
     result = v2.serialize_sequence_value(field=field, value=["x", "y"])
     assert result == ["x", "y"]
index d9b81e7c2e74f4dfdf3b8faf576a697a39edbd37..cf548f4aed7f90cbd4db6b3d13f780b1a1fc5bdb 100644 (file)
@@ -1,5 +1,4 @@
 from pathlib import Path
-from typing import Optional
 
 from fastapi import APIRouter, FastAPI, File, UploadFile
 from fastapi.exceptions import HTTPException
@@ -17,7 +16,7 @@ class ContentSizeLimitMiddleware:
       max_content_size (optional): the maximum content size allowed in bytes, None for no limit
     """
 
-    def __init__(self, app: APIRouter, max_content_size: Optional[int] = None):
+    def __init__(self, app: APIRouter, max_content_size: int | None = None):
         self.app = app
         self.max_content_size = max_content_size
 
index 60b795e9badf6c93eed6f2c221f3a8f18ed801ca..c907c542424f88658d82e8c3dd0d8e4d45c6d9e4 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
@@ -10,9 +10,9 @@ app = FastAPI()
 class Item(BaseModel):
     name: str
 
-    description: Annotated[
-        Optional[str], WithJsonSchema({"type": ["string", "null"]})
-    ] = None
+    description: Annotated[str | None, WithJsonSchema({"type": ["string", "null"]})] = (
+        None
+    )
 
     model_config = {
         "json_schema_extra": {
index 0c2e5594b6f1de890bdfa7b2fbc18da06b77f8e5..eba135785b23d078e626961073255d89b39c4d04 100644 (file)
@@ -1,11 +1,11 @@
-from collections.abc import Awaitable
+from collections.abc import Awaitable, Callable
 from contextvars import ContextVar
-from typing import Any, Callable, Optional
+from typing import Any
 
 from fastapi import Depends, FastAPI, Request, Response
 from fastapi.testclient import TestClient
 
-legacy_request_state_context_var: ContextVar[Optional[dict[str, Any]]] = ContextVar(
+legacy_request_state_context_var: ContextVar[dict[str, Any] | None] = ContextVar(
     "legacy_request_state_context_var", default=None
 )
 
index e25db624d8fdcf0d19823e33a6f2327500e8d27e..7c99d9d9d3ed7e1ba719c4120985d815adb6f019 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from fastapi import APIRouter, Depends, FastAPI
 from fastapi.testclient import TestClient
@@ -38,7 +36,7 @@ app.include_router(router)
 client = TestClient(app)
 
 
-async def overrider_dependency_simple(q: Optional[str] = None):
+async def overrider_dependency_simple(q: str | None = None):
     return {"q": q, "skip": 5, "limit": 10}
 
 
index 1774196fe47e2a8a0baa45fbea369ada7ba59c8e..62c977b8258b61b0644454612b75546b4c16e9b5 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Union
+from typing import Annotated
 
 from fastapi import FastAPI, HTTPException, Security
 from fastapi.security import (
@@ -13,7 +13,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 
 def process_auth(
-    credentials: Annotated[Union[str, None], Security(oauth2_scheme)],
+    credentials: Annotated[str | None, Security(oauth2_scheme)],
     security_scopes: SecurityScopes,
 ):
     # This is an incorrect way of using it, this is not checking if the scopes are
index 0dee15c25d74dda79376af33b79456c0e409cdf1..9f8a9645436a55418223e5fcbe824600599707ea 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import Depends, FastAPI, Query
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -11,7 +9,7 @@ def _get_client_key(client_id: str = Query(...)) -> str:
     return f"{client_id}_key"
 
 
-def _get_client_tag(client_id: Optional[str] = Query(None)) -> Optional[str]:
+def _get_client_tag(client_id: str | None = Query(None)) -> str | None:
     if client_id is None:
         return None
     return f"{client_id}_tag"
@@ -20,7 +18,7 @@ def _get_client_tag(client_id: Optional[str] = Query(None)) -> Optional[str]:
 @app.get("/foo")
 def foo_handler(
     client_key: str = Depends(_get_client_key),
-    client_tag: Optional[str] = Depends(_get_client_tag),
+    client_tag: str | None = Depends(_get_client_tag),
 ):
     return {"client_id": client_key, "client_tag": client_tag}
 
index 96f85b44658a049585f55272c45b2969199a09ba..985adb94395872444b792bcd8b9267d0c24dd488 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from fastapi.responses import JSONResponse
 from fastapi.testclient import TestClient
@@ -11,7 +9,7 @@ app = FastAPI()
 
 class Item(BaseModel):
     name: str
-    price: Optional[float] = None
+    price: float | None = None
 
 
 @app.api_route("/items/{item_id}", methods=["GET"])
index 1de2b50f7f4a42e0368c710936439d0d4f6b9354..1f39581c23cafddac2b9e1a826e08e999e823cdc 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from dirty_equals import HasRepr
 from fastapi import Depends, FastAPI
@@ -22,7 +20,7 @@ def get_client():
 
     class ModelA(BaseModel):
         name: str
-        description: Optional[str] = None
+        description: str | None = None
         foo: ModelB
         tags: dict[str, str] = {}
 
index 0b3eb8f2e2bb7a9002fdf6b3f3960de8d4ce6a42..c4d33e3fb6fbcac5409a06a5cf65bf855960e05b 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 from fastapi import FastAPI, File, Form
 from starlette.testclient import TestClient
@@ -7,14 +7,14 @@ app = FastAPI()
 
 
 @app.post("/urlencoded")
-async def post_url_encoded(age: Annotated[Optional[int], Form()] = None):
+async def post_url_encoded(age: Annotated[int | None, Form()] = None):
     return age
 
 
 @app.post("/multipart")
 async def post_multi_part(
-    age: Annotated[Optional[int], Form()] = None,
-    file: Annotated[Optional[bytes], File()] = None,
+    age: Annotated[int | None, Form()] = None,
+    file: Annotated[bytes | None, File()] = None,
 ):
     return {"file": file, "age": age}
 
index 7d03d29572d1b2f3988a50212a85b0c445d84192..4575e3335e677a27dc37999bad76ca3271833245 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 from fastapi import FastAPI, Form
 from fastapi.testclient import TestClient
@@ -10,7 +10,7 @@ app = FastAPI()
 class FormModel(BaseModel):
     username: str
     lastname: str
-    age: Optional[int] = None
+    age: int | None = None
     tags: list[str] = ["foo", "bar"]
     alias_with: str = Field(alias="with", default="nothing")
 
index bb20a4a1aa98c66bab561cee1b68628946febb80..2cf74e187a293bf4afeb1b45acdffc5c5a19cfe4 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import APIRouter, FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -22,7 +20,7 @@ def get_user(user_id: str):
 
 
 @item_router.get("/")
-def get_items(user_id: Optional[str] = None):
+def get_items(user_id: str | None = None):
     if user_id is None:
         return [{"item_id": "i1", "user_id": "u1"}, {"item_id": "i2", "user_id": "u2"}]
     else:
@@ -30,7 +28,7 @@ def get_items(user_id: Optional[str] = None):
 
 
 @item_router.get("/{item_id}")
-def get_item(item_id: str, user_id: Optional[str] = None):
+def get_item(item_id: str, user_id: str | None = None):
     if user_id is None:
         return {"item_id": item_id}
     else:
index 3695344f7a4acaa03dcbd8b63fc3746624ffd070..d137f6805edc8b64cbac1f90fad8ed90987933d2 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from fastapi import FastAPI, Query
 from pydantic import BaseModel
@@ -61,5 +59,5 @@ def test_invalid_simple_dict():
             title: str
 
         @app.get("/items/")
-        def read_items(q: Optional[dict] = Query(default=None)):
+        def read_items(q: dict | None = Query(default=None)):
             pass  # pragma: no cover
index 4528dff4406daa275bfecacf43fc6b74193213e4..595202beaf371044823a35c9c79479ac53d2d52b 100644 (file)
@@ -6,7 +6,7 @@ from decimal import Decimal
 from enum import Enum
 from math import isinf, isnan
 from pathlib import PurePath, PurePosixPath, PureWindowsPath
-from typing import Optional, TypedDict
+from typing import TypedDict
 
 import pytest
 from fastapi._compat import Undefined
@@ -57,7 +57,7 @@ class RoleEnum(Enum):
 
 
 class ModelWithConfig(BaseModel):
-    role: Optional[RoleEnum] = None
+    role: RoleEnum | None = None
 
     model_config = {"use_enum_values": True}
 
index deb74d8a0fa5b32229a6807612d6bedb3e9f1b91..e27dd2be085f3852bab42660c46834745a0255c9 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 from fastapi import Body, Cookie, FastAPI, Header, Path, Query
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -57,7 +55,7 @@ def path_examples(
 
 @app.get("/query_examples/")
 def query_examples(
-    data: Union[str, None] = Query(
+    data: str | None = Query(
         default=None,
         examples=[
             "json_schema_query1",
@@ -80,7 +78,7 @@ def query_examples(
 
 @app.get("/header_examples/")
 def header_examples(
-    data: Union[str, None] = Header(
+    data: str | None = Header(
         default=None,
         examples=[
             "json_schema_header1",
@@ -103,7 +101,7 @@ def header_examples(
 
 @app.get("/cookie_examples/")
 def cookie_examples(
-    data: Union[str, None] = Cookie(
+    data: str | None = Cookie(
         default=None,
         examples=["json_schema_cookie1", "json_schema_cookie2"],
         openapi_examples={
index 836a0a7ee5719af70f37b0ded73b18e35fc4a5dd..118d5181495ab7613ee3ecc2f864e4ae689ad440 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -26,7 +24,7 @@ app = FastAPI()
         ]
     },
 )
-def route_with_extra_query_parameters(standard_query_param: Optional[int] = 50):
+def route_with_extra_query_parameters(standard_query_param: int | None = 50):
     return {}
 
 
index 98d9787455c08ab185d6298ef772d84e0b79a4ee..e8166d2fb989fe5c5c54f90dcabca8d0b14e284c 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional, Union
-
 import pytest
 from fastapi.openapi.models import Schema, SchemaType
 
@@ -13,7 +11,7 @@ from fastapi.openapi.models import Schema, SchemaType
     ],
 )
 def test_allowed_schema_type(
-    type_value: Optional[Union[SchemaType, list[SchemaType]]],
+    type_value: SchemaType | list[SchemaType] | None,
 ) -> None:
     """Test that Schema accepts SchemaType, List[SchemaType] and None for type field."""
     schema = Schema(type=type_value)
index 0efeece01734d55e8e06fb8befbdbab6a5ae61a8..50255ed09aa579b5eed5b4dc67e8c01bf760cd1e 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -8,15 +6,15 @@ from pydantic import BaseModel, computed_field
 
 class SubItem(BaseModel):
     subname: str
-    sub_description: Optional[str] = None
+    sub_description: str | None = None
     tags: list[str] = []
     model_config = {"json_schema_serialization_defaults_required": True}
 
 
 class Item(BaseModel):
     name: str
-    description: Optional[str] = None
-    sub: Optional[SubItem] = None
+    description: str | None = None
+    sub: SubItem | None = None
     model_config = {"json_schema_serialization_defaults_required": True}
 
 
index 68602586434b4ccfc92bd722b42322ff5de313d1..a57e6358fd6f5bb1b104557ebbac395cd7cea688 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, File
 from fastapi.testclient import TestClient
 
@@ -7,7 +5,7 @@ app = FastAPI()
 
 
 @app.post("/files")
-async def upload_files(files: Optional[list[bytes]] = File(None)):
+async def upload_files(files: list[bytes] | None = File(None)):
     if files is None:
         return {"files_count": 0}
     return {"files_count": len(files), "sizes": [len(f) for f in files]}
index 1fd40dcd218b903281b4ca94d9344aa65ec56d7f..e6642daeac853cd82307bdf7d48bbff87fbb7530 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from fastapi.params import Param
 from fastapi.testclient import TestClient
@@ -8,7 +6,7 @@ app = FastAPI()
 
 
 @app.get("/items/")
-def read_items(q: Optional[str] = Param(default=None)):  # type: ignore
+def read_items(q: str | None = Param(default=None)):  # type: ignore
     return {"q": q}
 
 
index 463fb51b10ef4842e45bc289c9ec46e5b52e5380..727552b4668b3cdfc410735f9c654a5536610171 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from fastapi import Cookie, FastAPI, Header, Path, Query
 from fastapi.testclient import TestClient
@@ -10,14 +8,14 @@ app = FastAPI()
 
 @app.get("/hidden_cookie")
 async def hidden_cookie(
-    hidden_cookie: Optional[str] = Cookie(default=None, include_in_schema=False),
+    hidden_cookie: str | None = Cookie(default=None, include_in_schema=False),
 ):
     return {"hidden_cookie": hidden_cookie}
 
 
 @app.get("/hidden_header")
 async def hidden_header(
-    hidden_header: Optional[str] = Header(default=None, include_in_schema=False),
+    hidden_header: str | None = Header(default=None, include_in_schema=False),
 ):
     return {"hidden_header": hidden_header}
 
@@ -29,7 +27,7 @@ async def hidden_path(hidden_path: str = Path(include_in_schema=False)):
 
 @app.get("/hidden_query")
 async def hidden_query(
-    hidden_query: Optional[str] = Query(default=None, include_in_schema=False),
+    hidden_query: str | None = Query(default=None, include_in_schema=False),
 ):
     return {"hidden_query": hidden_query}
 
index 13229a3137b28f0651408f1878234a17bc7640a6..044fdf0d65d7650d2472131da501133bc076485e 100644 (file)
@@ -1,6 +1,5 @@
 import sys
 import warnings
-from typing import Union
 
 import pytest
 
@@ -80,7 +79,7 @@ def test_raises_pydantic_v1_model_in_union() -> None:
     with pytest.raises(PydanticV1NotSupportedError):
 
         @app.post("/union")
-        def endpoint(data: Union[dict, ModelV1A]):  # pragma: no cover
+        def endpoint(data: dict | ModelV1A):  # pragma: no cover
             return data
 
 
index b72b0518a131e6446a097f64f2ac7e46e198c3a2..4f7b0b2a0ac5408f316abcb749db136ec768eb46 100644 (file)
@@ -2,7 +2,6 @@ from __future__ import annotations
 
 import uuid
 from dataclasses import dataclass, field
-from typing import Union
 
 from dirty_equals import IsUUID
 from fastapi import FastAPI
@@ -16,8 +15,8 @@ class Item:
     name: str
     price: float
     tags: list[str] = field(default_factory=list)
-    description: Union[str, None] = None
-    tax: Union[float, None] = None
+    description: str | None = None
+    tax: float | None = None
 
 
 app = FastAPI()
index 970e6a6607e99c54033def7ac528feefb4b5a876..aa9745f84f84453ef475a761dc4bed5b23a4e199 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Union
+from typing import Annotated
 
 import pytest
 from dirty_equals import IsOneOf, IsPartialDict
@@ -55,7 +55,7 @@ def test_required_list_str_schema(path: str):
     "path",
     ["/required-list-str", "/model-required-list-str"],
 )
-def test_required_list_str_missing(path: str, json: Union[dict, None]):
+def test_required_list_str_missing(path: str, json: dict | None):
     client = TestClient(app)
     response = client.post(path, json=json)
     assert response.status_code == 422
@@ -132,7 +132,7 @@ def test_required_list_str_alias_schema(path: str):
     "path",
     ["/required-list-alias", "/model-required-list-alias"],
 )
-def test_required_list_alias_missing(path: str, json: Union[dict, None]):
+def test_required_list_alias_missing(path: str, json: dict | None):
     client = TestClient(app)
     response = client.post(path, json=json)
     assert response.status_code == 422
@@ -236,7 +236,7 @@ def test_required_list_validation_alias_schema(path: str):
         "/model-required-list-validation-alias",
     ],
 )
-def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]):
+def test_required_list_validation_alias_missing(path: str, json: dict | None):
     client = TestClient(app)
     response = client.post(path, json=json)
     assert response.status_code == 422
index ba8ba9092ee9ca2cd00fcfcf7e6ddc692b551228..2c5c5d61b67294e0f1c557ff1d33496359be5736 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import Body, FastAPI
@@ -15,13 +15,13 @@ app = FastAPI()
 
 @app.post("/optional-list-str", operation_id="optional_list_str")
 async def read_optional_list_str(
-    p: Annotated[Optional[list[str]], Body(embed=True)] = None,
+    p: Annotated[list[str] | None, Body(embed=True)] = None,
 ):
     return {"p": p}
 
 
 class BodyModelOptionalListStr(BaseModel):
-    p: Optional[list[str]] = None
+    p: list[str] | None = None
 
 
 @app.post("/model-optional-list-str", operation_id="model_optional_list_str")
@@ -103,13 +103,13 @@ def test_optional_list_str(path: str):
 
 @app.post("/optional-list-alias", operation_id="optional_list_alias")
 async def read_optional_list_alias(
-    p: Annotated[Optional[list[str]], Body(embed=True, alias="p_alias")] = None,
+    p: Annotated[list[str] | None, Body(embed=True, alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class BodyModelOptionalListAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, alias="p_alias")
+    p: list[str] | None = Field(None, alias="p_alias")
 
 
 @app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
@@ -208,14 +208,14 @@ def test_optional_list_alias_by_alias(path: str):
 )
 def read_optional_list_validation_alias(
     p: Annotated[
-        Optional[list[str]], Body(embed=True, validation_alias="p_val_alias")
+        list[str] | None, Body(embed=True, validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class BodyModelOptionalListValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, validation_alias="p_val_alias")
+    p: list[str] | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.post(
@@ -323,7 +323,7 @@ def test_optional_list_validation_alias_by_validation_alias(path: str):
 )
 def read_optional_list_alias_and_validation_alias(
     p: Annotated[
-        Optional[list[str]],
+        list[str] | None,
         Body(embed=True, alias="p_alias", validation_alias="p_val_alias"),
     ] = None,
 ):
@@ -331,9 +331,7 @@ def read_optional_list_alias_and_validation_alias(
 
 
 class BodyModelOptionalListAliasAndValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(
-        None, alias="p_alias", validation_alias="p_val_alias"
-    )
+    p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.post(
index b9c18034da420f415068eaf021fe86013260c31c..184fc94cb2094915c5669437e75f64426ec4b462 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import Body, FastAPI
@@ -14,12 +14,12 @@ app = FastAPI()
 
 
 @app.post("/optional-str", operation_id="optional_str")
-async def read_optional_str(p: Annotated[Optional[str], Body(embed=True)] = None):
+async def read_optional_str(p: Annotated[str | None, Body(embed=True)] = None):
     return {"p": p}
 
 
 class BodyModelOptionalStr(BaseModel):
-    p: Optional[str] = None
+    p: str | None = None
 
 
 @app.post("/model-optional-str", operation_id="model_optional_str")
@@ -98,13 +98,13 @@ def test_optional_str(path: str):
 
 @app.post("/optional-alias", operation_id="optional_alias")
 async def read_optional_alias(
-    p: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None,
+    p: Annotated[str | None, Body(embed=True, alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class BodyModelOptionalAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias")
+    p: str | None = Field(None, alias="p_alias")
 
 
 @app.post("/model-optional-alias", operation_id="model_optional_alias")
@@ -197,15 +197,13 @@ def test_optional_alias_by_alias(path: str):
 
 @app.post("/optional-validation-alias", operation_id="optional_validation_alias")
 def read_optional_validation_alias(
-    p: Annotated[
-        Optional[str], Body(embed=True, validation_alias="p_val_alias")
-    ] = None,
+    p: Annotated[str | None, Body(embed=True, validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class BodyModelOptionalValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, validation_alias="p_val_alias")
+    p: str | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.post(
@@ -309,14 +307,14 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 )
 def read_optional_alias_and_validation_alias(
     p: Annotated[
-        Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
+        str | None, Body(embed=True, alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class BodyModelOptionalAliasAndValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+    p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.post(
index 5b434fa1dbf28d999e11d4579746dc3ed136de99..2e02f8d2038e42cc50747b12f79c11dd7b8fec3c 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Any, Union
+from typing import Annotated, Any
 
 import pytest
 from dirty_equals import IsOneOf
@@ -51,7 +51,7 @@ def test_required_str_schema(path: str):
     "path",
     ["/required-str", "/model-required-str"],
 )
-def test_required_str_missing(path: str, json: Union[dict[str, Any], None]):
+def test_required_str_missing(path: str, json: dict[str, Any] | None):
     client = TestClient(app)
     response = client.post(path, json=json)
     assert response.status_code == 422
@@ -124,7 +124,7 @@ def test_required_str_alias_schema(path: str):
     "path",
     ["/required-alias", "/model-required-alias"],
 )
-def test_required_alias_missing(path: str, json: Union[dict[str, Any], None]):
+def test_required_alias_missing(path: str, json: dict[str, Any] | None):
     client = TestClient(app)
     response = client.post(path, json=json)
     assert response.status_code == 422
@@ -221,9 +221,7 @@ def test_required_validation_alias_schema(path: str):
         "/model-required-validation-alias",
     ],
 )
-def test_required_validation_alias_missing(
-    path: str, json: Union[dict[str, Any], None]
-):
+def test_required_validation_alias_missing(path: str, json: dict[str, Any] | None):
     client = TestClient(app)
     response = client.post(path, json=json)
     assert response.status_code == 422
@@ -338,7 +336,7 @@ def test_required_alias_and_validation_alias_schema(path: str):
     ],
 )
 def test_required_alias_and_validation_alias_missing(
-    path: str, json: Union[dict[str, Any], None]
+    path: str, json: dict[str, Any] | None
 ):
     client = TestClient(app)
     response = client.post(path, json=json)
index 1b2a18b072ca8d4e03d65ea68dab19379964f397..227d2bccc221bfbb2190066a3f869f42971c850a 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import Cookie, FastAPI
@@ -13,12 +13,12 @@ app = FastAPI()
 
 
 @app.get("/optional-str")
-async def read_optional_str(p: Annotated[Optional[str], Cookie()] = None):
+async def read_optional_str(p: Annotated[str | None, Cookie()] = None):
     return {"p": p}
 
 
 class CookieModelOptionalStr(BaseModel):
-    p: Optional[str] = None
+    p: str | None = None
 
 
 @app.get("/model-optional-str")
@@ -75,13 +75,13 @@ def test_optional_str(path: str):
 
 @app.get("/optional-alias")
 async def read_optional_alias(
-    p: Annotated[Optional[str], Cookie(alias="p_alias")] = None,
+    p: Annotated[str | None, Cookie(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class CookieModelOptionalAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias")
+    p: str | None = Field(None, alias="p_alias")
 
 
 @app.get("/model-optional-alias")
@@ -153,13 +153,13 @@ def test_optional_alias_by_alias(path: str):
 
 @app.get("/optional-validation-alias")
 def read_optional_validation_alias(
-    p: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None,
+    p: Annotated[str | None, Cookie(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class CookieModelOptionalValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, validation_alias="p_val_alias")
+    p: str | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-validation-alias")
@@ -237,14 +237,14 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 @app.get("/optional-alias-and-validation-alias")
 def read_optional_alias_and_validation_alias(
     p: Annotated[
-        Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias")
+        str | None, Cookie(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class CookieModelOptionalAliasAndValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+    p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-alias-and-validation-alias")
index 45ef7bdec4efcf4d9b7527f3a59cad1d02049db7..b4dc11a06aad47160cfaf2104e4705a62a90e094 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, File, UploadFile
@@ -13,12 +13,12 @@ app = FastAPI()
 
 
 @app.post("/optional-bytes", operation_id="optional_bytes")
-async def read_optional_bytes(p: Annotated[Optional[bytes], File()] = None):
+async def read_optional_bytes(p: Annotated[bytes | None, File()] = None):
     return {"file_size": len(p) if p else None}
 
 
 @app.post("/optional-uploadfile", operation_id="optional_uploadfile")
-async def read_optional_uploadfile(p: Annotated[Optional[UploadFile], File()] = None):
+async def read_optional_uploadfile(p: Annotated[UploadFile | None, File()] = None):
     return {"file_size": p.size if p else None}
 
 
@@ -82,14 +82,14 @@ def test_optional(path: str):
 
 @app.post("/optional-bytes-alias", operation_id="optional_bytes_alias")
 async def read_optional_bytes_alias(
-    p: Annotated[Optional[bytes], File(alias="p_alias")] = None,
+    p: Annotated[bytes | None, File(alias="p_alias")] = None,
 ):
     return {"file_size": len(p) if p else None}
 
 
 @app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias")
 async def read_optional_uploadfile_alias(
-    p: Annotated[Optional[UploadFile], File(alias="p_alias")] = None,
+    p: Annotated[UploadFile | None, File(alias="p_alias")] = None,
 ):
     return {"file_size": p.size if p else None}
 
@@ -170,7 +170,7 @@ def test_optional_alias_by_alias(path: str):
     "/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias"
 )
 def read_optional_bytes_validation_alias(
-    p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None,
+    p: Annotated[bytes | None, File(validation_alias="p_val_alias")] = None,
 ):
     return {"file_size": len(p) if p else None}
 
@@ -180,7 +180,7 @@ def read_optional_bytes_validation_alias(
     operation_id="optional_uploadfile_validation_alias",
 )
 def read_optional_uploadfile_validation_alias(
-    p: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None,
+    p: Annotated[UploadFile | None, File(validation_alias="p_val_alias")] = None,
 ):
     return {"file_size": p.size if p else None}
 
@@ -263,7 +263,7 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 )
 def read_optional_bytes_alias_and_validation_alias(
     p: Annotated[
-        Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias")
+        bytes | None, File(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"file_size": len(p) if p else None}
@@ -275,7 +275,7 @@ def read_optional_bytes_alias_and_validation_alias(
 )
 def read_optional_uploadfile_alias_and_validation_alias(
     p: Annotated[
-        Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias")
+        UploadFile | None, File(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"file_size": p.size if p else None}
index 162fbe08ae195967ba6cb0bdb935408509c8b3c9..a506ec991fba61f4fc57bbf88adf0dcb5288fea9 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, File, UploadFile
@@ -13,13 +13,13 @@ app = FastAPI()
 
 
 @app.post("/optional-list-bytes")
-async def read_optional_list_bytes(p: Annotated[Optional[list[bytes]], File()] = None):
+async def read_optional_list_bytes(p: Annotated[list[bytes] | None, File()] = None):
     return {"file_size": [len(file) for file in p] if p else None}
 
 
 @app.post("/optional-list-uploadfile")
 async def read_optional_list_uploadfile(
-    p: Annotated[Optional[list[UploadFile]], File()] = None,
+    p: Annotated[list[UploadFile] | None, File()] = None,
 ):
     return {"file_size": [file.size for file in p] if p else None}
 
@@ -87,14 +87,14 @@ def test_optional_list(path: str):
 
 @app.post("/optional-list-bytes-alias")
 async def read_optional_list_bytes_alias(
-    p: Annotated[Optional[list[bytes]], File(alias="p_alias")] = None,
+    p: Annotated[list[bytes] | None, File(alias="p_alias")] = None,
 ):
     return {"file_size": [len(file) for file in p] if p else None}
 
 
 @app.post("/optional-list-uploadfile-alias")
 async def read_optional_list_uploadfile_alias(
-    p: Annotated[Optional[list[UploadFile]], File(alias="p_alias")] = None,
+    p: Annotated[list[UploadFile] | None, File(alias="p_alias")] = None,
 ):
     return {"file_size": [file.size for file in p] if p else None}
 
@@ -176,16 +176,14 @@ def test_optional_list_alias_by_alias(path: str):
 
 @app.post("/optional-list-bytes-validation-alias")
 def read_optional_list_bytes_validation_alias(
-    p: Annotated[Optional[list[bytes]], File(validation_alias="p_val_alias")] = None,
+    p: Annotated[list[bytes] | None, File(validation_alias="p_val_alias")] = None,
 ):
     return {"file_size": [len(file) for file in p] if p else None}
 
 
 @app.post("/optional-list-uploadfile-validation-alias")
 def read_optional_list_uploadfile_validation_alias(
-    p: Annotated[
-        Optional[list[UploadFile]], File(validation_alias="p_val_alias")
-    ] = None,
+    p: Annotated[list[UploadFile] | None, File(validation_alias="p_val_alias")] = None,
 ):
     return {"file_size": [file.size for file in p] if p else None}
 
@@ -270,7 +268,7 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 @app.post("/optional-list-bytes-alias-and-validation-alias")
 def read_optional_list_bytes_alias_and_validation_alias(
     p: Annotated[
-        Optional[list[bytes]], File(alias="p_alias", validation_alias="p_val_alias")
+        list[bytes] | None, File(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"file_size": [len(file) for file in p] if p else None}
@@ -279,7 +277,7 @@ def read_optional_list_bytes_alias_and_validation_alias(
 @app.post("/optional-list-uploadfile-alias-and-validation-alias")
 def read_optional_list_uploadfile_alias_and_validation_alias(
     p: Annotated[
-        Optional[list[UploadFile]],
+        list[UploadFile] | None,
         File(alias="p_alias", validation_alias="p_val_alias"),
     ] = None,
 ):
index 6d1957a18c3a8bcff20bbfc5bb4ef65cebc4ef5b..7ecf9c9bfc951e2a6310df497dadc173f37a4164 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, Form
@@ -15,13 +15,13 @@ app = FastAPI()
 
 @app.post("/optional-list-str", operation_id="optional_list_str")
 async def read_optional_list_str(
-    p: Annotated[Optional[list[str]], Form()] = None,
+    p: Annotated[list[str] | None, Form()] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalListStr(BaseModel):
-    p: Optional[list[str]] = None
+    p: list[str] | None = None
 
 
 @app.post("/model-optional-list-str", operation_id="model_optional_list_str")
@@ -80,13 +80,13 @@ def test_optional_list_str(path: str):
 
 @app.post("/optional-list-alias", operation_id="optional_list_alias")
 async def read_optional_list_alias(
-    p: Annotated[Optional[list[str]], Form(alias="p_alias")] = None,
+    p: Annotated[list[str] | None, Form(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalListAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, alias="p_alias")
+    p: list[str] | None = Field(None, alias="p_alias")
 
 
 @app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
@@ -163,13 +163,13 @@ def test_optional_list_alias_by_alias(path: str):
     "/optional-list-validation-alias", operation_id="optional_list_validation_alias"
 )
 def read_optional_list_validation_alias(
-    p: Annotated[Optional[list[str]], Form(validation_alias="p_val_alias")] = None,
+    p: Annotated[list[str] | None, Form(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalListValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, validation_alias="p_val_alias")
+    p: list[str] | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.post(
@@ -251,16 +251,14 @@ def test_optional_list_validation_alias_by_validation_alias(path: str):
 )
 def read_optional_list_alias_and_validation_alias(
     p: Annotated[
-        Optional[list[str]], Form(alias="p_alias", validation_alias="p_val_alias")
+        list[str] | None, Form(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalListAliasAndValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(
-        None, alias="p_alias", validation_alias="p_val_alias"
-    )
+    p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.post(
index 810e83caa38c7fe4ab99cac7d547111b17886260..4ef16756ef43474b74e3532f6f39d00367b82272 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, Form
@@ -14,12 +14,12 @@ app = FastAPI()
 
 
 @app.post("/optional-str", operation_id="optional_str")
-async def read_optional_str(p: Annotated[Optional[str], Form()] = None):
+async def read_optional_str(p: Annotated[str | None, Form()] = None):
     return {"p": p}
 
 
 class FormModelOptionalStr(BaseModel):
-    p: Optional[str] = None
+    p: str | None = None
 
 
 @app.post("/model-optional-str", operation_id="model_optional_str")
@@ -75,13 +75,13 @@ def test_optional_str(path: str):
 
 @app.post("/optional-alias", operation_id="optional_alias")
 async def read_optional_alias(
-    p: Annotated[Optional[str], Form(alias="p_alias")] = None,
+    p: Annotated[str | None, Form(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias")
+    p: str | None = Field(None, alias="p_alias")
 
 
 @app.post("/model-optional-alias", operation_id="model_optional_alias")
@@ -151,13 +151,13 @@ def test_optional_alias_by_alias(path: str):
 
 @app.post("/optional-validation-alias", operation_id="optional_validation_alias")
 def read_optional_validation_alias(
-    p: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None,
+    p: Annotated[str | None, Form(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, validation_alias="p_val_alias")
+    p: str | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.post(
@@ -238,14 +238,14 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 )
 def read_optional_alias_and_validation_alias(
     p: Annotated[
-        Optional[str], Form(alias="p_alias", validation_alias="p_val_alias")
+        str | None, Form(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class FormModelOptionalAliasAndValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+    p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.post(
index 3bbb73d5442459fccd5f590bd6c0e0c5af1428e0..9f4eacc2357b464d4c4af4ba9f1c02ee6fd61f5c 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, Header
@@ -14,13 +14,13 @@ app = FastAPI()
 
 @app.get("/optional-list-str")
 async def read_optional_list_str(
-    p: Annotated[Optional[list[str]], Header()] = None,
+    p: Annotated[list[str] | None, Header()] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalListStr(BaseModel):
-    p: Optional[list[str]] = None
+    p: list[str] | None = None
 
 
 @app.get("/model-optional-list-str")
@@ -81,13 +81,13 @@ def test_optional_list_str(path: str):
 
 @app.get("/optional-list-alias")
 async def read_optional_list_alias(
-    p: Annotated[Optional[list[str]], Header(alias="p_alias")] = None,
+    p: Annotated[list[str] | None, Header(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalListAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, alias="p_alias")
+    p: list[str] | None = Field(None, alias="p_alias")
 
 
 @app.get("/model-optional-list-alias")
@@ -162,13 +162,13 @@ def test_optional_list_alias_by_alias(path: str):
 
 @app.get("/optional-list-validation-alias")
 def read_optional_list_validation_alias(
-    p: Annotated[Optional[list[str]], Header(validation_alias="p_val_alias")] = None,
+    p: Annotated[list[str] | None, Header(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalListValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, validation_alias="p_val_alias")
+    p: list[str] | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-list-validation-alias")
@@ -246,16 +246,14 @@ def test_optional_list_validation_alias_by_validation_alias(path: str):
 @app.get("/optional-list-alias-and-validation-alias")
 def read_optional_list_alias_and_validation_alias(
     p: Annotated[
-        Optional[list[str]], Header(alias="p_alias", validation_alias="p_val_alias")
+        list[str] | None, Header(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalListAliasAndValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(
-        None, alias="p_alias", validation_alias="p_val_alias"
-    )
+    p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-list-alias-and-validation-alias")
index a5174e59af24ea9979093a1205b11aae0bb9527b..04773c83f96763bebac8588090b41837b46222bd 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, Header
@@ -13,12 +13,12 @@ app = FastAPI()
 
 
 @app.get("/optional-str")
-async def read_optional_str(p: Annotated[Optional[str], Header()] = None):
+async def read_optional_str(p: Annotated[str | None, Header()] = None):
     return {"p": p}
 
 
 class HeaderModelOptionalStr(BaseModel):
-    p: Optional[str] = None
+    p: str | None = None
 
 
 @app.get("/model-optional-str")
@@ -74,13 +74,13 @@ def test_optional_str(path: str):
 
 @app.get("/optional-alias")
 async def read_optional_alias(
-    p: Annotated[Optional[str], Header(alias="p_alias")] = None,
+    p: Annotated[str | None, Header(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias")
+    p: str | None = Field(None, alias="p_alias")
 
 
 @app.get("/model-optional-alias")
@@ -150,13 +150,13 @@ def test_optional_alias_by_alias(path: str):
 
 @app.get("/optional-validation-alias")
 def read_optional_validation_alias(
-    p: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None,
+    p: Annotated[str | None, Header(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, validation_alias="p_val_alias")
+    p: str | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-validation-alias")
@@ -232,14 +232,14 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 @app.get("/optional-alias-and-validation-alias")
 def read_optional_alias_and_validation_alias(
     p: Annotated[
-        Optional[str], Header(alias="p_alias", validation_alias="p_val_alias")
+        str | None, Header(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class HeaderModelOptionalAliasAndValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+    p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-alias-and-validation-alias")
index 5608c6499b8ee88ba2fd152fd07c497f8585c2ee..6b70b75a4fc9edfc0aef4cd8230b0b5504e93da4 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, Query
@@ -14,13 +14,13 @@ app = FastAPI()
 
 @app.get("/optional-list-str")
 async def read_optional_list_str(
-    p: Annotated[Optional[list[str]], Query()] = None,
+    p: Annotated[list[str] | None, Query()] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalListStr(BaseModel):
-    p: Optional[list[str]] = None
+    p: list[str] | None = None
 
 
 @app.get("/model-optional-list-str")
@@ -81,13 +81,13 @@ def test_optional_list_str(path: str):
 
 @app.get("/optional-list-alias")
 async def read_optional_list_alias(
-    p: Annotated[Optional[list[str]], Query(alias="p_alias")] = None,
+    p: Annotated[list[str] | None, Query(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalListAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, alias="p_alias")
+    p: list[str] | None = Field(None, alias="p_alias")
 
 
 @app.get("/model-optional-list-alias")
@@ -162,13 +162,13 @@ def test_optional_list_alias_by_alias(path: str):
 
 @app.get("/optional-list-validation-alias")
 def read_optional_list_validation_alias(
-    p: Annotated[Optional[list[str]], Query(validation_alias="p_val_alias")] = None,
+    p: Annotated[list[str] | None, Query(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalListValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(None, validation_alias="p_val_alias")
+    p: list[str] | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-list-validation-alias")
@@ -244,16 +244,14 @@ def test_optional_list_validation_alias_by_validation_alias(path: str):
 @app.get("/optional-list-alias-and-validation-alias")
 def read_optional_list_alias_and_validation_alias(
     p: Annotated[
-        Optional[list[str]], Query(alias="p_alias", validation_alias="p_val_alias")
+        list[str] | None, Query(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalListAliasAndValidationAlias(BaseModel):
-    p: Optional[list[str]] = Field(
-        None, alias="p_alias", validation_alias="p_val_alias"
-    )
+    p: list[str] | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-list-alias-and-validation-alias")
index b181686b05f7b396d7dc933fae133abd926b4cff..f7f35860b60c26aa1a54340744e7a488c91ac398 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Optional
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI, Query
@@ -13,12 +13,12 @@ app = FastAPI()
 
 
 @app.get("/optional-str")
-async def read_optional_str(p: Optional[str] = None):
+async def read_optional_str(p: str | None = None):
     return {"p": p}
 
 
 class QueryModelOptionalStr(BaseModel):
-    p: Optional[str] = None
+    p: str | None = None
 
 
 @app.get("/model-optional-str")
@@ -74,13 +74,13 @@ def test_optional_str(path: str):
 
 @app.get("/optional-alias")
 async def read_optional_alias(
-    p: Annotated[Optional[str], Query(alias="p_alias")] = None,
+    p: Annotated[str | None, Query(alias="p_alias")] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias")
+    p: str | None = Field(None, alias="p_alias")
 
 
 @app.get("/model-optional-alias")
@@ -150,13 +150,13 @@ def test_optional_alias_by_alias(path: str):
 
 @app.get("/optional-validation-alias")
 def read_optional_validation_alias(
-    p: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None,
+    p: Annotated[str | None, Query(validation_alias="p_val_alias")] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, validation_alias="p_val_alias")
+    p: str | None = Field(None, validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-validation-alias")
@@ -232,14 +232,14 @@ def test_optional_validation_alias_by_validation_alias(path: str):
 @app.get("/optional-alias-and-validation-alias")
 def read_optional_alias_and_validation_alias(
     p: Annotated[
-        Optional[str], Query(alias="p_alias", validation_alias="p_val_alias")
+        str | None, Query(alias="p_alias", validation_alias="p_val_alias")
     ] = None,
 ):
     return {"p": p}
 
 
 class QueryModelOptionalAliasAndValidationAlias(BaseModel):
-    p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
+    p: str | None = Field(None, alias="p_alias", validation_alias="p_val_alias")
 
 
 @app.get("/model-optional-alias-and-validation-alias")
index 5da8cd4d09095c3e9a09c5a4ef6e06d69bc84a38..c99f20212f7c9fba90af10b0f376b7cc82dcd5a8 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 from fastapi import Body, FastAPI, Query
 from fastapi.testclient import TestClient
 
@@ -7,17 +5,17 @@ app = FastAPI()
 
 
 @app.get("/query")
-def read_query(q: Union[str, None]):
+def read_query(q: str | None):
     return q
 
 
 @app.get("/explicit-query")
-def read_explicit_query(q: Union[str, None] = Query()):
+def read_explicit_query(q: str | None = Query()):
     return q
 
 
 @app.post("/body-embed")
-def send_body_embed(b: Union[str, None] = Body(embed=True)):
+def send_body_embed(b: str | None = Body(embed=True)):
     return b
 
 
index ded5971027750d1329f7e8dd6e4ee8743df7fa96..7be7902adaeb63e2463e4736a6320129feefa1eb 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 import pytest
 from fastapi import FastAPI
 from fastapi.exceptions import FastAPIError, ResponseValidationError
@@ -216,7 +214,7 @@ def no_response_model_annotation_forward_ref_list_of_model() -> "list[User]":
 
 @app.get(
     "/response_model_union-no_annotation-return_model1",
-    response_model=Union[User, Item],
+    response_model=User | Item,
 )
 def response_model_union_no_annotation_return_model1():
     return DBUser(name="John", surname="Doe", password_hash="secret")
@@ -224,19 +222,19 @@ def response_model_union_no_annotation_return_model1():
 
 @app.get(
     "/response_model_union-no_annotation-return_model2",
-    response_model=Union[User, Item],
+    response_model=User | Item,
 )
 def response_model_union_no_annotation_return_model2():
     return Item(name="Foo", price=42.0)
 
 
 @app.get("/no_response_model-annotation_union-return_model1")
-def no_response_model_annotation_union_return_model1() -> Union[User, Item]:
+def no_response_model_annotation_union_return_model1() -> User | Item:
     return DBUser(name="John", surname="Doe", password_hash="secret")
 
 
 @app.get("/no_response_model-annotation_union-return_model2")
-def no_response_model_annotation_union_return_model2() -> Union[User, Item]:
+def no_response_model_annotation_union_return_model2() -> User | Item:
     return Item(name="Foo", price=42.0)
 
 
@@ -503,7 +501,7 @@ def test_invalid_response_model_field():
     with pytest.raises(FastAPIError) as e:
 
         @app.get("/")
-        def read_root() -> Union[Response, None]:
+        def read_root() -> Response | None:
             return Response(content="Foo")  # pragma: no cover
 
     assert "valid Pydantic field type" in e.value.args[0]
index a47d11913991a11119a3ffa70475fb6975f8541c..7869a7afcdc9f4b78fd7a5c775e6312fbfcf7e5c 100644 (file)
@@ -1,6 +1,5 @@
 from collections.abc import AsyncGenerator
 from contextlib import asynccontextmanager
-from typing import Union
 
 import pytest
 from fastapi import APIRouter, FastAPI, Request
@@ -176,7 +175,7 @@ def test_router_nested_lifespan_state_overriding_by_parent() -> None:
     @asynccontextmanager
     async def lifespan(
         app: FastAPI,
-    ) -> AsyncGenerator[dict[str, Union[str, bool]], None]:
+    ) -> AsyncGenerator[dict[str, str | bool], None]:
         yield {
             "app_specific": True,
             "overridden": "app",
@@ -185,7 +184,7 @@ def test_router_nested_lifespan_state_overriding_by_parent() -> None:
     @asynccontextmanager
     async def router_lifespan(
         app: FastAPI,
-    ) -> AsyncGenerator[dict[str, Union[str, bool]], None]:
+    ) -> AsyncGenerator[dict[str, str | bool], None]:
         yield {
             "router_specific": True,
             "overridden": "router",  # should override parent
index 9ec41e7e84a4d9f0ba1051099177f0a804cca343..32f5cea476284990f7fa373e3f4aed88f35b8a27 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Union
-
 import pytest
 from fastapi import Body, Cookie, FastAPI, Header, Path, Query
 from fastapi.exceptions import FastAPIDeprecationWarning
@@ -117,7 +115,7 @@ def create_app():
 
         @app.get("/query_example/")
         def query_example(
-            data: Union[str, None] = Query(
+            data: str | None = Query(
                 default=None,
                 example="query1",
             ),
@@ -126,7 +124,7 @@ def create_app():
 
     @app.get("/query_examples/")
     def query_examples(
-        data: Union[str, None] = Query(
+        data: str | None = Query(
             default=None,
             examples=["query1", "query2"],
         ),
@@ -137,7 +135,7 @@ def create_app():
 
         @app.get("/query_example_examples/")
         def query_example_examples(
-            data: Union[str, None] = Query(
+            data: str | None = Query(
                 default=None,
                 example="query_overridden",
                 examples=["query1", "query2"],
@@ -149,7 +147,7 @@ def create_app():
 
         @app.get("/header_example/")
         def header_example(
-            data: Union[str, None] = Header(
+            data: str | None = Header(
                 default=None,
                 example="header1",
             ),
@@ -158,7 +156,7 @@ def create_app():
 
     @app.get("/header_examples/")
     def header_examples(
-        data: Union[str, None] = Header(
+        data: str | None = Header(
             default=None,
             examples=[
                 "header1",
@@ -172,7 +170,7 @@ def create_app():
 
         @app.get("/header_example_examples/")
         def header_example_examples(
-            data: Union[str, None] = Header(
+            data: str | None = Header(
                 default=None,
                 example="header_overridden",
                 examples=["header1", "header2"],
@@ -184,7 +182,7 @@ def create_app():
 
         @app.get("/cookie_example/")
         def cookie_example(
-            data: Union[str, None] = Cookie(
+            data: str | None = Cookie(
                 default=None,
                 example="cookie1",
             ),
@@ -193,7 +191,7 @@ def create_app():
 
     @app.get("/cookie_examples/")
     def cookie_examples(
-        data: Union[str, None] = Cookie(
+        data: str | None = Cookie(
             default=None,
             examples=["cookie1", "cookie2"],
         ),
@@ -204,7 +202,7 @@ def create_app():
 
         @app.get("/cookie_example_examples/")
         def cookie_example_examples(
-            data: Union[str, None] = Cookie(
+            data: str | None = Cookie(
                 default=None,
                 example="cookie_overridden",
                 examples=["cookie1", "cookie2"],
index 7988d80443b96a63f30d36917effad83f3e5f9de..e911654fac90306459a41cf2edc8890e6efa280c 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import Depends, FastAPI, Security
 from fastapi.security import APIKeyCookie
 from fastapi.testclient import TestClient
@@ -15,7 +13,7 @@ class User(BaseModel):
     username: str
 
 
-def get_current_user(oauth_header: Optional[str] = Security(api_key)):
+def get_current_user(oauth_header: str | None = Security(api_key)):
     if oauth_header is None:
         return None
     user = User(username=oauth_header)
index 51abd0bb964a6ba84d7aa83c7d4abd9dfd317772..0a8cf420ed00dd1a28a0537ba49e2831f9fcc043 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import Depends, FastAPI, Security
 from fastapi.security import APIKeyHeader
 from fastapi.testclient import TestClient
@@ -15,7 +13,7 @@ class User(BaseModel):
     username: str
 
 
-def get_current_user(oauth_header: Optional[str] = Security(api_key)):
+def get_current_user(oauth_header: str | None = Security(api_key)):
     if oauth_header is None:
         return None
     user = User(username=oauth_header)
@@ -23,7 +21,7 @@ def get_current_user(oauth_header: Optional[str] = Security(api_key)):
 
 
 @app.get("/users/me")
-def read_current_user(current_user: Optional[User] = Depends(get_current_user)):
+def read_current_user(current_user: User | None = Depends(get_current_user)):
     if current_user is None:
         return {"msg": "Create an account first"}
     return current_user
index 26fbb9ee4fae08b6c6c4b6d5d95038df3cee41d1..e9fba30435b5d5cf3ba8514a785aad54572f0d33 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import Depends, FastAPI, Security
 from fastapi.security import APIKeyQuery
 from fastapi.testclient import TestClient
@@ -15,7 +13,7 @@ class User(BaseModel):
     username: str
 
 
-def get_current_user(oauth_header: Optional[str] = Security(api_key)):
+def get_current_user(oauth_header: str | None = Security(api_key)):
     if oauth_header is None:
         return None
     user = User(username=oauth_header)
@@ -23,7 +21,7 @@ def get_current_user(oauth_header: Optional[str] = Security(api_key)):
 
 
 @app.get("/users/me")
-def read_current_user(current_user: Optional[User] = Depends(get_current_user)):
+def read_current_user(current_user: User | None = Depends(get_current_user)):
     if current_user is None:
         return {"msg": "Create an account first"}
     return current_user
index 612a7909fef0b0243e668fd1d7067b3310310312..1d1944ab0aca7377079b8e7e2945586e794b7d4e 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase
 from fastapi.testclient import TestClient
@@ -12,7 +10,7 @@ security = HTTPBase(scheme="Other", auto_error=False)
 
 @app.get("/users/me")
 def read_current_user(
-    credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
+    credentials: HTTPAuthorizationCredentials | None = Security(security),
 ):
     if credentials is None:
         return {"msg": "Create an account first"}
index e94565c7bb997b787f0e3b967b19c19e739b335a..78abf2b680d662c066066126bc8b407b9a7009b1 100644 (file)
@@ -1,5 +1,4 @@
 from base64 import b64encode
-from typing import Optional
 
 from fastapi import FastAPI, Security
 from fastapi.security import HTTPBasic, HTTPBasicCredentials
@@ -12,7 +11,7 @@ security = HTTPBasic(auto_error=False)
 
 
 @app.get("/users/me")
-def read_current_user(credentials: Optional[HTTPBasicCredentials] = Security(security)):
+def read_current_user(credentials: HTTPBasicCredentials | None = Security(security)):
     if credentials is None:
         return {"msg": "Create an account first"}
     return {"username": credentials.username, "password": credentials.password}
index b49a6593ec652f79e1c0a08d772a6a3e72edd791..06d9d03db4f2a8eaa82c051a58592d1ee05456ef 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.testclient import TestClient
@@ -12,7 +10,7 @@ security = HTTPBearer(auto_error=False)
 
 @app.get("/users/me")
 def read_current_user(
-    credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
+    credentials: HTTPAuthorizationCredentials | None = Security(security),
 ):
     if credentials is None:
         return {"msg": "Create an account first"}
index 97e62634d830c14190d8e6d784b87e3fe26d8853..d1056b1918039b6a6a4ac6b0f17d7988f27f6b3b 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest
 from fastapi.testclient import TestClient
@@ -12,7 +10,7 @@ security = HTTPDigest(auto_error=False)
 
 @app.get("/users/me")
 def read_current_user(
-    credentials: Optional[HTTPAuthorizationCredentials] = Security(security),
+    credentials: HTTPAuthorizationCredentials | None = Security(security),
 ):
     if credentials is None:
         return {"msg": "Create an account first"}
index 1ba577e9fff49135d44ad88b39371682bf392a98..587486c76bcb52b3956f1656dad84d8c814613ed 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security import OAuth2AuthorizationCodeBearer
 from fastapi.testclient import TestClient
@@ -13,7 +11,7 @@ oauth2_scheme = OAuth2AuthorizationCodeBearer(
 
 
 @app.get("/items/")
-async def read_items(token: Optional[str] = Security(oauth2_scheme)):
+async def read_items(token: str | None = Security(oauth2_scheme)):
     return {"token": token}
 
 
index 73807c31a336e978db8403c424b1ac0e029a2eb7..f878ede643e449bc87b768b8fba41cf7341747ed 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security import OAuth2AuthorizationCodeBearer
 from fastapi.testclient import TestClient
@@ -16,7 +14,7 @@ oauth2_scheme = OAuth2AuthorizationCodeBearer(
 
 
 @app.get("/items/")
-async def read_items(token: Optional[str] = Security(oauth2_scheme)):
+async def read_items(token: str | None = Security(oauth2_scheme)):
     return {"token": token}
 
 
index 583007c8b77d3b1805afe4253727a470b73d5916..6fcce6fed7ec46747d42c3eca8b98652302979eb 100644 (file)
@@ -1,6 +1,6 @@
 # Ref: https://github.com/fastapi/fastapi/issues/14454
 
-from typing import Annotated, Optional
+from typing import Annotated
 
 from fastapi import APIRouter, Depends, FastAPI, Security
 from fastapi.security import OAuth2AuthorizationCodeBearer
@@ -46,13 +46,13 @@ router = APIRouter(dependencies=[Security(oauth2_scheme, scopes=["read"])])
 
 
 @router.get("/items/")
-async def read_items(token: Optional[str] = Depends(oauth2_scheme)):
+async def read_items(token: str | None = Depends(oauth2_scheme)):
     return {"token": token}
 
 
 @router.post("/items/")
 async def create_item(
-    token: Optional[str] = Security(oauth2_scheme, scopes=["read", "write"]),
+    token: str | None = Security(oauth2_scheme, scopes=["read", "write"]),
 ):
     return {"token": token}
 
index cb79afdb86938877f7bc21a82f5424b7b46abf68..a7eaf594438ea02f8f2b0fac54544030975cccad 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from fastapi import Depends, FastAPI, Security
 from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
@@ -24,7 +22,7 @@ class User(BaseModel):
     username: str
 
 
-def get_current_user(oauth_header: Optional[str] = Security(reusable_oauth2)):
+def get_current_user(oauth_header: str | None = Security(reusable_oauth2)):
     if oauth_header is None:
         return None
     user = User(username=oauth_header)
@@ -37,7 +35,7 @@ def login(form_data: OAuth2PasswordRequestFormStrict = Depends()):
 
 
 @app.get("/users/me")
-def read_users_me(current_user: Optional[User] = Depends(get_current_user)):
+def read_users_me(current_user: User | None = Depends(get_current_user)):
     if current_user is None:
         return {"msg": "Create an account first"}
     return current_user
index b3fae37a170be54830998cc40c3a68cb4cfa3aa9..0918d352ead3f0402aadb2fa915af9f5952d79b7 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from fastapi import Depends, FastAPI, Security
 from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict
@@ -25,7 +23,7 @@ class User(BaseModel):
     username: str
 
 
-def get_current_user(oauth_header: Optional[str] = Security(reusable_oauth2)):
+def get_current_user(oauth_header: str | None = Security(reusable_oauth2)):
     if oauth_header is None:
         return None
     user = User(username=oauth_header)
@@ -38,7 +36,7 @@ def login(form_data: OAuth2PasswordRequestFormStrict = Depends()):
 
 
 @app.get("/users/me")
-def read_users_me(current_user: Optional[User] = Depends(get_current_user)):
+def read_users_me(current_user: User | None = Depends(get_current_user)):
     if current_user is None:
         return {"msg": "Create an account first"}
     return current_user
index 01e2f65ed97ed36ab08b936f08e8654a675b85a4..263359c950ac5cea56186e6657ccaf5c302479d9 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security import OAuth2PasswordBearer
 from fastapi.testclient import TestClient
@@ -11,7 +9,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token", auto_error=False)
 
 
 @app.get("/items/")
-async def read_items(token: Optional[str] = Security(oauth2_scheme)):
+async def read_items(token: str | None = Security(oauth2_scheme)):
     if token is None:
         return {"msg": "Create an account first"}
     return {"token": token}
index fec8d03a7a0bee43b1cdfc42f6ee5cf299e3b7c2..0deb7e48ff79ccbb9acebf9a09b9b4f1d9a737bd 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI, Security
 from fastapi.security import OAuth2PasswordBearer
 from fastapi.testclient import TestClient
@@ -15,7 +13,7 @@ oauth2_scheme = OAuth2PasswordBearer(
 
 
 @app.get("/items/")
-async def read_items(token: Optional[str] = Security(oauth2_scheme)):
+async def read_items(token: str | None = Security(oauth2_scheme)):
     if token is None:
         return {"msg": "Create an account first"}
     return {"token": token}
index ebaf394dc99569f7ae2a658f81d543d6624382c0..44e1a4866eba27c8e4788fc3c05393d7cf520430 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import Depends, FastAPI, Security
 from fastapi.security.open_id_connect_url import OpenIdConnect
 from fastapi.testclient import TestClient
@@ -15,7 +13,7 @@ class User(BaseModel):
     username: str
 
 
-def get_current_user(oauth_header: Optional[str] = Security(oid)):
+def get_current_user(oauth_header: str | None = Security(oid)):
     if oauth_header is None:
         return None
     user = User(username=oauth_header)
@@ -23,7 +21,7 @@ def get_current_user(oauth_header: Optional[str] = Security(oid)):
 
 
 @app.get("/users/me")
-def read_current_user(current_user: Optional[User] = Depends(get_current_user)):
+def read_current_user(current_user: User | None = Depends(get_current_user)):
     if current_user is None:
         return {"msg": "Create an account first"}
     return current_user
index 14f88dd931f4e1caf10b2b99e1202a2bc0e15c74..114c3c6cb2b13cf83b96c3c2b1fc8ee5aa6c951c 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from pydantic import BaseModel
@@ -9,8 +7,8 @@ app = FastAPI()
 
 class Item(BaseModel):
     name: str
-    price: Optional[float] = None
-    owner_ids: Optional[list[int]] = None
+    price: float | None = None
+    owner_ids: list[int] | None = None
 
 
 @app.get("/items/valid", response_model=Item)
index ee695368b897ae83c620dab3a9662c8fc5b54c9d..ae05f14d1ae032486114db2abadffb6068371ccd 100644 (file)
@@ -1,6 +1,5 @@
 from dataclasses import dataclass
 from datetime import datetime
-from typing import Optional
 
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
@@ -12,8 +11,8 @@ app = FastAPI()
 class Item:
     name: str
     date: datetime
-    price: Optional[float] = None
-    owner_ids: Optional[list[int]] = None
+    price: float | None = None
+    owner_ids: list[int] | None = None
 
 
 @app.get("/items/valid", response_model=Item)
index 79c90c9c2912db9766c37156e8d772ed4f41d6a9..bb05f7bc402fff0ef8af9a7fe171a0aece4e8feb 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from pydantic import BaseModel, Field
 from starlette.testclient import TestClient
@@ -9,8 +7,8 @@ app = FastAPI()
 
 class Item(BaseModel):
     name: str = Field(alias="aliased_name")
-    price: Optional[float] = None
-    owner_ids: Optional[list[int]] = None
+    price: float | None = None
+    owner_ids: list[int] | None = None
 
 
 @app.get("/items/valid", response_model=Item)
index 02765291cb7a7d6c0649ae6814b197b2a4c1d4fb..238da7392f06c86d43f7d99aba92682544e169bd 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from pydantic import BaseModel
@@ -8,23 +6,23 @@ app = FastAPI()
 
 
 class SubModel(BaseModel):
-    a: Optional[str] = "foo"
+    a: str | None = "foo"
 
 
 class Model(BaseModel):
-    x: Optional[int] = None
+    x: int | None = None
     sub: SubModel
 
 
 class ModelSubclass(Model):
     y: int
     z: int = 0
-    w: Optional[int] = None
+    w: int | None = None
 
 
 class ModelDefaults(BaseModel):
-    w: Optional[str] = None
-    x: Optional[str] = None
+    w: str | None = None
+    x: str | None = None
     y: str = "y"
     z: str = "z"
 
index 86dc4d00e27fe7d625e8e50fb1221f5c2d5d2b4d..b8a9dd2921882fd5c3b19470c70edef9594ad3f3 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 from fastapi import APIRouter, FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -10,7 +8,7 @@ app = FastAPI()
 
 class Invoice(BaseModel):
     id: str
-    title: Optional[str] = None
+    title: str | None = None
     customer: str
     total: float
 
@@ -51,7 +49,7 @@ subrouter = APIRouter()
 
 
 @subrouter.post("/invoices/", callbacks=invoices_callback_router.routes)
-def create_invoice(invoice: Invoice, callback_url: Optional[HttpUrl] = None):
+def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None):
     """
     Create an invoice.
 
index e333e2499fcf3c6bd92cfa145bb3322877c00f9e..88f9e06cc8821cff5dede6298af7a269360ec406 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional, Union
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -9,7 +7,7 @@ app = FastAPI()
 
 
 class Item(BaseModel):
-    name: Optional[str] = None
+    name: str | None = None
 
 
 class OtherItem(BaseModel):
@@ -17,7 +15,7 @@ class OtherItem(BaseModel):
 
 
 @app.post("/items/")
-def save_union_body(item: Union[OtherItem, Item]):
+def save_union_body(item: OtherItem | Item):
     return {"item": item}
 
 
index 4afe7be4b4f36c33198c778b617835ec95345de0..1b682c7751f94c3ec9e364dafc0ed263dbff6e79 100644 (file)
@@ -1,10 +1,9 @@
-from typing import Annotated, Any, Union
+from typing import Annotated, Any, Literal
 
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
 from pydantic import BaseModel, Field
-from typing_extensions import Literal
 
 
 def test_discriminator_pydantic_v2() -> None:
@@ -21,7 +20,7 @@ def test_discriminator_pydantic_v2() -> None:
         price: float
 
     Item = Annotated[
-        Union[Annotated[FirstItem, Tag("first")], Annotated[OtherItem, Tag("other")]],
+        Annotated[FirstItem, Tag("first")] | Annotated[OtherItem, Tag("other")],
         Field(discriminator="value"),
     ]
 
index 6644d106c8708ca9c24f73d80a3c1d3559e86d3b..7e64ea75b48787749da734da67b875b16c305121 100644 (file)
@@ -1,6 +1,6 @@
 # Ref: https://github.com/fastapi/fastapi/discussions/14495
 
-from typing import Annotated, Union
+from typing import Annotated
 
 import pytest
 from fastapi import FastAPI
@@ -27,7 +27,7 @@ def client_fixture() -> TestClient:
         return v.get("pet_type", "")
 
     Pet = Annotated[
-        Union[Annotated[Cat, Tag("cat")], Annotated[Dog, Tag("dog")]],
+        Annotated[Cat, Tag("cat")] | Annotated[Dog, Tag("dog")],
         Discriminator(get_pet_type),
     ]
 
index f6c2658f960afdf2b262ac8e1d9bcc2c871cb2e2..8cd7b4f01751c0d8c16536b5bba8f265752972a7 100644 (file)
@@ -1,4 +1,4 @@
-from typing import Annotated, Union
+from typing import Annotated
 
 from fastapi import FastAPI, Form
 from fastapi.testclient import TestClient
@@ -19,7 +19,7 @@ class CompanyForm(BaseModel):
 
 
 @app.post("/form-union/")
-def post_union_form(data: Annotated[Union[UserForm, CompanyForm], Form()]):
+def post_union_form(data: Annotated[UserForm | CompanyForm, Form()]):
     return {"received": data}
 
 
index 5378880a47224170507dd86415384900fb0cb02b..c997a87a35541e791f0cb708d2bbe3d9ef70b9e1 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional, Union
-
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from inline_snapshot import snapshot
@@ -9,7 +7,7 @@ app = FastAPI()
 
 
 class Item(BaseModel):
-    name: Optional[str] = None
+    name: str | None = None
 
 
 class ExtendedItem(Item):
@@ -17,7 +15,7 @@ class ExtendedItem(Item):
 
 
 @app.post("/items/")
-def save_union_different_body(item: Union[ExtendedItem, Item]):
+def save_union_different_body(item: ExtendedItem | Item):
     return {"item": item}
 
 
index 938d41956634a01e3037075c7dcf453300d3f0a7..7288220eabea4b834be29427a2cc77867b3c26ff 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional, Union
-
 import pytest
 from fastapi import FastAPI
 from fastapi.exceptions import ResponseValidationError
@@ -11,8 +9,8 @@ app = FastAPI()
 
 class Item(BaseModel):
     name: str
-    price: Optional[float] = None
-    owner_ids: Optional[list[int]] = None
+    price: float | None = None
+    owner_ids: list[int] | None = None
 
 
 @app.get("/items/invalid", response_model=Item)
@@ -25,7 +23,7 @@ def get_invalid_none():
     return None
 
 
-@app.get("/items/validnone", response_model=Union[Item, None])
+@app.get("/items/validnone", response_model=Item | None)
 def get_valid_none(send_none: bool = False):
     if send_none:
         return None
index 67282bcde1b188898b22f79529b24225ddf053f0..03b7d5f338377496483f269f953688a405d125d8 100644 (file)
@@ -1,5 +1,3 @@
-from typing import Optional
-
 import pytest
 from fastapi import FastAPI
 from fastapi.exceptions import ResponseValidationError
@@ -12,8 +10,8 @@ app = FastAPI()
 @dataclass
 class Item:
     name: str
-    price: Optional[float] = None
-    owner_ids: Optional[list[int]] = None
+    price: float | None = None
+    owner_ids: list[int] | None = None
 
 
 @app.get("/items/invalid", response_model=Item)
index 4cbfee79f5d383a180a6a8b8fd5c181239e860ea..09c4e13b0015abbf0e455c8aa12ac4cc1f2607a2 100644 (file)
@@ -2,7 +2,6 @@ import sys
 
 import pytest
 
-needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+")
 needs_py310 = pytest.mark.skipif(
     sys.version_info < (3, 10), reason="requires python3.10+"
 )