Course Content
FastAPI Essentials
1 sections · 32 lessons
What features does FastAPI provide for form handling?
What you need to know
There are two form encodings:
application/x-www-form-urlencoded— simplekey=value&key2=value2text, sent by basic HTML forms.multipart/form-data— the body is split into parts; needed when a form includes files.
FastAPI supports both through Form(), and adds files with File() / UploadFile.
1from typing import Annotated2from fastapi import Depends, FastAPI, File, Form, UploadFile3from fastapi.security import OAuth2PasswordRequestForm4from pydantic import BaseModel, Field56app = FastAPI()78class Feedback(BaseModel):9 run_id: str10 rating: int = Field(ge=1, le=5)11 comment: str = ""1213@app.post("/feedback")14def feedback(form: Annotated[Feedback, Form()]): # a whole model as a form15 return form1617@app.post("/feedback-with-screenshot")18def feedback_shot(rating: Annotated[int, Form(ge=1, le=5)],19 screenshot: Annotated[UploadFile | None, File()] = None):20 return {"rating": rating, "screenshot": screenshot.filename if screenshot else None}2122@app.post("/token")23def token(form: Annotated[OAuth2PasswordRequestForm, Depends()]):24 return {"username": form.username}Real responses:
POST /feedback form run_id=r-17 rating=4 comment=Good summary -> {'run_id': 'r-17', 'rating': 4, 'comment': 'Good summary'}POST /feedback form rating=9 -> 422 [(['body', 'rating'], 'Input should be less than or equal to 5')]POST /feedback as JSON -> 422 (the endpoint expects a form)POST /feedback-with-screenshot rating=5 + bug.png -> {'rating': 5, 'screenshot': 'bug.png'}POST /token username=asha password=... -> {'username': 'asha'}The form value "4" arrived as text and became the integer 4; 9 broke the le=5 rule and got a normal 422. Sending JSON to a form endpoint fails, because the content type is wrong.
Mixing forms and JSON
A route declared as rating: Annotated[int, Form()], body: Question looks reasonable but cannot work. FastAPI saw a form field and documented the whole request body as application/x-www-form-urlencoded; the JSON model was then expected as a form field called body, and the real request failed with (['body', 'body'], 'missing'). One request has one body. If you need structured data plus a file, send the structured part as form fields, or as a JSON string in one form field that you parse with Model.model_validate_json.
A real-life example
An AI writing tool shows "Rate this answer" with 1–5 stars, a comment box and an optional screenshot. The front end posts it as multipart/form-data. The team first declared each field separately in the route signature; when they added model_version and latency_ms, the signature grew to eight parameters and the rules drifted between two endpoints.
They switched to form: Annotated[Feedback, Form()] with one Feedback model shared by both endpoints and by the analytics job that reads the feedback table. Validation messages now match across the product, and the /docs page shows a single, clear form.
Follow-up questions to expect
- "Why is the OAuth2 token endpoint a form?" — The OAuth2 specification defines the password grant request as
application/x-www-form-urlencoded, soOAuth2PasswordRequestFormreadsusername,password,scopeandgrant_typeas form fields. - "What happens without python-multipart installed?" — FastAPI raises an error at startup telling you to install it, because it cannot parse forms.
- "Are form fields limited in size?" — Yes. Starlette limits each non-file field to 1 MB by default and the number of fields to 1,000, which protects the server from huge form bodies.