Skip to main content

multipart-response

Stream one HTTP response as multiple MIME parts. For Django, FastAPI, FastHTML, and Starlette.

Choose an adapter

Framework Import Primary classes
Django multipart_response.django MultipartResponse, Part, JsonPart
Starlette multipart_response.starlette MultipartResponse, Part
FastAPI multipart_response.fastapi MultipartResponse, Part, HTMLMultipartResponse, JSONMultipartResponse
FastHTML multipart_response.fasthtml MultipartResponse, Part

Stream one or many parts

Every adapter accepts one part or a sequence, sync iterable, or async iterable of parts:

MultipartResponse(Part("one"))
MultipartResponse([Part("one"), Part("two")])
MultipartResponse((Part("one"), Part("two")))

An iterable passed to MultipartResponse supplies several parts. An iterable passed to Part streams one part body:

MultipartResponse(parts())
MultipartResponse(Part(body_chunks()))

Integrations

Django

uv add "multipart-response[django]"

Stream parts from a Django view:

from multipart_response.django import JsonPart, MultipartResponse, Part


def generate_report(request):
    def parts():
        yield Part(
            "<p>Generating report...</p>",
            headers={"HX-Target": "#status"},
        )
        yield Part(
            '<li><a href="/reports/42">Quarterly report</a></li>',
            headers={
                "HX-Target": "#reports",
                "HX-Swap": "beforeend",
            },
        )
        yield JsonPart(
            {"report_id": 42, "status": "done"},
            headers={"Content-ID": "result"},
        )

    return MultipartResponse(parts())
  • Part(...) follows Django's HttpResponse API for content, headers, and cookies. HTML is the default content type.
  • JsonPart(...) follows JsonResponse for encoder, safe, json_dumps_params, content type, charset, and headers.
  • MultipartResponse(...) follows StreamingHttpResponse and adds subtype and boundary.
  • content_type="application/json" on Part remains metadata. Use JsonPart to serialize a Python value.

Use an async part source under ASGI:

async def updates(request):
    async def parts():
        yield Part("<p>Ready</p>")
        yield Part("<p>Done</p>")

    return MultipartResponse(parts())

Django 4.2 or later is required.

MultipartResponse subclasses Django's native StreamingHttpResponse.

FastAPI

uv add "multipart-response[fastapi]"

Stream multiple content types from one path operation:

import json

from fastapi import FastAPI
from multipart_response.fastapi import MultipartResponse, Part

app = FastAPI()


@app.get("/updates", response_class=MultipartResponse)
async def updates():
    yield Part("<p>Ready</p>", media_type="text/html")
    yield Part(
        json.dumps({"status": "ready"}),
        media_type="application/json",
        headers={"HX-Target": "#status"},
    )
    yield Part(chunk_stream(), media_type="video/mp4")

Stream HTML or JSON values directly when every part has one content type:

from multipart_response.fastapi import HTMLMultipartResponse, JSONMultipartResponse


@app.get("/html-updates", response_class=HTMLMultipartResponse)
async def html_updates():
    yield "<p>Ready</p>"
    yield "<p>Done</p>", {"HX-Target": "#status"}


@app.get("/json-updates", response_class=JSONMultipartResponse)
async def json_updates():
    yield {"status": "ready"}, {"HX-Target": "#status"}
    yield "done"

HTMLMultipartResponse renders strings as HTML. JSONMultipartResponse uses FastAPI's jsonable_encoder() and sets each part to application/json. The final part above contains the JSON string "done", including its quotes.

(value, headers) is shorthand for one converted part. Lists and tuples otherwise supply several parts. Wrap a JSON array in the outer source to keep it together:

JSONMultipartResponse([1, 2])    # Two number parts
JSONMultipartResponse([[1, 2]])  # One array part

MultipartResponse subclasses Starlette's native StreamingResponse, which FastAPI uses for streamed responses.

FastHTML

uv add "multipart-response[fasthtml]"

Return FastHTML values as parts:

from fasthtml.common import Div, P, fast_app
from multipart_response.fasthtml import MultipartResponse, Part

app, rt = fast_app()


@rt
def updates():
    return MultipartResponse([
        P("Ready"),
        {"status": "working"},
        Part(Div("Done"), hx_target="#status"),
    ])

FastHTML components and strings become HTML parts. Mappings become JSON parts. Component rendering follows FastHTML's fh_cfg.indent setting. (value, headers) is shorthand for one part.

The final part above includes:

Content-Type: text/html; charset=utf-8
HX-Target: #status

<div>Done</div>

Stream one component per part:

