Chapter 7 · Forms, File Upload, and Download
Chapter goals: Master receiving form fields with
Form, file uploads withFile/UploadFile(including multi-file), understand the underlying form encoding, and implement file download endpoints.
7.1 Why Forms Are Needed
In previous chapters we received JSON request bodies. But two scenarios require form data:
- Native HTML
<form>submission (login pages, JS-free legacy pages); - OAuth2 password grant specification requires
usernameandpasswordto be sent as form fields, not JSON.
Form data uses two encodings:
application/x-www-form-urlencodedwhen no files are involved;multipart/form-datawhen files are included.
WARNING
You must install python-multipart before receiving forms or files, or requests will error immediately:
pip install python-multipartThis is an HTTP protocol-level encoding parsing requirement, not an optional FastAPI component.
7.2 Declaring Form Fields with Form
Form works identically to Body and Query — supporting defaults, validation, aliases, and all parameters. The official docs explicitly state: Form is a direct subclass of Body.
from fastapi import FastAPI, Form
app = FastAPI()
@app.post("/login/")
async def login(
username: str = Form(), # Required form field
password: str = Form(), # Required form field
remember: bool = Form(False), # Optional field with default
):
# Demo only: never compare plaintext passwords in production
return {"username": username, "remember": remember}Verify with curl (note -F instead of JSON's -d):
curl -X POST http://127.0.0.1:8000/login/ \
-F username=alice -F password=secret -F remember=trueCannot mix with JSON Body
You can declare multiple Form parameters in one path operation, but you cannot also declare a JSON Body field — because the request body encoding is already application/x-www-form-urlencoded or multipart/form-data, not application/json. This is an HTTP protocol limitation, not a FastAPI limitation.
7.3 File Upload: bytes vs UploadFile
File declares an uploaded file (it inherits from Form). FastAPI provides two receiving approaches:
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload-small/")
async def upload_small(file: bytes = File()):
# Method 1: declare as bytes — entire file content loaded into memory at once
return {"size": len(file)}
@app.post("/upload/")
async def upload(myfile: UploadFile): # Method 2: UploadFile (recommended)
contents = await myfile.read() # Async read content
await myfile.seek(0) # Reset cursor for re-reading
return {
"filename": myfile.filename, # Original filename, e.g. report.pdf
"content_type": myfile.content_type, # MIME type, e.g. application/pdf
"length": len(contents),
}Key differences:
| Aspect | bytes = File() | UploadFile |
|---|---|---|
| Memory usage | Entire file in memory | Spooled: small files in memory, auto-flushes to disk when oversized |
| Metadata | None | Has filename, content_type, file attributes |
| Interface | Plain bytes | file-like async methods: read / write / seek / close |
| Best for | Small files (icons, thumbnails) | Large files (images, video, arbitrary binaries) |
UploadFile async methods all require await; in a plain def path function, you can operate on the sync myfile.file directly (a real SpooledTemporaryFile).
Optional files and metadata
Make the type Optional[UploadFile] with default None for an optional upload; or myfile: UploadFile = File(description="Report file") to add a description in the docs.
7.4 Multi-File Upload and Size Limit Practices
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/upload-multiple/")
async def upload_multiple(files: list[UploadFile] = File(default=[])):
results = []
for file in files:
contents = await file.read()
results.append({
"filename": file.filename,
"size": len(contents),
})
return {"uploaded": results}For size limits, use UploadFile with manual checks or configure max_file_size at the middleware level. Starlette's FormParser has a max_file_size parameter you can set on the app.
7.5 File Download
Return a FileResponse to stream a file to the client:
from fastapi import FastAPI
from fastapi.responses import FileResponse
from pathlib import Path
app = FastAPI()
@app.get("/download/{filename}")
def download_file(filename: str):
path = Path("static/files") / filename
if not path.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(
path,
filename=filename, # Suggested download filename
media_type="application/octet-stream",
)FileResponse streams the file in chunks — memory efficient even for gigabyte-sized files.
7.6 Chapter Summary
- Install
python-multipartfor form/file support; Form()for text fields;File()/UploadFile()for file uploads;UploadFileis preferred for large files (spooled, async, metadata-rich);bytes = File()for small files where simplicity matters;FileResponsestreams downloads efficiently;- Cannot mix form and JSON body in the same endpoint.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Why must python-multipart be installed separately?
2. When should you prefer UploadFile over bytes = File()?
3. Can you declare both Form fields and a JSON Body in the same endpoint?
4. What does FileResponse do differently from returning a dict?
🛠️ Hands-On Exercises
- Write a POST
/uploadendpoint accepting a singleUploadFile; save it to disk and return the filename and size. - Write a POST
/upload-multipleendpoint acceptinglist[UploadFile]; verify all files are saved and metadata is correct. - Write a GET
/download/{filename}endpoint usingFileResponse; test with curl--outputand verify the file matches the source.
Proceed to Chapter 8: Cookie and Header Parameters.