mrkeyoor.com_
Thu 06 Aug 00:57 UTC
PyPIWeb Backendupdated 05 Aug 2026

python-multipart

python-multipart is a streaming parser for multipart/form-data, the encoding browsers use for file uploads and HTML forms. Instead of buffering a whole request body, you feed it bytes as they arrive and it fires callbacks as fields and files are discovered, spilling large files to temp storage past a memory threshold. Most of its 96M weekly downloads come from one fact: Starlette and FastAPI require it for request.form() and UploadFile, so nearly every FastAPI app installs it without ever importing it directly.

Verdict

If you run FastAPI or Starlette with uploads, this is not a choice, it is a requirement, and it does that one job well under active maintenance. Nobody else should reach for it unless they are building framework-level plumbing and want callback-driven streaming.

API stability3/5Still 0.0.x with no declared stable API, and the import rename from multipart to python_multipart broke downstream code paths in 2024; the parser API itself has changed little in practice.
Docs3/5kludex.github.io/python-multipart has an API reference with docstrings and a short usage page, but the README is four lines and real-world examples (framework integration, config options) are thin.
Maintenance4/5Maintained by Marcelo Trylesinski (Kludex, also a Starlette/Uvicorn maintainer), pushed August 2026 with only 9 open issues and PRs and regular releases; single-maintainer risk is the only caveat.
Ecosystem5/5About 96M weekly downloads as the form-parsing dependency of the FastAPI/Starlette world; any question you have is answered in those frameworks' issue trackers.

Use it if

  • You use FastAPI or Starlette with forms or file uploads: it is the mandatory backend for Form(), File(), and UploadFile, full stop
  • You are writing your own ASGI/WSGI framework or raw server and need an incremental multipart parser you can feed chunk by chunk
  • You handle large uploads and cannot afford to hold them in RAM: the File abstraction spills to disk after a configurable threshold (1 MiB by default)
  • You need to parse multipart bodies outside HTTP servers, for example replaying stored requests or handling multipart payloads from queues
Skip it if

Setup reality

pip install python-multipart is trivial: pure Python, zero dependencies, Python 3.10+ for current releases. The one real trap is the naming mess: the PyPI package is python-multipart but the import is python_multipart, and there is an unrelated PyPI package called multipart that historically fought over the same import name; environments that somehow get both installed produce confusing shadowing. In FastAPI you never import it, you just install it, and forgetting it yields the runtime error telling you form data support needs python-multipart.

Patterns

Enable form and file support in FastAPIinstall-for-fastapi

$ pip install python-multipart

# app.py
from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()

@app.post("/upload")
async def upload(file: UploadFile = File(...), note: str = Form("")):
    data = await file.read()
    return {"filename": file.filename, "size": len(data), "note": note}

You never import python_multipart here; FastAPI detects it at runtime and raises a clear error at startup of the route if it is missing.

Parse a complete body with parse_formparse-form-helper

from python_multipart import parse_form

fields, files = [], []

headers = {"Content-Type": content_type_header, "Content-Length": str(length)}
parse_form(headers, body_stream, fields.append, files.append)

for f in fields:
    print(f.field_name, f.value)
for f in files:
    print(f.file_name, f.size)

headers needs at least Content-Type including the boundary parameter; body_stream is any object with read(). Field names and values arrive as bytes, not str.

Stream chunks into MultipartParser with callbacksstreaming-parser-callbacks

from python_multipart import MultipartParser

parts = []

callbacks = {
    "on_part_data": lambda data, start, end: parts.append(data[start:end]),
    "on_part_end": lambda: parts.append(b"--END--"),
}
parser = MultipartParser(boundary, callbacks)

for chunk in iter_request_chunks():
    parser.write(chunk)
parser.finalize()

Callbacks receive (data, start, end) slices into the chunk buffer, so copy the slice out if you keep it; forgetting finalize() silently drops the last part.

Extract the boundary from Content-Typeparse-content-type-header

from python_multipart import multipart

ctype, params = multipart.parse_options_header(
    "multipart/form-data; boundary=----WebKitFormBoundaryX"
)
boundary = params[b"boundary"]

Both the media type and param values come back as bytes; parse_options_header also handles quoted and encoded filename* parameters from upload headers.

Control when uploads spill to diskspill-large-files-to-disk

from python_multipart import create_form_parser

config = {
    "UPLOAD_DIR": "/var/uploads/tmp",
    "MAX_MEMORY_FILE_SIZE": 256 * 1024,  # bytes kept in RAM per file
    "UPLOAD_KEEP_FILENAME": True,
    "UPLOAD_KEEP_EXTENSIONS": True,
}

def on_file(f):
    print("stored at", f.actual_file_name, "in_memory:", f.in_memory)

parser = create_form_parser(headers, None, on_file, config=config)
for chunk in body_chunks:
    parser.write(chunk)
parser.finalize()

parse_form() does not take a config argument, so use create_form_parser when you need these knobs; files under MAX_MEMORY_FILE_SIZE stay in a BytesIO and actual_file_name is None until something forces a disk write.

Read a parsed File object safelyread-parsed-file

def on_file(f):
    f.file_object.seek(0)
    content = f.file_object.read()
    name = (f.file_name or b"unnamed").decode("utf-8", "replace")
    save(name, content)

The parser leaves the file position at the end of the written data, so seek(0) before reading; file_name is bytes and attacker-controlled, so never trust it as a path.

Stream-parse urlencoded bodies tooparse-querystring

from python_multipart import QuerystringParser

pairs = []
callbacks = {
    "on_field_name": lambda d, s, e: pairs.append([d[s:e], b""]),
    "on_field_data": lambda d, s, e: pairs.__setitem__(-1, [pairs[-1][0], pairs[-1][1] + d[s:e]]),
}
parser = QuerystringParser(callbacks)
parser.write(b"a=1&b=hello")
parser.finalize()

For application/x-www-form-urlencoded bodies; values arrive still percent-encoded, so run urllib.parse.unquote_to_bytes on them yourself.

Handle raw application/octet-stream uploadsoctet-stream-upload

from python_multipart import OctetStreamParser

received = []
parser = OctetStreamParser(callbacks={
    "on_data": lambda d, s, e: received.append(d[s:e]),
})
for chunk in body_chunks:
    parser.write(chunk)
parser.finalize()

Octet-stream bodies have no field name or filename; if you need those, clients must send them in separate headers like Content-Disposition.

Access parsed forms in Starlettestarlette-form-usage

from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import JSONResponse

async def handler(request):
    form = await request.form()  # requires python-multipart installed
    upload = form["file"]
    return JSONResponse({"name": upload.filename, "type": upload.content_type})

app = Starlette(routes=[Route("/u", handler, methods=["POST"])])

Starlette streams the body through this package's parser and hands you UploadFile objects backed by SpooledTemporaryFile; large files never fully occupy RAM.

Alternatives

PackageRegistryPick it when
multipartPyPIYou want a small, strict multipart parser with a dict-like parsed result instead of callbacks (defnull's package, unrelated to this one).
streaming-form-dataPyPIYou need maximum throughput on very large uploads; its Cython parser streams parts directly into user-defined targets.
werkzeugPyPIYou are in Flask territory already; its form data parsing is built in and battle-tested.