@rt
async def messages():
    return MultipartResponse(
        P(message) async for message in inbox()
    )

Each component produces one part and one htmx swap. Return MultipartResponse(...) explicitly. FastHTML does not infer it from a bare async-generator route.

Set htmx response headers on one part:

yield Part(
    P(message.text),
    hx_target="#feed",
    hx_swap="beforeend",
    hx_part_id=str(message.id),
)

Any hx_* keyword becomes an HX-* header. This includes future htmx response headers. Keyword values override matching entries in headers. A None value does not add or replace a header; an empty string is preserved.

Set envelope defaults for every part:

return MultipartResponse(
    (P(message) async for message in inbox()),
    hx_target="#feed",
    hx_swap="beforeend",
)

The response starts with:

Content-Type: multipart/mixed; boundary=...
HX-Target: #feed
HX-Swap: beforeend

Part headers override envelope defaults. MultipartResponse accepts the same open-ended hx_* keywords as Part.

Resume a persistent connection after its last completed part:

@rt
async def events(hx_last_part_id: str | None = None):
    async def parts():
        async for message in messages_after(hx_last_part_id):
            yield Part(
                P(message.text),
                hx_target="#feed",
                hx_swap="beforeend",
                hx_part_id=str(message.id),
            )

    return MultipartResponse(parts())

FastHTML injects the request's HX-Last-Part-ID header into the annotated parameter. An empty hx_part_id="" resets the reconnect cursor.

Stream several components inside one part:

@rt
async def transcript():
    return MultipartResponse(
        Part(P(message) async for message in inbox())
    )

Sync and async component iterables work. Their output forms one part body and one htmx swap.

MultipartResponse subclasses Starlette's native StreamingResponse, which FastHTML uses for streamed responses.

Starlette

uv add "multipart-response[starlette]"

Return a Starlette response from an endpoint:

from multipart_response.starlette import MultipartResponse, Part


async def updates(request):
    part = Part("Ready", media_type="text/plain", headers={"Content-ID": "status"})
    part.headers["HX-Target"] = "#status"
    part.set_cookie("seen", "yes")

    return MultipartResponse(
        [part],
        status_code=200,
        headers={"X-Stream": "updates"},
    )
  • Part(...) follows Starlette's Response API for content, media type, headers, and cookies.
  • MultipartResponse(...) follows StreamingResponse and adds subtype and boundary.
  • media_type="application/json" does not serialize a Python value. Pass a JSON string or bytes.

MultipartResponse subclasses Starlette's native StreamingResponse.

Nested parts

MultipartResponse accepts Part, MIMEPart, and nested MIMEMultipart values:

from multipart_response import MIMEMultipart
from multipart_response.fastapi import Part

alternative = MIMEMultipart(
    [
        Part("Plain text", media_type="text/plain"),
        Part("<p>HTML</p>", media_type="text/html"),
    ],
    subtype="alternative",
)

A sequence is buffered. A sync or async iterable streams.

htmx

The hx-multipart extension swaps each part as it arrives.

The hx-multipart extension documentation

Use HX-Target, HX-Swap, and HX-Select to control each part.

It uses fetch-multipart to parse the stream.

Low-level MIME API

For nested MIME entities and custom integrations, the dependency-free package root exports MIMEMultipart, MIMEPart, and MIMEMultipartWriter.

  • Boundaries and MIME headers are validated against RFC 2046 limits.
  • Body chunks are checked for boundary collisions.
  • Static, streamed, and nested multipart content is supported.

License

BSD-3-Clause

Metadata

Release files for multipart-response 0.6.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for multipart-response 0.6.1
File Size Uploaded
multipart_response-0.6.1.tar.gz 270.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for multipart-response 0.6.1
File Interpreter ABI Platform
multipart_response-0.6.1-py3-none-any.whl Python 3 none any Details

Total release size: 289.3 kB

Release files / multipart_response-0.6.1.tar.gz

Download URL multipart_response-0.6.1.tar.gz
Size 270.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0ab1442964b3109522a7820a987c2e625782725f7e0378ebd2681041385104f8
BLAKE2b-256 checksum
How to use checksums
cc8078b8707601f8a90453f38924e2650a09d62e57de25a0c832e79fe797fcc8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release files / multipart_response-0.6.1-py3-none-any.whl

Download URL multipart_response-0.6.1-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
11abf73e28ddb259f927b5f8cc941fae044a3ee98fe8e21f0879dee1b4938656
BLAKE2b-256 checksum
How to use checksums
a7c844bba18c70e7d6c4742749c1c43b552c869fcc21db165ba539090c84acac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.1 This release

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page