In the last post, Response Status Code, we set the number that rides with a response. Every request body we have read so far arrived as JSON. But classic HTML forms, the kind behind a login page, do not send JSON. They send form-encoded data. FastAPI has a dedicated tool for that: Form.
Reading form fields
Import Form from fastapi and use it inside Annotated, the same shape as Body and Query.
from typing import Annotatedfrom fastapi import FastAPI, Formapp = FastAPI()@app.post("/login")async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): return {"username": username}Let's review:
Form()tells FastAPI to read each field from form-encoded data, not from JSON.Without it, FastAPI would treat
usernameas a query parameter or a JSON body field. The marker is required.Formaccepts the same extras asBody, like validation and an alias, since it inherits fromBody.
One setup note: forms need the python-multipart package. Install it once with pip install python-multipart, or FastAPI will remind you.
Why a separate tool
HTML forms encode their data differently from JSON, using a media type called application/x-www-form-urlencoded. Form simply tells FastAPI to look in that spot. This is also exactly how the OAuth2 password flow expects a username and password, so you will see Form again when you reach security.
⚠️You cannot mix Form fields and a JSON Body in the same endpoint. A request body is encoded one way or the other, form or JSON, never both at once. That is an HTTP rule, not a FastAPI limit. Pick one per endpoint. 😁
