From 1377052c6cc5eff92a6bcbb54c0fcb66e14e06d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 27 Feb 2026 12:51:40 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20responses?= =?UTF-8?q?=20and=20new=20stream=20with=20`yield`=20(#15023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/custom-response.md | 32 +++++++------------ docs/en/docs/advanced/response-directly.md | 12 ++++--- docs/en/docs/advanced/stream-data.md | 18 +++++++++-- docs_src/stream_data/tutorial002_py310.py | 26 ++++++++++----- .../test_stream_data/test_tutorial002.py | 15 +++++++++ 5 files changed, 67 insertions(+), 36 deletions(-) diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index 823ee5ff2c..e0fafa5dfe 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -138,6 +138,14 @@ Takes some data and returns an `application/json` encoded response. This is the default response used in **FastAPI**, as you read above. +/// note | Technical Details + +But if you declare a response model or return type, that will be used directly to serialize the data to JSON, and a response with the right media type for JSON will be returned directly, without using the `JSONResponse` class. + +This is the ideal way to get the best performance. + +/// + ### `RedirectResponse` { #redirectresponse } Returns an HTTP redirect. Uses a 307 status code (Temporary Redirect) by default. @@ -165,7 +173,7 @@ You can also use the `status_code` parameter combined with the `response_class` ### `StreamingResponse` { #streamingresponse } -Takes an async generator or a normal generator/iterator and streams the response body. +Takes an async generator or a normal generator/iterator (a function with `yield`) and streams the response body. {* ../../docs_src/custom_response/tutorial007_py310.py hl[3,16] *} @@ -179,27 +187,11 @@ This would be even more important with large or infinite streams. /// -#### Using `StreamingResponse` with file-like objects { #using-streamingresponse-with-file-like-objects } - -If you have a file-like object (e.g. the object returned by `open()`), you can create a generator function to iterate over that file-like object. - -That way, you don't have to read it all first in memory, and you can pass that generator function to the `StreamingResponse`, and return it. - -This includes many libraries to interact with cloud storage, video processing, and others. - -{* ../../docs_src/custom_response/tutorial008_py310.py hl[2,10:12,14] *} - -1. This is the generator function. It's a "generator function" because it contains `yield` statements inside. -2. By using a `with` block, we make sure that the file-like object is closed after the generator function is done. So, after it finishes sending the response. -3. This `yield from` tells the function to iterate over that thing named `file_like`. And then, for each part iterated, yield that part as coming from this generator function (`iterfile`). - - So, it is a generator function that transfers the "generating" work to something else internally. - - By doing it this way, we can put it in a `with` block, and that way, ensure that the file-like object is closed after finishing. - /// tip -Notice that here as we are using standard `open()` that doesn't support `async` and `await`, we declare the path operation with normal `def`. +Instead of returning a `StreamingResponse` directly, you should probably follow the style in [Stream Data](./stream-data.md){.internal-link target=_blank}, it's much more convenient and handles cancellation behind the scenes for you. + +If you are streaming JSON Lines, follow the [Stream JSON Lines](../tutorial/stream-json-lines.md){.internal-link target=_blank} tutorial. /// diff --git a/docs/en/docs/advanced/response-directly.md b/docs/en/docs/advanced/response-directly.md index 9d58490eb1..dd0e63c396 100644 --- a/docs/en/docs/advanced/response-directly.md +++ b/docs/en/docs/advanced/response-directly.md @@ -16,7 +16,7 @@ You will normally have much better performance using a [Response Model](../tutor ## Return a `Response` { #return-a-response } -You can return any `Response` or any sub-class of it. +You can return a `Response` or any sub-class of it. /// info @@ -28,7 +28,9 @@ And when you return a `Response`, **FastAPI** will pass it directly. It won't do any data conversion with Pydantic models, it won't convert the contents to any type, etc. -This gives you a lot of flexibility. You can return any data type, override any data declaration or validation, etc. +This gives you a lot of **flexibility**. You can return any data type, override any data declaration or validation, etc. + +It also gives you a lot of **responsibility**. You have to make sure that the data you return is correct, in the correct format, that it can be serialized, etc. ## Using the `jsonable_encoder` in a `Response` { #using-the-jsonable-encoder-in-a-response } @@ -62,15 +64,15 @@ You could put your XML content in a string, put that in a `Response`, and return ## How a Response Model Works { #how-a-response-model-works } -When you declare a [Response Model](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic. +When you declare a [Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank} in a path operation, **FastAPI** will use it to serialize the data to JSON, using Pydantic. {* ../../docs_src/response_model/tutorial001_01_py310.py hl[16,21] *} As that will happen on the Rust side, the performance will be much better than if it was done with regular Python and the `JSONResponse` class. -When using a response model FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class. +When using a `response_model` or return type, FastAPI won't use the `jsonable_encoder` to convert the data (which would be slower) nor the `JSONResponse` class. -Instead it takes the JSON bytes generated with Pydantic using the response model and returns a `Response` with the right media type for JSON directly (`application/json`). +Instead it takes the JSON bytes generated with Pydantic using the response model (or return type) and returns a `Response` with the right media type for JSON directly (`application/json`). ## Notes { #notes } diff --git a/docs/en/docs/advanced/stream-data.md b/docs/en/docs/advanced/stream-data.md index 4bec4edf99..422ade867a 100644 --- a/docs/en/docs/advanced/stream-data.md +++ b/docs/en/docs/advanced/stream-data.md @@ -54,7 +54,7 @@ For example, you can create a `PNGStreamingResponse` that sets the `Content-Type Then you can use this new class in `response_class=PNGStreamingResponse` in your *path operation function*: -{* ../../docs_src/stream_data/tutorial002_py310.py ln[23:26] hl[23] *} +{* ../../docs_src/stream_data/tutorial002_py310.py ln[23:27] hl[23] *} ### Simulate a File { #simulate-a-file } @@ -62,7 +62,7 @@ In this example, we are simulating a file with `io.BytesIO`, which is a file-lik For example, we can iterate over it to consume its contents, as we could with a file. -{* ../../docs_src/stream_data/tutorial002_py310.py ln[1:26] hl[3,12:13,25] *} +{* ../../docs_src/stream_data/tutorial002_py310.py ln[1:27] hl[3,12:13,25] *} /// note | Technical Details @@ -72,6 +72,10 @@ Only so that it can live in the same file for this example and you can copy it a /// +By using a `with` block, we make sure that the file-like object is closed after the generator function (the function with `yield`) is done. So, after it finishes sending the response. + +It wouldn't be that important in this specific example because it's a fake in-memory file (with `io.BytesIO`), but with a real file, it would be important to make sure the file is closed after the work with it is done. + ### Files and Async { #files-and-async } In most cases, file-like objects are not compatible with async and await by default. @@ -90,10 +94,18 @@ But in many cases reading a file or a file-like object would block. To avoid blocking the event loop, you can simply declare the *path operation function* with regular `def` instead of `async def`, that way FastAPI will run it on a threadpool worker, to avoid blocking the main loop. -{* ../../docs_src/stream_data/tutorial002_py310.py ln[29:32] hl[30] *} +{* ../../docs_src/stream_data/tutorial002_py310.py ln[30:34] hl[31] *} /// tip If you need to call blocking code from inside of an async function, or an async function from inside of a blocking function, you could use Asyncer, a sibling library to FastAPI. /// + +### `yield from` { #yield-from } + +When you are iterating over something, like a file-like object, and then you are doing `yield` for each item, you could also use `yield from` to yield each item directly and skip the `for` loop. + +This is not particular to FastAPI, it's just Python, but it's a nice trick to know. 😎 + +{* ../../docs_src/stream_data/tutorial002_py310.py ln[37:40] hl[40] *} diff --git a/docs_src/stream_data/tutorial002_py310.py b/docs_src/stream_data/tutorial002_py310.py index 7fc884fa25..aa8bcee3a9 100644 --- a/docs_src/stream_data/tutorial002_py310.py +++ b/docs_src/stream_data/tutorial002_py310.py @@ -22,23 +22,33 @@ class PNGStreamingResponse(StreamingResponse): @app.get("/image/stream", response_class=PNGStreamingResponse) async def stream_image() -> AsyncIterable[bytes]: - for chunk in read_image(): - yield chunk + with read_image() as image_file: + for chunk in image_file: + yield chunk @app.get("/image/stream-no-async", response_class=PNGStreamingResponse) def stream_image_no_async() -> Iterable[bytes]: - for chunk in read_image(): - yield chunk + with read_image() as image_file: + for chunk in image_file: + yield chunk + + +@app.get("/image/stream-no-async-yield-from", response_class=PNGStreamingResponse) +def stream_image_no_async_yield_from() -> Iterable[bytes]: + with read_image() as image_file: + yield from image_file @app.get("/image/stream-no-annotation", response_class=PNGStreamingResponse) async def stream_image_no_annotation(): - for chunk in read_image(): - yield chunk + with read_image() as image_file: + for chunk in image_file: + yield chunk @app.get("/image/stream-no-async-no-annotation", response_class=PNGStreamingResponse) def stream_image_no_async_no_annotation(): - for chunk in read_image(): - yield chunk + with read_image() as image_file: + for chunk in image_file: + yield chunk diff --git a/tests/test_tutorial/test_stream_data/test_tutorial002.py b/tests/test_tutorial/test_stream_data/test_tutorial002.py index 83201a7a22..8bd7384c57 100644 --- a/tests/test_tutorial/test_stream_data/test_tutorial002.py +++ b/tests/test_tutorial/test_stream_data/test_tutorial002.py @@ -26,6 +26,7 @@ def get_client(mod): [ "/image/stream", "/image/stream-no-async", + "/image/stream-no-async-yield-from", "/image/stream-no-annotation", "/image/stream-no-async-no-annotation", ], @@ -73,6 +74,20 @@ def test_openapi_schema(client: TestClient): }, } }, + "/image/stream-no-async-yield-from": { + "get": { + "summary": "Stream Image No Async Yield From", + "operationId": "stream_image_no_async_yield_from_image_stream_no_async_yield_from_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "image/png": {"schema": {"type": "string"}} + }, + } + }, + } + }, "/image/stream-no-annotation": { "get": { "summary": "Stream Image No Annotation",