🔧 Add LLM prompt file for Turkish, generated from the existing translations

---

This will be used to update existing translations and generate new ones.

@@ -0,0 +1,52 @@
+### Target language
+
+Translate to Turkish (Türkçe).
+
+Language code: tr.
+
+### Grammar and tone
+
+1) Use instructional Turkish, consistent with existing Turkish docs.
+2) Use imperative/guide language when appropriate (e.g. “açalım”, “gidin”, “kopyalayalım”).
+
+### Headings
+
+1) Follow existing Turkish heading style ([NAME_REDACTED] where used; no trailing period).
+
+### Quotes
+
+1) Keep quote style consistent with the existing Turkish docs (they frequently use ASCII quotes in prose).
+2) Never change quotes inside inline code, code blocks, URLs, or file paths.
+
+### Ellipsis
+
+1) Keep ellipsis style consistent with existing Turkish docs.
+2) Never change `...` in code, URLs, or CLI examples.
+
+### Preferred translations / glossary
+
+Do not translate technical terms like path, route, request, response, query, body, cookie, and header, keep them as is.
+
+- Suffixing is very important, when adding Turkish suffixes to the English words, do that based on the pronunciation of the word and with an apostrophe.
+
+- Suffixes also changes based on what word comes next in Turkish too, here is an example:
+
+"Server'a gelen request'leri intercept... " or this could have been "request'e", "request'i" etc.
+
+- Some words are tricky like "path'e" can't be used like "path'a" but it could have been "path'i" "path'leri" etc.
+
+- You can use a more instructional style, that is consistent with the document, you can add the Turkish version of the term in parenthesis if it is not something very obvious, or an advanced concept, but do not over do it, do it only the first time it is mentioned, but keep the English term as the primary word.
+
+### `///` admonitions
+
+1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
+2) If a title is present, prefer these canonical titles:
+
+- `/// note | Not`
+- `/// note | [NAME_REDACTED]`
+- `/// tip | İpucu`
+- `/// warning | Uyarı`
+- `/// info | Bilgi`
+- `/// check | Ek bilgi`
+
+Prefer `İpucu` over `Ipucu`.

I think this is a great opportunity to rewrite these, like I never heard anyone using these terms IRL, but if we prefer to convert like this for other languages too, then it's completely fine!

Ah, then actually it's probably better to explicitly instruct the LLM to _not_ convert them if those are not used IRL. 🤔

What do you think?

Yeah, that would be better IMO, even I got confused with "yol operasyonu fonksiyonu" 😂

I agree, we should explicitly state this, we can add something like this to the prompt:

```txt
- Do not translate the terms like path, route, request, response, query, body, cookie, and header, keep them as it is.
- Suffixing is very important, when adding Turkish suffixes to the English words, do that based on the pronunciation of the word and with an apostrophe.
    - Suffixes also changes based on what word comes next in Turkish too, here is an example: "Server'a gelen request'leri intercept... " or this could have been "request'e", "request'i" etc.
    - Some words are tricky like "path'e" can't be used like "path'a" but it could have been "path'i" "path'leri" etc.
- You can use a more instructional style, that is consistent with the document, you can add the the Turkish version of the term in parenthesis if it is not something very obvious, or an advanced concept, but do not over do it, do it only the first time it is mentioned, but keep the English term as the primary word.
```

These are the probably most tricky and important parts, when keeping the term(s) English.

 Also [Turkish](https://news.ycombinator.com/item?id=42343920) is weird and [known](https://blog.codinghorror.com/whats-wrong-with-turkey/) to be quite [problematic](https://news.ycombinator.com/item?id=45559767) too. 😭

Perfect, thank you! I added your suggestions, replacing the old ones.

Do you think this looks good as it is?

Yeah, it looks looks good now, thank you!

I'm excited for the PR 🙂

Awesome, thank you! 🚀


🔧 Add LLM prompt file for Korean, generated from the existing translations

---

This will be used to update existing translations and generate new ones.

@@ -0,0 +1,51 @@
+### Target language
+
+Translate to Korean (한국어).
+
+Language code: ko.
+
+### Grammar and tone
+
+1) Use polite, instructional Korean (e.g. 합니다/하세요 style).
+2) Keep the tone consistent with the existing [NAME_REDACTED] docs.
+
+### Headings
+
+1) Follow existing Korean heading style (short, action-oriented headings like “확인하기”).
+2) Do not add trailing punctuation to headings.
+
+### Quotes
+
+1) Keep quote style consistent with the existing Korean docs.
+2) Never change quotes inside inline code, code blocks, URLs, or file paths.
+
+### Ellipsis
+
+1) Keep ellipsis style consistent with existing Korean docs (often `...`).
+2) Never change `...` in code, URLs, or CLI examples.
+
+### Preferred translations / glossary
+
+Use the following preferred translations when they apply in documentation prose:
+
+- request (HTTP): 요청
+- response (HTTP): 응답
+- path operation: 경로 처리
+- path operation function: 경로 처리 함수
+
+### `///` admonitions
+
+1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
+2) If a title is present, prefer these canonical titles:
+
+- `/// note | 참고`
+- `/// tip | 팁`
+- `/// warning | 경고`
+- `/// info | 정보`
+- `/// danger | 위험`
+- `/// note [NAME_REDACTED] | 기술 세부사항`
+- `/// check | 확인`
+Notes:
+
+- `details` blocks exist in Korean docs; keep `/// details` as-is and translate only the title after `|`.
+- Example canonical title used: `/// details | 상세 설명`

Looks good to me overall. I’m not deeply familiar with FastAPI docs conventions, so I’d appreciate a final review/approval from someone more familiar with the FastAPI docs style.

LGTM 🚀

```suggestion
- path operation: 경로 처리
- path operation function: 경로 처리 함수
```

"Path operation" was translated too literally as "경로 작동", which sounded awkward and made people misunderstand it as a new concept.
I think it would be better to replace it with the more general term "경로 처리"

```suggestion
- `/// note [NAME_REDACTED] | 기술 세부사항`
- `/// check | 확인`
```

ref: https://github.com/fastapi/fastapi/discussions/3167#discussioncomment-721448

```suggestion
- Example canonical title used: `/// details | 상세 설명`
```

Please remove "명령어"(It means command or instruction). It an unnecessary addition for this instruction.

Excellent, thank you! :raised_hands:

Perfect, thank you! :raised_hands:


🔧 Add LLM prompt file for Japanese, generated from the existing translations.

---

This will be used to update existing translations and generate new ones.

@@ -0,0 +1,47 @@
+### Target language
+
+Translate to Japanese (日本語).
+
+Language code: ja.
+
+### Grammar and tone
+
+1) Use polite, instructional Japanese (です/ます調).
+2) Keep the tone concise and technical (match existing [NAME_REDACTED] docs).
+
+### Headings
+
+1) Follow the existing Japanese style: short, descriptive headings (often noun phrases), e.g. 「チェック」.
+2) Do not add a trailing period at the end of headings.
+
+### Quotes
+
+1) [NAME_REDACTED] corner brackets 「」 in normal prose when quoting a term.
+2) Do not change quotes inside inline code, code blocks, URLs, or file paths.
+
+### Ellipsis
+
+1) Keep ellipsis style consistent with existing Japanese docs (commonly `...`).
+2) Never change `...` in code, URLs, or CLI examples.
+
+### Preferred translations / glossary
+
+Use the following preferred translations when they apply in documentation prose:
+
+- request (HTTP): リクエスト
+- response (HTTP): レスポンス
+- path operation: パスオペレーション
+- path operation function: パスオペレーション関数
+
+### `///` admonitions
+
+1) Keep the admonition keyword in English (do not translate `note`, `tip`, etc.).
+2) If a title is present, prefer these canonical titles:
+
+- `/// note | 備考`
+- `/// note | 技術詳細`
+- `/// tip | 豆知識`
+- `/// warning | 注意`
+- `/// info | 情報`
+- `/// check | 確認`
+- `/// danger | 警告`

Thank you.
 Everything looks good to me.
It seems to comply with the current translation rules.
I am still looking for more technical terms to add, but since I don't have any at the moment, I approve this.
Thank you again!

Thank you.
I approved this.

Sorry for commenting after approved myself.
I think it is better to change "警告"

