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.
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.
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
- You are on Django, Flask, or another full framework: they ship their own multipart parsing and this package adds nothing
- You want a friendly parsed-result API: the native interface is on_field/on_file callbacks over bytes, which is low-level plumbing; frameworks exist to hide exactly this
- The 0.0.x version number tells the truth about scope: it is a single-purpose parser, not a form toolkit; validation, size limits per field, and CSRF are all on you
- You still import it as 'multipart': the legacy import name was removed after clashing with the separate multipart package on PyPI, and old snippets using 'import multipart' now target the wrong project
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
| Package | Registry | Pick it when |
|---|---|---|
| multipart | PyPI | You want a small, strict multipart parser with a dict-like parsed result instead of callbacks (defnull's package, unrelated to this one). |
| streaming-form-data | PyPI | You need maximum throughput on very large uploads; its Cython parser streams parts directly into user-defined targets. |
| werkzeug | PyPI | You are in Flask territory already; its form data parsing is built in and battle-tested. |