Skip to main content

Form Data and File Uploads

In this tutorial, you will build a robust endpoint for processing user submissions that include both text data and file uploads. You will learn how to use the Form and File classes to handle multipart/form-data and how to use UploadFile for efficient file processing.

Prerequisites

To handle form data and file uploads, you must install the python-multipart package. FastAPI relies on this library to parse the incoming request body.

pip install python-multipart

[!WARNING] Ensure you install python-multipart and not the package named multipart. Installing the wrong package will result in a RuntimeError when your application tries to parse form data.

Step 1: Defining Form Fields

Standard HTML forms send data as application/x-www-form-urlencoded. To receive these fields in FastAPI, use the Form class from fastapi.params.

Create a simple login endpoint that receives a username and password:

from typing import Annotated
from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login/")
async def login(
username: Annotated[str, Form()],
password: Annotated[str, Form()]
):
return {"username": username}

By using Annotated[str, Form()], you tell FastAPI that these parameters should be extracted from the form body rather than from query parameters or a JSON body.

Step 2: Handling File Uploads

When you need to receive files, the request uses multipart/form-data. FastAPI provides the File class to define these parameters. You can receive files in two ways: as bytes or as an UploadFile object.

Using bytes

If you declare the type as bytes, FastAPI will read the entire file into memory. This is suitable for small files.

from typing import Annotated
from fastapi import FastAPI, File

app = FastAPI()

@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
return {"file_size": len(file)}

Using UploadFile

For larger files, use the UploadFile class from fastapi.datastructures. UploadFile has several advantages:

  • It uses a "spooled" file: files up to a certain size are kept in memory, but beyond that, they are stored on disk.
  • It provides metadata like filename, size, and content_type.
  • It has an async interface for reading and writing.
from typing import Annotated
from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {
"filename": file.filename,
"content_type": file.content_type,
"size": file.size
}

The UploadFile object includes methods like await file.read(), await file.seek(0), and await file.close().

Step 3: Combining Forms and Files

You can define multiple File and Form parameters in the same path operation. FastAPI will correctly parse the multipart/form-data request and populate each parameter.

from typing import Annotated
from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()

@app.post("/profile/")
async def update_profile(
file: Annotated[bytes, File()],
fileb: Annotated[UploadFile, File()],
notes: Annotated[str, Form()],
):
return {
"file_size": len(file),
"notes": notes,
"fileb_content_type": fileb.content_type,
}

In this example, file is read into memory as bytes, while fileb is handled as an UploadFile.

Step 4: Uploading Multiple Files

To receive multiple files in a single field, use a list of bytes or UploadFile.

from typing import Annotated
from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/upload-multiple/")
async def create_upload_files(
files: Annotated[list[UploadFile], File(description="Multiple files as UploadFile")]
):
return {"filenames": [file.filename for file in files]}

When you define a list of files, the client must send multiple parts with the same field name (in this case, files).

Step 5: Grouping Form Fields with Pydantic

As your forms grow, you might want to group fields into a single object. You can use a Pydantic model with Form().

from typing import Annotated
from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()

class UserForm(BaseModel):
username: str
lastname: str
age: int | None = None

@app.post("/user/")
async def post_form(user: Annotated[UserForm, Form()]):
return user

FastAPI will extract the fields defined in UserForm (username, lastname, age) from the form data and validate them using the Pydantic model. This keeps your function signatures clean and leverages Pydantic's powerful validation.

Complete Result

By combining these techniques, you can build complex endpoints that handle diverse data types. Here is a complete example of a user registration endpoint:

from typing import Annotated
from fastapi import FastAPI, File, Form, UploadFile
from pydantic import BaseModel

app = FastAPI()

class RegistrationData(BaseModel):
email: str
marketing_consent: bool = False

@app.post("/register/")
async def register_user(
user_data: Annotated[RegistrationData, Form()],
profile_pic: UploadFile,
documents: Annotated[list[UploadFile], File()]
):
# Process the UploadFile
content = await profile_pic.read()

return {
"email": user_data.email,
"profile_pic_name": profile_pic.filename,
"document_count": len(documents)
}

This endpoint handles a Pydantic model (parsed from form fields), a single file upload, and a list of supporting documents, all within a single multipart/form-data request.