```suggestion
- `/// danger | 警告`
```

Perfect, thanks! :raised_hands:


🔧 Add LLM prompt file for French, generated from the existing French docs

---

This will be used to update existing translations and generate new ones.

@@ -0,0 +1,128 @@
+### Target language
+
+Translate to French (français).
+
+Language code: fr.
+
+### Grammar to use when talking to the reader
+
+Use the formal grammar (use «vous» instead of «tu»).
+
+### Quotes
+
+1) Convert neutral double quotes («"») and English double typographic quotes («“» and «”») to French guillemets (««» and «»»).
+
+2) In the French docs, guillemets are written without extra spaces: use «texte», not « texte ».
+
+3) Do not convert quotes inside code blocks, inline code, paths, URLs, or anything wrapped in backticks.
+
+Examples:
+
+    Source (English):
+
+        «««
+        "Hello world"
+        “[NAME_REDACTED]”
+        "He said: 'Hello'"
+        "The module is `__main__`"
+        »»»
+
+    Result (French):
+
+        «««
+        «Hello world»
+        «[NAME_REDACTED]»
+        «He said: 'Hello'»
+        «The module is `__main__`»
+        »»»
+
+### Ellipsis
+
+1) Make sure there is a space between an ellipsis and a word following or preceding the ellipsis.
+
+Examples:
+
+    Source (English):
+
+        «««
+        ...as we intended.
+        ...this would work:
+        ...etc.
+        others...
+        More to come...
+        »»»
+
+    Result (French):
+
+        «««
+        ... comme prévu.
+        ... cela fonctionnerait :
+        ... etc.
+        D'autres ...
+        La suite ...
+        »»»
+
+2) This does not apply in URLs, code blocks, and code snippets. Do not remove or add spaces there.
+
+### Headings
+
+1) Prefer translating headings using the infinitive form (as is common in the existing French docs): «Créer…», «Utiliser…», «Ajouter…».
+
+2) For headings that are instructions written in imperative in English (e.g. “Go check …”), keep them in imperative in French, using the formal grammar (e.g. «Allez voir …»).
+
+3) Keep heading punctuation as in the source. In particular, keep occurrences of literal « - » (space-hyphen-space) as « - » (the existing French docs use a hyphen here).
+
+### French instructions about technical terms
+
+Do not try to translate everything. In particular, keep common programming terms when that is the established usage in the French docs (e.g. «framework», «endpoint», «plug-in», «payload»). Use French where the existing docs already consistently use French (e.g. «requête», «réponse»).
+
+Keep class names, function names, modules, file names, and CLI commands unchanged.
+
+### List of English terms and their preferred French translations
+
+Below is a list of English terms and their preferred French translations, separated by a colon («:»). Use these translations, do not use your own. If an existing translation does not use these terms, update it to use them.
+
+* «/// note | [NAME_REDACTED]»: «/// note | Détails techniques»
+* «/// note»: «/// note | Remarque»
+* «/// tip»: «/// tip | Astuce»
+* «/// warning»: «/// warning | Attention»
+* «/// check»: «/// check | vérifier»
+* «/// info»: «/// info»
+
+* «the docs»: «les documents»
+* «the documentation»: «la documentation»
+
+* «framework»: «framework» (do not translate to «cadre»)
+* «performance»: «performance»
+
+* «type hints»: «annotations de type»
+* «type annotations»: «annotations de type»
+
+* «autocomplete»: «autocomplétion»
+* «autocompletion»: «autocomplétion»
+
+* «the request» (what the client sends to the server): «la requête»
+* «the response» (what the server sends back to the client): «la réponse»
+
+* «the request body»: «le corps de la requête»
+* «the response body»: «le corps de la réponse»
+
+* «path operation»: «opération de chemin»
+* «path operations» (plural): «opérations de chemin»
+* «path operation function»: «fonction de chemin»
+* «path operation decorator»: «décorateur d'opération de chemin»
+
+* «path parameter»: «paramètre de chemin»
+* «query parameter»: «paramètre de requête»
+
+* «the `Request`»: «`Request`» (keep as code identifier)
+* «the `Response`»: «`Response`» (keep as code identifier)
+
+* «deployment»: «déploiement»
+* «to upgrade»: «mettre à niveau»
+
+* «deprecated»: «déprécié»
+* «to deprecate»: «déprécier»
+
+* «cheat sheet»: «aide-mémoire»
+* «plug-in»: «plug-in»

All good for me

attention

maybe `les documents` ? Not sure in which context is `the docs` but it sounds like plurals of document, not documentation. Or maybe it's for documentation`s` so in this case it will be `les documentations`

If your really want to translate it in french, it should be `autcomplétion` for both line but does it make sense to translate it ?

`le corps de la requête`

`le corps de la réponse`

`mettre à niveau`

`aide-mémoire`

So, should it be like this?

```suggestion
* «/// warning»: «/// warning | attention»
```

I wouldn't know for sure, what do you think would sound better / more natural?

Nice, thanks!

Perfect :ok_hand:

Not sure because others are capitalized so this one as well I guess but you didn't capitalized in english.  Your call. All could be capitalized or none of them

We can go with `autocomplétion`

Get it, thanks!

Perfect, thank you!


🚸  Improve tracebacks by [NAME_REDACTED]

Today, when validation errors occur, error messages don't indicate what endpoint caused the problem, which can make debugging difficult.

This PR adds endpoint context metadata to validation error messages to show the filepath (clickable in IDEs!) and line number, function name and HTTP method and route where applicable. The endpoint context is extracted once per endpoint and then cached to avoid any additional performance overhead on subsequent requests. Just to double check, I also ran a rough benchmark (100k requests):
```
Baseline (without changes):     0.0006s
Uncached:     					1.3281s (13.28µs per request)
**Cached**:       				0.0060s (0.06µs per request)
```

Endpoint context only appears in the exception's string representation (server logs). It is **not** exposed to clients in HTTP responses. Clients still receive the standard 422 response with validation error details.

## Examples

### ResponseValidationError

**Before:**
```
fastapi.exceptions.ResponseValidationError: 1 validation error:
  {'type': 'missing', 'loc': ('response', 'id'), 'msg': 'Field required', 'input': {'name': 'Widget'}}
```

**After:**
```
fastapi.exceptions.ResponseValidationError: 1 validation error:
  {'type': 'missing', 'loc': ('response', 'id'), 'msg': 'Field required', 'input': {'name': 'Widget'}}

  File "/Users/savannah/work/fastapi/capture_real_examples.py", line 43, in create_item
    POST /items/
