Skip to content

Chapter 7 · Forms, File Upload, and Download ​

Chapter goals: Master receiving form fields with Form, file uploads with File/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:

  1. Native HTML <form> submission (login pages, JS-free legacy pages);
  2. OAuth2 password grant specification requires username and password to be sent as form fields, not JSON.

Form data uses two encodings:

  • application/x-www-form-urlencoded when no files are involved;
  • multipart/form-data when files are included.

WARNING

You must install python-multipart before receiving forms or files, or requests will error immediately:

bash
pip install python-multipart

This 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.

python
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):

bash
curl -X POST http://127.0.0.1:8000/login/ \
     -F username=alice -F password=secret -F remember=true

Cannot 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:

python
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:

Aspectbytes = File()UploadFile
Memory usageEntire file in memorySpooled: small files in memory, auto-flushes to disk when oversized
MetadataNoneHas filename, content_type, file attributes
InterfacePlain bytesfile-like async methods: read / write / seek / close
Best forSmall 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 ​

python
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:

python
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-multipart for form/file support;
  • Form() for text fields; File() / UploadFile() for file uploads;
  • UploadFile is preferred for large files (spooled, async, metadata-rich);
  • bytes = File() for small files where simplicity matters;
  • FileResponse streams 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 ​

  1. Write a POST /upload endpoint accepting a single UploadFile; save it to disk and return the filename and size.
  2. Write a POST /upload-multiple endpoint accepting list[UploadFile]; verify all files are saved and metadata is correct.
  3. Write a GET /download/{filename} endpoint using FileResponse; test with curl --output and verify the file matches the source.

Proceed to Chapter 8: Cookie and Header Parameters.