```

### RequestValidationError

**Before:**
```
fastapi.exceptions.RequestValidationError: 1 validation error:
  {'type': 'int_parsing', 'loc': ('path', 'user_id'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'invalid'}
```

**After:**
```
fastapi.exceptions.RequestValidationError: 1 validation error:
  {'type': 'int_parsing', 'loc': ('path', 'user_id'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'invalid'}

  File "/Users/savannah/work/fastapi/capture_real_examples.py", line 38, in get_user
    GET /users/{user_id}
```

### WebSocketRequestValidationError

**Before:**
```
fastapi.exceptions.WebSocketRequestValidationError: 1 validation error:
  {'type': 'int_parsing', 'loc': ('path', 'item_id'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'invalid'}
```

**After:**
```
fastapi.exceptions.WebSocketRequestValidationError: 1 validation error:
  {'type': 'int_parsing', 'loc': ('path', 'item_id'), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'invalid'}

  File "/Users/savannah/work/fastapi/capture_real_examples.py", line 48, in websocket_endpoint
    WS /ws/{item_id}
```

@@ -1,4 +1,4 @@
-from typing import Any, Dict, Optional, Sequence, Type, Union
+from typing import Any, Dict, Optional, Sequence, Type, TypedDict, Union

 from annotated_doc import Doc
 from pydantic import BaseModel, create_model
@@ -7,6 +7,13 @@
 from typing_extensions import Annotated

+class EndpointContext(TypedDict, total=False):
+    function: str
+    path: str
+    file: str
+    line: int
+
+
 class HTTPException(StarletteHTTPException):
     """
     An HTTP exception you can raise in your own code to show errors to the client.
@@ -155,30 +162,72 @@ class DependencyScopeError(FastAPIError):

 class ValidationException(Exception):
-    def __init__(self, errors: Sequence[Any]) -> None:
+    def __init__(
+        self,
+        errors: Sequence[Any],
+        *,
+        endpoint_ctx: Optional[EndpointContext] = None,
+    ) -> None:
         self._errors = errors
+        self.endpoint_ctx = endpoint_ctx
+
+        ctx = endpoint_ctx or {}
+        self.endpoint_function = ctx.get("function")
+        self.endpoint_path = ctx.get("path")
+        self.endpoint_file = ctx.get("file")
+        self.endpoint_line = ctx.get("line")

     def errors(self) -> Sequence[Any]:
         return self._errors

+    def _format_endpoint_context(self) -> str:
+        if not (self.endpoint_file and self.endpoint_line and self.endpoint_function):
+            if self.endpoint_path:
+                return f"\n  Endpoint: {self.endpoint_path}"
+            return ""
+
+        context = f'\n  File "{self.endpoint_file}", line {self.endpoint_line}, in {self.endpoint_function}'
+        if self.endpoint_path:
+            context += f"\n    {self.endpoint_path}"
+        return context
+
+    def __str__(self) -> str:
+        message = f"{len(self._errors)} validation error{'s' if len(self._errors) != 1 else ''}:\n"
+        for err in self._errors:
+            message += f"  {err}\n"
+        message += self._format_endpoint_context()
+        return message.rstrip()
+

 class RequestValidationError(ValidationException):
-    def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
-        super().__init__(errors)
+    def __init__(
+        self,
+        errors: Sequence[Any],
+        *,
+        body: Any = None,
+        endpoint_ctx: Optional[EndpointContext] = None,
+    ) -> None:
+        super().__init__(errors, endpoint_ctx=endpoint_ctx)
         self.body = body

 class WebSocketRequestValidationError(ValidationException):
-    pass
+    def __init__(
+        self,
+        errors: Sequence[Any],
+        *,
+        endpoint_ctx: Optional[EndpointContext] = None,
+    ) -> None:
+        super().__init__(errors, endpoint_ctx=endpoint_ctx)

 class ResponseValidationError(ValidationException):
-    def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
-        super().__init__(errors)
+    def __init__(
+        self,
+        errors: Sequence[Any],
+        *,
+        body: Any = None,
+        endpoint_ctx: Optional[EndpointContext] = None,
+    ) -> None:
+        super().__init__(errors, endpoint_ctx=endpoint_ctx)
         self.body = body
-
-    def __str__(self) -> str:
-        message = f"{len(self._errors)} validation errors:\n"
-        for err in self._errors:
-            message += f"  {err}\n"
-        return message

@@ -46,6 +46,7 @@
 )
 from fastapi.encoders import jsonable_encoder
 from fastapi.exceptions import (
+    EndpointContext,
     FastAPIError,
     RequestValidationError,
     ResponseValidationError,
@@ -212,6 +213,33 @@ async def merged_lifespan(
     return merged_lifespan  # type: ignore[return-value]

+# Cache for endpoint context to avoid re-extracting on every request
+_endpoint_context_cache: Dict[int, EndpointContext] = {}
+
+
+def _extract_endpoint_context(func: Any) -> EndpointContext:
+    """Extract endpoint context with caching to avoid repeated file I/O."""
+    func_id = id(func)
+
+    if func_id in _endpoint_context_cache:
+        return _endpoint_context_cache[func_id]
+
+    try:
+        ctx: EndpointContext = {}
+
+        if (source_file := inspect.getsourcefile(func)) is not None:
+            ctx["file"] = source_file
+        if (line_number := inspect.getsourcelines(func)[1]) is not None:
+            ctx["line"] = line_number
+        if (func_name := getattr(func, "__name__", None)) is not None:
+            ctx["function"] = func_name
+    except Exception:
+        ctx = EndpointContext()
+
+    _endpoint_context_cache[func_id] = ctx
+    return ctx
+
+
 async def serialize_response(
     *,
     field: Optional[ModelField] = None,
@@ -223,6 +251,7 @@ async def serialize_response(
     exclude_defaults: bool = False,
     exclude_none: bool = False,
     is_coroutine: bool = True,
+    endpoint_ctx: Optional[EndpointContext] = None,
 ) -> Any:
     if field:
         errors = []
@@ -245,8 +274,11 @@ async def serialize_response(
         elif errors_:
             errors.append(errors_)
         if errors:
+            ctx = endpoint_ctx or EndpointContext()
             raise ResponseValidationError(
-                errors=_normalize_errors(errors), body=response_content
+                errors=_normalize_errors(errors),
+                body=response_content,
+                endpoint_ctx=ctx,
             )

         if hasattr(field, "serialize"):
@@ -318,6 +350,18 @@ async def app(request: Request) -> Response:
             "fastapi_middleware_astack not found in request scope"
         )

+        # Extract endpoint context for error messages
+        endpoint_ctx = (
+            _extract_endpoint_context(dependant.call)
+            if dependant.call
+            else EndpointContext()
+        )
+
+        if dependant.path:
+            # For mounted sub-apps, include the mount path prefix
+            mount_path = request.scope.get("root_path", "").rstrip("/")
+            endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}"
+
         # Read body and auto-close files
         try:
             body: Any = None
@@ -355,6 +399,7 @@ async def app(request: Request) -> Response:
                     }
                 ],
                 body=e.doc,
+                endpoint_ctx=endpoint_ctx,
             )
             raise validation_error from e
         except HTTPException:
@@ -414,14 +459,15 @@ async def app(request: Request) -> Response:
                     exclude_defaults=response_model_exclude_defaults,
                     exclude_none=response_model_exclude_none,
                     is_coroutine=is_coroutine,
+                    endpoint_ctx=endpoint_ctx,
                 )
                 response = actual_response_class(content, **response_args)
                 if not is_body_allowed_for_status_code(response.status_code):
                     response.body = b""
                 response.headers.raw.extend(solved_result.response.headers.raw)
         if errors:
             validation_error = RequestValidationError(
-                _normalize_errors(errors), body=body
+                _normalize_errors(errors), body=body, endpoint_ctx=endpoint_ctx
             )
             raise validation_error

@@ -438,6 +484,15 @@ def get_websocket_app(
     embed_body_fields: bool = False,
 ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:
     async def app(websocket: WebSocket) -> None:
+        endpoint_ctx = (
+            _extract_endpoint_context(dependant.call)
+            if dependant.call
+            else EndpointContext()
+        )
+        if dependant.path:
+            # For mounted sub-apps, include the mount path prefix
+            mount_path = websocket.scope.get("root_path", "").rstrip("/")
+            endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}"
         async_exit_stack = websocket.scope.get("fastapi_inner_astack")
         assert isinstance(async_exit_stack, AsyncExitStack), (
             "fastapi_inner_astack not found in request scope"
@@ -451,7 +506,8 @@ async def app(websocket: WebSocket) -> None:
         )
         if solved_result.errors:
             raise WebSocketRequestValidationError(
-                _normalize_errors(solved_result.errors)
+                _normalize_errors(solved_result.errors),
+                endpoint_ctx=endpoint_ctx,
             )
         assert dependant.call is not None, "dependant.call must be a function"
         await dependant.call(**solved_result.values)

@@ -0,0 +1,168 @@
+from fastapi import FastAPI, Request, WebSocket
+from fastapi.exceptions import (
+    RequestValidationError,
+    ResponseValidationError,
+    WebSocketRequestValidationError,
+)
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+
+class Item(BaseModel):
+    id: int
+    name: str
+
+
+class ExceptionCapture:
+    def __init__(self):
+        self.exception = None
+
+    def capture(self, exc):
+        self.exception = exc
+        return exc
+
+
+app = FastAPI()
+sub_app = FastAPI()
+captured_exception = ExceptionCapture()
+
+app.mount(path="/sub", app=sub_app)
+
+
+[USER_REDACTED].exception_handler(RequestValidationError)
+[USER_REDACTED]_app.exception_handler(RequestValidationError)
+async def request_validation_handler(request: Request, exc: RequestValidationError):
+    captured_exception.capture(exc)
+    raise exc
+
+
+[USER_REDACTED].exception_handler(ResponseValidationError)
+[USER_REDACTED]_app.exception_handler(ResponseValidationError)
+async def response_validation_handler(_: Request, exc: ResponseValidationError):
+    captured_exception.capture(exc)
+    raise exc
+
+
+[USER_REDACTED].exception_handler(WebSocketRequestValidationError)
+[USER_REDACTED]_app.exception_handler(WebSocketRequestValidationError)
+async def websocket_validation_handler(
+    websocket: WebSocket, exc: WebSocketRequestValidationError
+):
+    captured_exception.capture(exc)
+    raise exc
+
+
+[USER_REDACTED].get("/users/{user_id}")
+def get_user(user_id: int):
+    return {"user_id": user_id}  # pragma: no cover
+
+
+[USER_REDACTED].get("/items/", response_model=Item)
+def get_item():
+    return {"name": "Widget"}
+
+
+[USER_REDACTED]_app.get("/items/", response_model=Item)
+def get_sub_item():
+    return {"name": "Widget"}  # pragma: no cover
+
+
+[USER_REDACTED].websocket("/ws/{item_id}")
+async def websocket_endpoint(websocket: WebSocket, item_id: int):
+    await websocket.accept()  # pragma: no cover
+    await websocket.send_text(f"Item: {item_id}")  # pragma: no cover
+    await websocket.close()  # pragma: no cover
+
+
+[USER_REDACTED]_app.websocket("/ws/{item_id}")
+async def subapp_websocket_endpoint(websocket: WebSocket, item_id: int):
+    await websocket.accept()  # pragma: no cover
+    await websocket.send_text(f"Item: {item_id}")  # pragma: no cover
+    await websocket.close()  # pragma: no cover
+
+
+client = TestClient(app)
+
+
+def test_request_validation_error_includes_endpoint_context():
+    captured_exception.exception = None
+    try:
+        client.get("/users/invalid")
+    except Exception:
+        pass
+
+    assert captured_exception.exception is not None
+    error_str = str(captured_exception.exception)
+    assert "get_user" in error_str
+    assert "/users/" in error_str
+
+
+def test_response_validation_error_includes_endpoint_context():
+    captured_exception.exception = None
+    try:
+        client.get("/items/")
+    except Exception:
+        pass
+
+    assert captured_exception.exception is not None
+    error_str = str(captured_exception.exception)
+    assert "get_item" in error_str
+    assert "/items/" in error_str
+
+
+def test_websocket_validation_error_includes_endpoint_context():
+    captured_exception.exception = None
+    try:
+        with client.websocket_connect("/ws/invalid"):
+            pass  # pragma: no cover
+    except Exception:
+        pass
+
+    assert captured_exception.exception is not None
+    error_str = str(captured_exception.exception)
+    assert "websocket_endpoint" in error_str
+    assert "/ws/" in error_str
+
+
+def test_subapp_request_validation_error_includes_endpoint_context():
+    captured_exception.exception = None
+    try:
+        client.get("/sub/items/")
+    except Exception:
+        pass
+
+    assert captured_exception.exception is not None
+    error_str = str(captured_exception.exception)
+    assert "get_sub_item" in error_str
+    assert "/sub/items/" in error_str
+
+
+def test_subapp_websocket_validation_error_includes_endpoint_context():
+    captured_exception.exception = None
+    try:
+        with client.websocket_connect("/sub/ws/invalid"):
+            pass  # pragma: no cover
+    except Exception:
+        pass
+
+    assert captured_exception.exception is not None
+    error_str = str(captured_exception.exception)
+    assert "subapp_websocket_endpoint" in error_str
+    assert "/sub/ws/" in error_str
+
+
+def test_validation_error_with_only_path():
+    errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}]
+    exc = RequestValidationError(errors, endpoint_ctx={"path": "GET /api/test"})
+    error_str = str(exc)
+    assert "Endpoint: GET /api/test" in error_str
+    assert 'File "' not in error_str
+
+
+def test_validation_error_with_no_context():
+    errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}]
+    exc = RequestValidationError(errors, endpoint_ctx={})
+    error_str = str(exc)
+    assert "1 validation error:" in error_str
+    assert "Endpoint" not in error_str
+    assert 'File "' not in error_str

This is amazing, thank you! 🤩 🙌

It will be available in FastAPI 0.124.0, released in the next few hours. 🎉

```suggestion
            root_path = cast(str, request.scope.get("root_path", "")).rstrip("/")
            endpoint_ctx["path"] = (
                f"{request.method} {root_path}/{dependant.path.lstrip('/')}"
            )
```
Should we handle `root_path` here?

It would be helpful for mounted sub-apps.
In current implementation `path` will not contain mounting point for sub-apps and that might be confusing.

<details>

```py
from fastapi import FastAPI, Request, WebSocket
from fastapi.exceptions import (
    RequestValidationError,
    WebSocketRequestValidationError,
)
from fastapi.testclient import TestClient
from pydantic import BaseModel

class Item(BaseModel):
    id: int
    name: str

class ExceptionCapture:
    def __init__(self):
        self.exception = None

    def capture(self, exc):
        self.exception = exc
        return exc

app = FastAPI()

sub_app = FastAPI()

captured_exception = ExceptionCapture()

[USER_REDACTED].exception_handler(RequestValidationError)
[USER_REDACTED]_app.exception_handler(RequestValidationError)
async def request_validation_handler(request: Request, exc: RequestValidationError):
    captured_exception.capture(exc)
    raise exc

[USER_REDACTED].exception_handler(WebSocketRequestValidationError)
[USER_REDACTED]_app.exception_handler(WebSocketRequestValidationError)
async def websocket_validation_handler(
    websocket: WebSocket, exc: WebSocketRequestValidationError
):
    captured_exception.capture(exc)
    raise exc

[USER_REDACTED]_app.get("/users/{user_id}")
def get_user_router(user_id: int):
    return {"user_id": user_id}  # pragma: no cover

app.mount(path="/sub", app=sub_app)

client = TestClient(app, raise_server_exceptions=False)

def test_subapp_request_validation_error_includes_endpoint_context():
    captured_exception.exception = None
    try:
        client.get("/sub/users/invalid")
    except Exception:
        pass

    assert captured_exception.exception is not None
    error_str = str(captured_exception.exception)
    assert "get_user" in error_str
    assert "/sub/users/" in error_str  # <= Currently fails
```

</details>

TIL about sub apps 🤓 ! Thanks!


✨ Show a clear error on attempt to include router into itself

Previously discussed in https://github.com/fastapi/fastapi/discussions/11424 with the PR at https://github.com/fastapi/fastapi/pull/11425

Since I made modifications in the fork of the repo, the PR was automatically closed. This PR serves to add the circular reference check again. Discussed again in https://github.com/fastapi/fastapi/discussions/14251

@@ -1393,6 +1393,10 @@ def read_users():
         app.include_router(internal_router)
         ```
         """
+        assert self is not router, (
+            "Cannot include the same APIRouter instance into itself. "
+            "Did you mean to include a different router?"
+        )
         if prefix:
             assert prefix.startswith("/"), "A path prefix must start with '/'"
             assert not prefix.endswith("/"), (

@@ -0,0 +1,12 @@
+import pytest
+from fastapi import APIRouter
+
+
+def test_router_circular_import():
+    router = APIRouter()
+
+    with pytest.raises(
+        AssertionError,
+        match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?",
+    ):
+        router.include_router(router)

LGTM!

[USER_REDACTED], thank you!

Makes sense, thank you! This will be available in FastAPI 0.128.7, released in the next few hours. 🚀

```suggestion
import pytest
from fastapi import APIRouter

def test_router_circular_import():
    router = APIRouter()

    with pytest.raises(AssertionError, match="Cannot include router into itself"):
        router.include_router(router)
```

`app` is not needed here, and calling `router.include_router` after `app.include_router(router)` wrong

```suggestion
        assert self is not router, (
            "Cannot include the same APIRouter instance into itself. "
            "Did you mean to include a different router?"
        )
```
How about this version?

Also, we should probably raise [`FastAPIError`](https://github.com/fastapi/fastapi/blob/1fc586c3a5ae457457c5c70adddb07c7e9185f16/fastapi/exceptions.py#L144) instead of `assert`.

UPD: maybe not. In other cases just `assert` is used..


🐛 Fix internal Pydantic v1 compatibility (warnings) for Python 3.14 and Pydantic 2.12.1

The Python 3.14 tests started failing with the Pydantic v.2.12.1 release, because of the combination of https://github.com/fastapi/fastapi/pull/14168 and https://github.com/[USER_REDACTED]ntic/pydantic/pull/12367.

Basically we were importing Pydantic `v1` functionality even when the user had (only) Pydantic v2 installed. This was done in https://github.com/fastapi/fastapi/pull/14168 to support more easy migration from v1 to v2 by [NAME_REDACTED] the same app.

Here, we prevent importing any `v1` functionality by [NAME_REDACTED] Python 3.14 / Pydantic 2.12.0 onwards, which [drops support](https://github.com/[USER_REDACTED]ntic/pydantic/releases/tag/v2.12.0) for `v1` and emits a [warning](https://github.com/[USER_REDACTED]ntic/pydantic/pull/12367) when importing `v1` functionality.

### TODO

- [x] fix coverage
- [ ] check warning is still shown to users at some point, and not just swallowed by [NAME_REDACTED]

@@ -30,6 +30,10 @@
 from .main import (
     with_info_plain_validator_function as with_info_plain_validator_function,
 )
+from .may_v1 import CoreSchema as CoreSchema
+from .may_v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
+from .may_v1 import JsonSchemaValue as JsonSchemaValue
+from .may_v1 import _normalize_errors as _normalize_errors
 from .model_field import ModelField as ModelField
 from .shared import PYDANTIC_V2 as PYDANTIC_V2
 from .shared import PYDANTIC_VERSION_MINOR_TUPLE as PYDANTIC_VERSION_MINOR_TUPLE
@@ -44,7 +48,3 @@
 from .shared import lenient_issubclass as lenient_issubclass
 from .shared import sequence_types as sequence_types
 from .shared import value_is_sequence as value_is_sequence
-from .v1 import CoreSchema as CoreSchema
-from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
-from .v1 import JsonSchemaValue as JsonSchemaValue
-from .v1 import _normalize_errors as _normalize_errors

@@ -1,3 +1,4 @@
+import sys
 from functools import lru_cache
 from typing import (
     Any,
@@ -8,7 +9,7 @@
     Type,
 )

-from fastapi._compat import v1
+from fastapi._compat import may_v1
 from fastapi._compat.shared import PYDANTIC_V2, lenient_issubclass
 from fastapi.types import ModelNameMap
 from pydantic import BaseModel
@@ -50,7 +51,9 @@

 [USER_REDACTED]_cache
 def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:
-    if lenient_issubclass(model, v1.BaseModel):
+    if lenient_issubclass(model, may_v1.BaseModel):
+        from fastapi._compat import v1
+
         return v1.get_model_fields(model)
     else:
         from . import v2
@@ -59,7 +62,7 @@ def get_cached_model_fields(model: Type[BaseModel]) -> List[ModelField]:

 def _is_undefined(value: object) -> bool:
-    if isinstance(value, v1.UndefinedType):
+    if isinstance(value, may_v1.UndefinedType):
         return True
     elif PYDANTIC_V2:
         from . import v2
@@ -69,7 +72,9 @@ def _is_undefined(value: object) -> bool:

 def _get_model_config(model: BaseModel) -> Any:
-    if isinstance(model, v1.BaseModel):
+    if isinstance(model, may_v1.BaseModel):
+        from fastapi._compat import v1
+
         return v1._get_model_config(model)
     elif PYDANTIC_V2:
         from . import v2
@@ -80,7 +85,9 @@ def _get_model_config(model: BaseModel) -> Any:
 def _model_dump(
     model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
 ) -> Any:
-    if isinstance(model, v1.BaseModel):
+    if isinstance(model, may_v1.BaseModel):
+        from fastapi._compat import v1
+
         return v1._model_dump(model, mode=mode, **kwargs)
     elif PYDANTIC_V2:
         from . import v2
@@ -89,7 +96,7 @@ def _model_dump(

 def _is_error_wrapper(exc: Exception) -> bool:
-    if isinstance(exc, v1.ErrorWrapper):
+    if isinstance(exc, may_v1.ErrorWrapper):
         return True
     elif PYDANTIC_V2:
         from . import v2
@@ -99,7 +106,9 @@ def _is_error_wrapper(exc: Exception) -> bool:

 def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
-    if isinstance(field_info, v1.FieldInfo):
+    if isinstance(field_info, may_v1.FieldInfo):
+        from fastapi._compat import v1
+
         return v1.copy_field_info(field_info=field_info, annotation=annotation)
     else:
         assert PYDANTIC_V2
@@ -111,7 +120,9 @@ def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
 def create_body_model(
     *, fields: Sequence[ModelField], model_name: str
 ) -> Type[BaseModel]:
-    if fields and isinstance(fields[0], v1.ModelField):
+    if fields and isinstance(fields[0], may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.create_body_model(fields=fields, model_name=model_name)
     else:
         assert PYDANTIC_V2
@@ -123,7 +134,9 @@ def create_body_model(
 def get_annotation_from_field_info(
     annotation: Any, field_info: FieldInfo, field_name: str
 ) -> Any:
-    if isinstance(field_info, v1.FieldInfo):
+    if isinstance(field_info, may_v1.FieldInfo):
+        from fastapi._compat import v1
+
         return v1.get_annotation_from_field_info(
             annotation=annotation, field_info=field_info, field_name=field_name
         )
@@ -137,7 +150,9 @@ def get_annotation_from_field_info(

 def is_bytes_field(field: ModelField) -> bool:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.is_bytes_field(field)
     else:
         assert PYDANTIC_V2
@@ -147,7 +162,9 @@ def is_bytes_field(field: ModelField) -> bool:

 def is_bytes_sequence_field(field: ModelField) -> bool:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.is_bytes_sequence_field(field)
     else:
         assert PYDANTIC_V2
@@ -157,7 +174,9 @@ def is_bytes_sequence_field(field: ModelField) -> bool:

 def is_scalar_field(field: ModelField) -> bool:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.is_scalar_field(field)
     else:
         assert PYDANTIC_V2
@@ -167,7 +186,9 @@ def is_scalar_field(field: ModelField) -> bool:

 def is_scalar_sequence_field(field: ModelField) -> bool:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.is_scalar_sequence_field(field)
     else:
         assert PYDANTIC_V2
@@ -177,7 +198,9 @@ def is_scalar_sequence_field(field: ModelField) -> bool:

 def is_sequence_field(field: ModelField) -> bool:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.is_sequence_field(field)
     else:
         assert PYDANTIC_V2
@@ -187,7 +210,9 @@ def is_sequence_field(field: ModelField) -> bool:

 def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.serialize_sequence_value(field=field, value=value)
     else:
         assert PYDANTIC_V2
@@ -197,7 +222,9 @@ def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:

 def _model_rebuild(model: Type[BaseModel]) -> None:
-    if lenient_issubclass(model, v1.BaseModel):
+    if lenient_issubclass(model, may_v1.BaseModel):
+        from fastapi._compat import v1
+
         v1._model_rebuild(model)
     elif PYDANTIC_V2:
         from . import v2
@@ -206,9 +233,18 @@ def _model_rebuild(model: Type[BaseModel]) -> None:

 def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
-    v1_model_fields = [field for field in fields if isinstance(field, v1.ModelField)]
-    v1_flat_models = v1.get_flat_models_from_fields(v1_model_fields, known_models=set())  # type: ignore[attr-defined]
-    all_flat_models = v1_flat_models
+    v1_model_fields = [
+        field for field in fields if isinstance(field, may_v1.ModelField)
+    ]
+    if v1_model_fields:
+        from fastapi._compat import v1
+
+        v1_flat_models = v1.get_flat_models_from_fields(
+            v1_model_fields, known_models=set()
+        )
+        all_flat_models = v1_flat_models
+    else:
+        all_flat_models = set()
     if PYDANTIC_V2:
         from . import v2

@@ -222,6 +258,8 @@ def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:

         model_name_map = v2.get_model_name_map(all_flat_models)
         return model_name_map
+    from fastapi._compat import v1
+
     model_name_map = v1.get_model_name_map(all_flat_models)
     return model_name_map

@@ -232,17 +270,35 @@ def get_definitions(
     model_name_map: ModelNameMap,
     separate_input_output_schemas: bool = True,
 ) -> Tuple[
-    Dict[Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue],
+    Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]],
+        may_v1.JsonSchemaValue,
+    ],
     Dict[str, Dict[str, Any]],
 ]:
-    v1_fields = [field for field in fields if isinstance(field, v1.ModelField)]
-    v1_field_maps, v1_definitions = v1.get_definitions(
-        fields=v1_fields,
-        model_name_map=model_name_map,
-        separate_input_output_schemas=separate_input_output_schemas,
-    )
-    if not PYDANTIC_V2:
-        return v1_field_maps, v1_definitions
+    if sys.version_info < (3, 14):
+        v1_fields = [field for field in fields if isinstance(field, may_v1.ModelField)]
+        v1_field_maps, v1_definitions = may_v1.get_definitions(
+            fields=v1_fields,
+            model_name_map=model_name_map,
+            separate_input_output_schemas=separate_input_output_schemas,
+        )
+        if not PYDANTIC_V2:
+            return v1_field_maps, v1_definitions
+        else:
+            from . import v2
+
+            v2_fields = [field for field in fields if isinstance(field, v2.ModelField)]
+            v2_field_maps, v2_definitions = v2.get_definitions(
+                fields=v2_fields,
+                model_name_map=model_name_map,
+                separate_input_output_schemas=separate_input_output_schemas,
+            )
+            all_definitions = {**v1_definitions, **v2_definitions}
+            all_field_maps = {**v1_field_maps, **v2_field_maps}
+            return all_field_maps, all_definitions
+
+    # Pydantic v1 is not supported since Python 3.14
     else:
         from . import v2

@@ -252,21 +308,22 @@ def get_definitions(
             model_name_map=model_name_map,
             separate_input_output_schemas=separate_input_output_schemas,
         )
-        all_definitions = {**v1_definitions, **v2_definitions}
-        all_field_maps = {**v1_field_maps, **v2_field_maps}
-        return all_field_maps, all_definitions
+        return v2_field_maps, v2_definitions

 def get_schema_from_model_field(
     *,
     field: ModelField,
     model_name_map: ModelNameMap,
     field_mapping: Dict[
-        Tuple[ModelField, Literal["validation", "serialization"]], v1.JsonSchemaValue
+        Tuple[ModelField, Literal["validation", "serialization"]],
+        may_v1.JsonSchemaValue,
     ],
     separate_input_output_schemas: bool = True,
 ) -> Dict[str, Any]:
-    if isinstance(field, v1.ModelField):
+    if isinstance(field, may_v1.ModelField):
+        from fastapi._compat import v1
+
         return v1.get_schema_from_model_field(
             field=field,
             model_name_map=model_name_map,
@@ -286,7 +343,7 @@ def get_schema_from_model_field(

 def _is_model_field(value: Any) -> bool:
-    if isinstance(value, v1.ModelField):
+    if isinstance(value, may_v1.ModelField):
         return True
     elif PYDANTIC_V2:
         from . import v2
@@ -296,7 +353,7 @@ def _is_model_field(value: Any) -> bool:

 def _is_model_class(value: Any) -> bool:
-    if lenient_issubclass(value, v1.BaseModel):
+    if lenient_issubclass(value, may_v1.BaseModel):
         return True
     elif PYDANTIC_V2:
         from . import v2

@@ -0,0 +1,123 @@
+import sys
+from typing import Any, Dict, List, Literal, Sequence, Tuple, Type, Union
+
+from fastapi.types import ModelNameMap
+
+if sys.version_info >= (3, 14):
+
+    class AnyUrl:
+        pass
+
+    class BaseConfig:
+        pass
+
+    class BaseModel:
+        pass
+
+    class Color:
+        pass
+
+    class CoreSchema:
+        pass
+
+    class ErrorWrapper:
+        pass
+
+    class FieldInfo:
+        pass
+
+    class GetJsonSchemaHandler:
+        pass
+
+    class JsonSchemaValue:
+        pass
+
+    class ModelField:
+        pass
+
+    class NameEmail:
+        pass
+
+    class RequiredParam:
+        pass
+
+    class SecretBytes:
+        pass
+
+    class SecretStr:
+        pass
+
+    class Undefined:
+        pass
+
+    class UndefinedType:
+        pass
+
+    class Url:
+        pass
+
+    from .v2 import ValidationError, create_model
+
+    def get_definitions(
+        *,
+        fields: List[ModelField],
+        model_name_map: ModelNameMap,
+        separate_input_output_schemas: bool = True,
+    ) -> Tuple[
+        Dict[
+            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+        ],
+        Dict[str, Dict[str, Any]],
+    ]:
+        return {}, {}  # pragma: no cover
+
+
+else:
+    from .v1 import AnyUrl as AnyUrl
+    from .v1 import BaseConfig as BaseConfig
+    from .v1 import BaseModel as BaseModel
+    from .v1 import Color as Color
+    from .v1 import CoreSchema as CoreSchema
+    from .v1 import ErrorWrapper as ErrorWrapper
+    from .v1 import FieldInfo as FieldInfo
+    from .v1 import GetJsonSchemaHandler as GetJsonSchemaHandler
+    from .v1 import JsonSchemaValue as JsonSchemaValue
+    from .v1 import ModelField as ModelField
+    from .v1 import NameEmail as NameEmail
+    from .v1 import RequiredParam as RequiredParam
+    from .v1 import SecretBytes as SecretBytes
+    from .v1 import SecretStr as SecretStr
+    from .v1 import Undefined as Undefined
+    from .v1 import UndefinedType as UndefinedType
+    from .v1 import Url as Url
+    from .v1 import ValidationError, create_model
+    from .v1 import get_definitions as get_definitions
+
+
+RequestErrorModel: Type[BaseModel] = create_model("Request")
+
+
+def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
+    use_errors: List[Any] = []
+    for error in errors:
+        if isinstance(error, ErrorWrapper):
+            new_errors = ValidationError(  # type: ignore[call-arg]
+                errors=[error], model=RequestErrorModel
+            ).errors()
+            use_errors.extend(new_errors)
+        elif isinstance(error, list):
+            use_errors.extend(_normalize_errors(error))
+        else:
+            use_errors.append(error)
+    return use_errors
+
+
+def _regenerate_error_with_loc(
+    *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
+) -> List[Dict[str, Any]]:
+    updated_loc_errors: List[Any] = [
+        {**err, "loc": loc_prefix + err.get("loc", ())}
+        for err in _normalize_errors(errors)
+    ]
+
+    return updated_loc_errors

@@ -16,7 +16,7 @@
     Union,
 )

-from fastapi._compat import v1
+from fastapi._compat import may_v1
 from fastapi.types import UnionType
 from pydantic import BaseModel
 from pydantic.version import VERSION as PYDANTIC_VERSION
@@ -98,7 +98,9 @@ def value_is_sequence(value: Any) -> bool:

 def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool:
     return (
-        lenient_issubclass(annotation, (BaseModel, v1.BaseModel, Mapping, UploadFile))
+        lenient_issubclass(
+            annotation, (BaseModel, may_v1.BaseModel, Mapping, UploadFile)
+        )
         or _annotation_is_sequence(annotation)
         or is_dataclass(annotation)
     )
@@ -195,12 +197,12 @@ def is_uploadfile_sequence_annotation(annotation: Any) -> bool:

 def annotation_is_pydantic_v1(annotation: Any) -> bool:
-    if lenient_issubclass(annotation, v1.BaseModel):
+    if lenient_issubclass(annotation, may_v1.BaseModel):
         return True
     origin = get_origin(annotation)
     if origin is Union or origin is UnionType:
         for arg in get_args(annotation):
-            if lenient_issubclass(arg, v1.BaseModel):
+            if lenient_issubclass(arg, may_v1.BaseModel):
                 return True
     if field_annotation_is_sequence(annotation):
         for sub_annotation in get_args(annotation):

@@ -54,13 +54,15 @@
     from pydantic.schema import TypeModelSet as TypeModelSet
     from pydantic.schema import (
         field_schema,
-        get_flat_models_from_fields,
         model_process_schema,
     )
     from pydantic.schema import (
         get_annotation_from_field_info as get_annotation_from_field_info,
     )
     from pydantic.schema import get_flat_models_from_field as get_flat_models_from_field
+    from pydantic.schema import (
+        get_flat_models_from_fields as get_flat_models_from_fields,
+    )
     from pydantic.schema import get_model_name_map as get_model_name_map
     from pydantic.types import SecretBytes as SecretBytes
     from pydantic.types import SecretStr as SecretStr
@@ -99,7 +101,6 @@
     from pydantic.v1.schema import TypeModelSet as TypeModelSet
     from pydantic.v1.schema import (
         field_schema,
-        get_flat_models_from_fields,
         model_process_schema,
     )
     from pydantic.v1.schema import (
@@ -108,6 +109,9 @@
     from pydantic.v1.schema import (
         get_flat_models_from_field as get_flat_models_from_field,
     )
+    from pydantic.v1.schema import (
+        get_flat_models_from_fields as get_flat_models_from_fields,
+    )
     from pydantic.v1.schema import get_model_name_map as get_model_name_map
     from pydantic.v1.types import (  # type: ignore[assignment]
         SecretBytes as SecretBytes,
@@ -215,32 +219,6 @@ def is_pv1_scalar_sequence_field(field: ModelField) -> bool:
     return False

-def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]:
-    use_errors: List[Any] = []
-    for error in errors:
-        if isinstance(error, ErrorWrapper):
-            new_errors = ValidationError(  # type: ignore[call-arg]
-                errors=[error], model=RequestErrorModel
-            ).errors()
-            use_errors.extend(new_errors)
-        elif isinstance(error, list):
-            use_errors.extend(_normalize_errors(error))
-        else:
-            use_errors.append(error)
-    return use_errors
-
-
-def _regenerate_error_with_loc(
-    *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
-) -> List[Dict[str, Any]]:
-    updated_loc_errors: List[Any] = [
-        {**err, "loc": loc_prefix + err.get("loc", ())}
-        for err in _normalize_errors(errors)
-    ]
-
-    return updated_loc_errors
-
-
 def _model_rebuild(model: Type[BaseModel]) -> None:
     model.update_forward_refs()

@@ -15,7 +15,7 @@
     cast,
 )

-from fastapi._compat import shared, v1
+from fastapi._compat import may_v1, shared
 from fastapi.openapi.constants import REF_TEMPLATE
 from fastapi.types import IncEx, ModelNameMap
 from pydantic import BaseModel, TypeAdapter, create_model
@@ -116,7 +116,7 @@ def validate(
                 None,
             )
         except ValidationError as exc:
-            return None, v1._regenerate_error_with_loc(
+            return None, may_v1._regenerate_error_with_loc(
                 errors=exc.errors(include_url=False), loc_prefix=loc
             )

@@ -43,9 +43,9 @@
     is_uploadfile_or_nonable_uploadfile_annotation,
     is_uploadfile_sequence_annotation,
     lenient_issubclass,
+    may_v1,
     sequence_types,
     serialize_sequence_value,
-    v1,
     value_is_sequence,
 )
 from fastapi._compat.shared import annotation_is_pydantic_v1
@@ -380,7 +380,7 @@ def analyze_param(
         fastapi_annotations = [
             arg
             for arg in annotated_args[1:]
-            if isinstance(arg, (FieldInfo, v1.FieldInfo, params.Depends))
+            if isinstance(arg, (FieldInfo, may_v1.FieldInfo, params.Depends))
         ]
         fastapi_specific_annotations = [
             arg
@@ -397,21 +397,21 @@ def analyze_param(
             )
         ]
         if fastapi_specific_annotations:
-            fastapi_annotation: Union[FieldInfo, v1.FieldInfo, params.Depends, None] = (
-                fastapi_specific_annotations[-1]
-            )
+            fastapi_annotation: Union[
+                FieldInfo, may_v1.FieldInfo, params.Depends, None
+            ] = fastapi_specific_annotations[-1]
         else:
             fastapi_annotation = None
         # Set default for [NAME_REDACTED]
-        if isinstance(fastapi_annotation, (FieldInfo, v1.FieldInfo)):
+        if isinstance(fastapi_annotation, (FieldInfo, may_v1.FieldInfo)):
             # Copy `field_info` because we mutate `field_info.default` below.
             field_info = copy_field_info(
                 field_info=fastapi_annotation, annotation=use_annotation
             )
             assert field_info.default in {
                 Undefined,
-                v1.Undefined,
-            } or field_info.default in {RequiredParam, v1.RequiredParam}, (
+                may_v1.Undefined,
+            } or field_info.default in {RequiredParam, may_v1.RequiredParam}, (
                 f"`{field_info.__class__.__name__}` default value cannot be set in"
                 f" `Annotated` for {param_name!r}. Set the default value with `=` instead."
             )
@@ -435,7 +435,7 @@ def analyze_param(
         )
         depends = value
     # [NAME_REDACTED] from default value
-    elif isinstance(value, (FieldInfo, v1.FieldInfo)):
+    elif isinstance(value, (FieldInfo, may_v1.FieldInfo)):
         assert field_info is None, (
             "Cannot specify FastAPI annotations in `Annotated` and default value"
             f" together for {param_name!r}"
@@ -524,7 +524,8 @@ def analyze_param(
             type_=use_annotation_from_field_info,
             default=field_info.default,
             alias=alias,
-            required=field_info.default in (RequiredParam, v1.RequiredParam, Undefined),
+            required=field_info.default
+            in (RequiredParam, may_v1.RequiredParam, Undefined),
             field_info=field_info,
         )
         if is_path_param:
@@ -741,7 +742,7 @@ def _validate_value_with_model_field(
     if _is_error_wrapper(errors_):  # type: ignore[arg-type]
         return None, [errors_]
     elif isinstance(errors_, list):
-        new_errors = v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
+        new_errors = may_v1._regenerate_error_with_loc(errors=errors_, loc_prefix=())
         return None, new_errors
     else:
         return v_, []

@@ -17,7 +17,7 @@
 from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
 from uuid import UUID

-from fastapi._compat import v1
+from fastapi._compat import may_v1
 from fastapi.types import IncEx
 from pydantic import BaseModel
 from pydantic.color import Color
@@ -59,7 +59,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
 ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
     bytes: lambda o: o.decode(),
     Color: str,
-    v1.Color: str,
+    may_v1.Color: str,
     datetime.date: isoformat,
     datetime.datetime: isoformat,
     datetime.time: isoformat,
@@ -76,19 +76,19 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]:
     IPv6Interface: str,
     IPv6Network: str,
     NameEmail: str,
-    v1.NameEmail: str,
+    may_v1.NameEmail: str,
     Path: str,
     Pattern: lambda o: o.pattern,
     SecretBytes: str,
-    v1.SecretBytes: str,
+    may_v1.SecretBytes: str,
     SecretStr: str,
-    v1.SecretStr: str,
+    may_v1.SecretStr: str,
     set: list,
     UUID: str,
     Url: str,
-    v1.Url: str,
+    may_v1.Url: str,
     AnyUrl: str,
-    v1.AnyUrl: str,
+    may_v1.AnyUrl: str,
 }

@@ -220,10 +220,10 @@ def jsonable_encoder(
         include = set(include)
     if exclude is not None and not isinstance(exclude, (set, dict)):
         exclude = set(exclude)
-    if isinstance(obj, (BaseModel, v1.BaseModel)):
+    if isinstance(obj, (BaseModel, may_v1.BaseModel)):
         # TODO: remove when deprecating Pydantic v1
         encoders: Dict[Any, Any] = {}
-        if isinstance(obj, v1.BaseModel):
+        if isinstance(obj, may_v1.BaseModel):
             encoders = getattr(obj.__config__, "json_encoders", {})  # type: ignore[attr-defined]
             if custom_encoder:
                 encoders = {**encoders, **custom_encoder}

@@ -5,8 +5,8 @@
 from fastapi.params import ParamTypes
 from typing_extensions import Annotated, deprecated

+from ._compat.may_v1 import FieldInfo, Undefined
 from ._compat.shared import PYDANTIC_VERSION_MINOR_TUPLE
-from ._compat.v1 import FieldInfo, Undefined

 _Unset: Any = Undefined

@@ -25,7 +25,7 @@
     Validator,
     annotation_is_pydantic_v1,
     lenient_issubclass,
-    v1,
+    may_v1,
 )
 from fastapi.datastructures import DefaultPlaceholder, DefaultType
 from pydantic import BaseModel
@@ -87,8 +87,8 @@ def create_model_field(
 ) -> ModelField:
     class_validators = class_validators or {}

-    v1_model_config = v1.BaseConfig
-    v1_field_info = field_info or v1.FieldInfo()
+    v1_model_config = may_v1.BaseConfig
+    v1_field_info = field_info or may_v1.FieldInfo()
     v1_kwargs = {
         "name": name,
         "field_info": v1_field_info,
@@ -102,9 +102,11 @@ def create_model_field(

     if (
         annotation_is_pydantic_v1(type_)
-        or isinstance(field_info, v1.FieldInfo)
+        or isinstance(field_info, may_v1.FieldInfo)
         or version == "1"
     ):
+        from fastapi._compat import v1
+
         try:
             return v1.ModelField(**v1_kwargs)  # type: ignore[no-any-return]
         except RuntimeError:
@@ -122,6 +124,8 @@ def create_model_field(
             raise fastapi.exceptions.FastAPIError(_invalid_args_message) from None
     # Pydantic v2 is not installed, but it's not a Pydantic v1 ModelField, it could be
     # a Pydantic v1 type, like a constrained int
+    from fastapi._compat import v1
+
     try:
         return v1.ModelField(**v1_kwargs)  # type: ignore[no-any-return]
     except RuntimeError:
@@ -138,6 +142,9 @@ def create_cloned_field(

         if isinstance(field, v2.ModelField):
             return field
+
+    from fastapi._compat import v1
+
     # cloned_types caches already cloned types to support recursive models and improve
     # performance by [NAME_REDACTED] cloned_types is None:

@@ -7,7 +7,7 @@
     get_cached_model_fields,
     is_scalar_field,
     is_uploadfile_sequence_annotation,
-    v1,
+    may_v1,
 )
 from fastapi._compat.shared import is_bytes_sequence_annotation
 from fastapi.testclient import TestClient
@@ -27,7 +27,10 @@ def test_model_field_default_required():
     assert field.default is Undefined

+[USER_REDACTED]_py_lt_314
 def test_v1_plain_validator_function():
+    from fastapi._compat import v1
+
     # For coverage
     def func(v):  # pragma: no cover
         return v
@@ -135,6 +138,8 @@ def test_is_uploadfile_sequence_annotation():

 [USER_REDACTED]_py_lt_314
 def test_is_pv1_scalar_field():
+    from fastapi._compat import v1
+
     # For coverage
     class Model(v1.BaseModel):
         foo: Union[str, Dict[str, Any]]
@@ -143,8 +148,11 @@ class Model(v1.BaseModel):
     assert not is_scalar_field(fields[0])

+[USER_REDACTED]_py_lt_314
 def test_get_model_fields_cached():
-    class Model(v1.BaseModel):
+    from fastapi._compat import v1
+
+    class Model(may_v1.BaseModel):
         foo: str

     non_cached_fields = v1.get_model_fields(Model)

@@ -5,7 +5,6 @@
 import pydantic.schema
 import pytest
 from fastapi import FastAPI
-from fastapi._compat import v1
 from pydantic import BaseModel
 from starlette.testclient import TestClient

@@ -165,6 +164,8 @@ def test_model_description_escaped_with_formfeed(sort_reversed: bool):

     Test `get_model_definitions` with models passed in different order.
     """
+    from fastapi._compat import v1
+
     all_fields = fastapi.openapi.utils.get_fields_from_routes(app.routes)

     flat_models = v1.get_flat_models_from_fields(all_fields, known_models=set())

@@ -2,12 +2,13 @@

 import pytest
 from fastapi import FastAPI
-from fastapi._compat import v1
 from fastapi.exceptions import FastAPIError, ResponseValidationError
 from fastapi.responses import JSONResponse, Response
 from fastapi.testclient import TestClient
 from pydantic import BaseModel

+from tests.utils import needs_pydanticv1
+

 class BaseUser(BaseModel):
     name: str
@@ -511,7 +512,10 @@ def read_root() -> Union[Response, None]:

 # TODO: remove when dropping Pydantic v1 support
+[USER_REDACTED]_pydanticv1
 def test_invalid_response_model_field_pv1():
+    from fastapi._compat import v1
+
     app = FastAPI()

     class Model(v1.BaseModel):

Awesome, thank you [USER_REDACTED]! 🙌

I checked and confirmed that on Python 3.14, if users try to use Pydantic v1 they still get a warning on their own code, but not on older versions. So we're good to go! 🚀

Probably better to check directly for Pydantic 2.12.0+ ?

Then again, Pydantic themselves say

> The core functionality of Pydantic V1 is not compatible with Python 3.14 or greater.


⬆ Add support for Python 3.14

Python 3.14.0 is supposed to be released on October 7th, 2 weeks from now.

We'll need to merge this PR with https://github.com/fastapi/fastapi/pull/14036

Note: probably requires https://github.com/fastapi/sqlmodel/pull/1578 first

@@ -48,13 +48,17 @@ jobs:
     strategy:
       matrix:
         python-version:
+          - "3.14"
           - "3.13"
           - "3.12"
           - "3.11"
           - "3.10"
           - "3.9"
           - "3.8"
         pydantic-version: ["pydantic-v1", "pydantic-v2"]
+        exclude:
+          - python-version: "3.14"
+            pydantic-version: "pydantic-v1"
       fail-fast: false
     steps:
       - name: [NAME_REDACTED] context
@@ -66,6 +70,7 @@ jobs:
         uses: actions/setup-python@v6
         with:
           python-version: ${{ matrix.python-version }}
+          allow-prereleases: true
       - name: Setup uv
         uses: astral-sh/setup-uv@v6
         with:
@@ -74,13 +79,16 @@ jobs:
           cache-dependency-glob: |
             requirements**.txt
             pyproject.toml
+      - name: [NAME_REDACTED] 2.12.0a1 for Python 3.14
+        if: matrix.python-version == '3.14'
+        run: uv pip install --pre "pydantic==2.12.0a1"
       - name: [NAME_REDACTED]
         run: uv pip install -r requirements-tests.txt
       - name: [NAME_REDACTED] v1
         if: matrix.pydantic-version == 'pydantic-v1'
         run: uv pip install "pydantic>=1.10.0,<2.0.0"
       - name: [NAME_REDACTED] v2
-        if: matrix.pydantic-version == 'pydantic-v2'
+        if: matrix.pydantic-version == 'pydantic-v2' && matrix.python-version != '3.14'
         run: uv pip install --upgrade "pydantic>=2.0.2,<3.0.0"
       # TODO: Remove this once Python 3.8 is no longer supported
       - name: Install older AnyIO in Python 3.8

@@ -39,6 +39,7 @@ classifiers = [
     "[NAME_REDACTED] :: Python :: 3.11",
     "[NAME_REDACTED] :: Python :: 3.12",
     "[NAME_REDACTED] :: Python :: 3.13",
+    "[NAME_REDACTED] :: Python :: 3.14",
     "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
     "Topic :: Internet :: WWW/HTTP",
 ]

This can be removed when a stable Pydantic 2.12 release is available.

This can be removed when Python 3.14 is officially available.


⬆ [NAME_REDACTED] supported version range to >=0.40.0,<0.49.0

Starlette 0.48.0 introduced `HTTP_422_UNPROCESSABLE_CONTENT` as a new name for `HTTP_422_UNPROCESSABLE_ENTITY` and deprecated the old name due to RFC 9110; see https://github.com/Kludex/starlette/pull/2939.

I updated all instances ~to the new name and increased the lower bound on the Starlette version~ to use the literal integer or string value `422`/`"422"` instead of either `HTTP_422_UNPROCESSABLE_CONTENT` or `HTTP_422_UNPROCESSABLE_ENTITY`, which makes FastAPI compatible with Starlette >=0.48 without deprecation warnings while preserving backwards compatibility with Starlette<0.48.

[USER_REDACTED], FYI.

@@ -1,4 +1,4 @@
-from fastapi import FastAPI, Request, status
+from fastapi import FastAPI, Request
 from fastapi.encoders import jsonable_encoder
 from fastapi.exceptions import RequestValidationError
 from fastapi.responses import JSONResponse
@@ -10,7 +10,7 @@
 [USER_REDACTED].exception_handler(RequestValidationError)
 async def validation_exception_handler(request: Request, exc: RequestValidationError):
     return JSONResponse(
-        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+        status_code=422,
         content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
     )

@@ -5,7 +5,7 @@
 from starlette.exceptions import HTTPException
 from starlette.requests import Request
 from starlette.responses import JSONResponse, Response
-from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
+from starlette.status import WS_1008_POLICY_VIOLATION

 async def http_exception_handler(request: Request, exc: HTTPException) -> Response:
@@ -21,7 +21,7 @@ async def request_validation_exception_handler(
     request: Request, exc: RequestValidationError
 ) -> JSONResponse:
     return JSONResponse(
-        status_code=HTTP_422_UNPROCESSABLE_ENTITY,
+        status_code=422,
         content={"detail": jsonable_encoder(exc.errors())},
     )

@@ -35,7 +35,6 @@
 from pydantic import BaseModel
 from starlette.responses import JSONResponse
 from starlette.routing import BaseRoute
-from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
 from typing_extensions import Literal

 validation_error_definition = {
@@ -416,7 +415,7 @@ def get_openapi_path(
                     )
                     deep_dict_update(openapi_response, process_response)
                     openapi_response["description"] = description
-            http422 = str(HTTP_422_UNPROCESSABLE_ENTITY)
+            http422 = "422"
             all_route_params = get_flat_params(route.dependant)
             if (all_route_params or route.body_field) and not any(
                 status in operation["responses"]

@@ -102,7 +102,7 @@ def test_schema():

 def test_get_invalid():
     response = client.get("/foo")
-    assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+    assert response.status_code == 422

 def test_get_valid():

Nice, thank you! 🚀

And thanks [USER_REDACTED] and [USER_REDACTED]! 🙌

This will be available in FastAPI 0.116.2, released in the next hours.

We have to be carefull here, this script is referenced in the docs [here](https://fastapi.tiangolo.com/tutorial/handling-errors/#use-the-requestvalidationerror-body), and by [NAME_REDACTED], the wrong line will be highlighted in the docs (used to be L14 which is now L16).

Anyway, I don't think we need this comment here, or elsewhere where we're switching to 422. Let's just edit to `status_code=422` without the comments.

> We have to be carefull here, this script is referenced in the docs [here](https://fastapi.tiangolo.com/tutorial/handling-errors/#use-the-requestvalidationerror-body), and by [NAME_REDACTED], the wrong line will be highlighted in the docs (used to be L14 which is now L16).
>
> Anyway, I don't think we need this comment here, or elsewhere where we're switching to 422. Let's just edit to `status_code=422` without the comments.

Thanks. Your comment-removal commit looks good.


🛠 Update `docs.py generate-readme` command to remove permalinks from headers

Currently `docs.py generate-readme` command copies content from `index.md` and that document contains permalinks for headers.
Thus, these permalinks are displayed when people read `README.md` on GitHub: https://github.com/fastapi/fastapi?tab=readme-ov-file#sponsors--sponsors-

This PR modifies `docs.py` script to remove permalinks when `README.md` is generated

@@ -42,7 +42,7 @@ The key features are:

 * estimation based on tests on an internal development team, building production applications.

-## Sponsors { #sponsors }
+## Sponsors

@@ -67,7 +67,7 @@ The key features are:

 Other sponsors

-## Opinions { #opinions }
+## Opinions

 "_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._"

@@ -113,22 +113,22 @@ The key features are:

 ---

-## **Typer**, the FastAPI of CLIs { #typer-the-fastapi-of-clis }
+## **Typer**, the FastAPI of CLIs

 If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**.

 **Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨ 🚀

-## Requirements { #requirements }
+## Requirements

 FastAPI stands on the shoulders of giants:

 * Starlette for the web parts.
 * Pydantic for the data parts.

-## Installation { #installation }
+## Installation

 Create and activate a virtual environment and then install FastAPI:

@@ -144,9 +144,9 @@ $ pip install "fastapi[standard]"

 **Note**: Make sure you put `"fastapi[standard]"` in quotes to ensure it works in all terminals.

-## Example { #example }
+## Example

-### Create it { #create-it }
+### Create it

 Create a file `main.py` with:

@@ -197,7 +197,7 @@ If you don't know, check the _"In a hurry?"_ section about <a href="https://fast

-### Run it { #run-it }
+### Run it

 Run the server with:

@@ -239,7 +239,7 @@ You can read more about it in the <a href="https://fastapi.tiangolo.com/fastapi-

-### Check it { #check-it }
+### Check it

 Open your browser at http://127.0.0.1:8000/items/5?q=somequery.

@@ -256,23 +256,23 @@ You already created an API that:
 * The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`.
 * The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`.

-### Interactive API docs { #interactive-api-docs }
+### Interactive API docs

 Now go to http://127.0.0.1:8000/docs.

 You will see the automatic interactive API documentation (provided by Swagger UI):

 ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png)

-### Alternative API docs { #alternative-api-docs }
+### Alternative API docs

 And now, go to http://127.0.0.1:8000/redoc.

 You will see the alternative automatic documentation (provided by ReDoc):

 ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png)

-## Example upgrade { #example-upgrade }
+## Example upgrade

 Now modify the file `main.py` to receive a body from a `PUT` request.

@@ -310,7 +310,7 @@ def update_item(item_id: int, item: Item):

 The `fastapi dev` server should reload automatically.

-### Interactive API docs upgrade { #interactive-api-docs-upgrade }
+### Interactive API docs upgrade

 Now go to http://127.0.0.1:8000/docs.

@@ -326,15 +326,15 @@ Now go to <a href="http://127.0.0.1:8000/docs" class="external-link" target="_bl

 ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png)

-### Alternative API docs upgrade { #alternative-api-docs-upgrade }
+### Alternative API docs upgrade

 And now, go to http://127.0.0.1:8000/redoc.

 * The alternative documentation will also reflect the new query parameter and body:

 ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png)

-### Recap { #recap }
+### [NAME_REDACTED] summary, you declare **once** the types of parameters, body, etc. as function parameters.

@@ -446,17 +446,17 @@ For a more complete example including more features, see the <a href="https://fa
     * **[NAME_REDACTED]**
     * ...and more.

-## Performance { #performance }
+## [NAME_REDACTED] benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*)

 To understand more about it, see the section Benchmarks.

-## Dependencies { #dependencies }
+## [NAME_REDACTED] depends on Pydantic and Starlette.

-### `standard` Dependencies { #standard-dependencies }
+### `standard` [NAME_REDACTED] you install FastAPI with `pip install "fastapi[standard]"` it comes with the `standard` group of optional dependencies:

@@ -476,15 +476,15 @@ Used by FastAPI:
 * `fastapi-cli[standard]` - to provide the `fastapi` command.
     * This includes `fastapi-cloud-cli`, which allows you to deploy your FastAPI application to [NAME_REDACTED].

-### Without `standard` Dependencies { #without-standard-dependencies }
+### Without `standard` [NAME_REDACTED] you don't want to include the `standard` optional dependencies, you can install with `pip install fastapi` instead of `pip install "fastapi[standard]"`.

-### Without `fastapi-cloud-cli` { #without-fastapi-cloud-cli }
+### Without `fastapi-cloud-cli`

 If you want to install FastAPI with the standard dependencies but without the `fastapi-cloud-cli`, you can install with `pip install "fastapi[standard-no-fastapi-cloud-cli]"`.

-### [NAME_REDACTED] { #additional-optional-dependencies }
+### [NAME_REDACTED]

 There are some additional dependencies you might want to install.

@@ -498,6 +498,6 @@ Additional optional FastAPI dependencies:
 * orjson - Required if you want to use `ORJSONResponse`.
 * ujson - Required if you want to use `UJSONResponse`.

-## License { #license }
+## License

 This project is licensed under the terms of the MIT license.

@@ -44,6 +44,8 @@
 site_path = Path("site").absolute()
 build_site_path = Path("site_build").absolute()

+header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$")
+

 [USER_REDACTED]_cache
 def is_mkdocs_insiders() -> bool:
@@ -154,9 +156,21 @@ def build_lang(
 """

+def remove_header_permalinks(content: str):
+    lines: list[str] = []
+    for line in content.split("\n"):
+        match = header_with_permalink_pattern.match(line)
+        if match:
+            hashes, title, *_ = match.groups()
+            line = f"{hashes} {title}"
+        lines.append(line)
+    return "\n".join(lines)
+
+
 def generate_readme_content() -> str:
     en_index = en_docs_path / "docs" / "index.md"
     content = en_index.read_text("utf-8")
+    content = remove_header_permalinks(content)  # remove permalinks from headers
     match_pre = re.search(r"</style>\n\n", content)
     match_start = re.search(r"<!-- sponsors -->", content)
     match_end = re.search(r"<!-- /sponsors -->", content)

Excellent job, thanks [USER_REDACTED]! 🙌

[NAME_REDACTED]! 🤯 😎