Python & Backend
Upgrading to FastAPI 0.150: empty file uploads and recent changes
FastAPI 0.150.0 treats unselected browser file inputs as missing. Learn how to update file upload endpoints, form models, and tests.
By Tanvir Ahamed, published . 8 min read.
Researched and written with AI assistance; sources are linked below.
When a user submits an HTML form without selecting a file in a file input, browsers still send a multipart form part with an empty filename and empty content (FastAPI 0.150.0 release notes). Before FastAPI 0.150.0, FastAPI parsed that empty part into an UploadFile object with filename="" or empty bytes (b""), forcing endpoints to manually inspect filenames to know whether a file was actually uploaded (FastAPI 0.150.0 release notes). FastAPI 0.150.0 fixes this by treating multipart file parts with both an empty filename and empty content as missing (FastAPI 0.150.0 release notes). This guide walks through removing legacy filename checks in file upload endpoints, reviewing the related form and parameter changes from FastAPI 0.147.0 through 0.149.0, and updating TestClient suites.
Why browser file inputs caused validation workarounds before 0.150.0
In standard HTML forms using enctype="multipart/form-data", an <input type="file" name="file"> element does not omit its field when left blank by the user (FastAPI 0.150.0 release notes). Instead, the browser transmits a multipart segment where the filename attribute is "" and the body is zero bytes (b"") (FastAPI 0.150.0 release notes).
Prior to 0.150.0, FastAPI converted this empty segment directly into a valid parameter value (FastAPI 0.150.0 release notes). That created two problems for backend handlers:
- Required file parameters (
file: UploadFileorfile: Annotated[bytes, File()]) passed request validation even when the user never chose a file, because FastAPI saw anUploadFile(filename="")instance orb""rather than a missing field (FastAPI 0.150.0 release notes). - Optional file parameters (
file: UploadFile | None = None) received anUploadFileobject with an empty filename instead ofNonewhen submitted from a browser form (FastAPI 0.150.0 release notes).
To prevent empty uploads from reaching storage or parser logic, endpoints had to check if not file.filename: and manually raise an HTTP error or treat the object as absent (FastAPI 0.150.0 release notes, FastAPI Request Files). Remember that handling forms and uploaded files in FastAPI requires python-multipart (uv add python-multipart or pip install python-multipart) (FastAPI Request Files).
Here is what a defensive endpoint looked like before FastAPI 0.150.0:
from fastapi import FastAPI, HTTPException, UploadFile
app = FastAPI()
@app.post("/upload-required/")
async def upload_required_legacy(file: UploadFile):
if not file.filename:
raise HTTPException(status_code=422, detail="File was not selected")
content = await file.read()
return {"filename": file.filename, "size": len(content)}
@app.post("/upload-optional/")
async def upload_optional_legacy(file: UploadFile | None = None):
if file is None or not file.filename:
return {"message": "No upload file sent"}
return {"filename": file.filename}How FastAPI 0.150.0 handles empty file parts
Released in October 2026, FastAPI 0.150.0 treats any multipart file part that has both an empty filename (filename="") and empty content (b"") as missing (FastAPI 0.150.0 release notes). This affects four distinct parameter configurations (FastAPI 0.150.0 release notes):
- Required file fields: Endpoints declaring
file: UploadFileorfile: Annotated[bytes, File()]now reject unselected browser file inputs automatically with a422 Unprocessable Entityvalidation error (FastAPI 0.150.0 release notes). - Optional file fields and defaults: Endpoints declaring
file: UploadFile | None = None, or fields configured withdefaultordefault_factoryinFile(...)or Pydantic form models, receive their configured default value when the file input is left unselected (FastAPI 0.150.0 release notes). In form models, applying a default does not mark the field as explicitly set, so callingmodel_dump(exclude_unset=True)excludes the field (FastAPI Release Notes). - Lists of files: When accepting
files: list[UploadFile]orfiles: Annotated[list[bytes], File()], unselected file inputs inside the list are ignored and filtered out instead of producing a single-element list containingUploadFile(filename="")orb""(FastAPI 0.150.0 release notes). Whether a requiredlist[UploadFile]that receives only unselected inputs resolves to an empty list[]or returns a422validation error is unverified in the release notes. - Named zero-byte files: If a user uploads a real, empty file that has a filename, such as
empty.txtcontaining0bytes (filename="empty.txt", contentb""), FastAPI preserves it as a valid upload because the filename is not empty (FastAPI 0.150.0 release notes).
Updating single and optional file upload endpoints
With FastAPI 0.150.0, you can remove manual file.filename checks for unselected inputs and rely on standard type annotations (FastAPI 0.150.0 release notes, FastAPI Request Files). The following example shows the updated handlers for both bytes and UploadFile parameters (FastAPI Request Files, UploadFile reference):
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/required/")
async def create_required_file(file: UploadFile):
content = await file.read()
return {"filename": file.filename, "size": len(content)}
@app.post("/files/optional-bytes/")
async def create_optional_file(
file: Annotated[bytes | None, File()] = None,
):
if file is None:
return {"message": "No file sent"}
return {"file_size": len(file)}
@app.post("/uploadfile/optional/")
async def create_optional_upload_file(
file: UploadFile | None = None,
):
if file is None:
return {"message": "No upload file sent"}
return {"filename": file.filename}Notice the if file is None: check in create_optional_file. In FastAPI's official tutorial, optional bytes parameters often use if not file: (FastAPI Request Files). However, in Python, b"" is falsy. If a client uploads a named zero-byte file (filename="empty.txt", content b""), FastAPI 0.150.0 preserves it and passes b"" to file: Annotated[bytes | None, File()] = None (FastAPI 0.150.0 release notes). Checking if file is None: distinguishes an omitted or unselected file (None) from a named zero-byte file (b"").
If you group multipart fields using Pydantic form models (supported since FastAPI 0.113.0), optional file fields also stay excluded from model_dump(exclude_unset=True) when left unselected in a browser form (FastAPI Form Models, FastAPI Release Notes, FastAPI 0.150.0 release notes).
Handling multi-file uploads and zero-byte edge cases
When an HTML form includes <input type="file" name="files" multiple> or multiple file inputs sharing the same name, browsers still send an empty part if the user submits the form without choosing files (FastAPI 0.150.0 release notes, FastAPI Request Files). In FastAPI 0.150.0, unselected inputs inside list[UploadFile] or Annotated[list[bytes], File()] are filtered out (FastAPI 0.150.0 release notes).
This means your loop over uploaded files no longer needs to skip items where upload.filename == "" (FastAPI 0.150.0 release notes):
from fastapi import FastAPI, UploadFile
app = FastAPI()
@app.post("/batch-upload/")
async def upload_multiple_files(files: list[UploadFile]):
processed = []
for upload in files:
data = await upload.read()
processed.append({
"filename": upload.filename,
"size": len(data),
})
return {"files": processed}If a client submits two real files (including a zero-byte file named placeholder.txt) alongside an unselected input part, FastAPI filters out the unselected part and passes only the two named UploadFile instances to upload_multiple_files (FastAPI 0.150.0 release notes).
Other changes from FastAPI 0.147.0 to 0.149.0
If you are upgrading from FastAPI 0.146.0 (which deprecated the root_path parameter in FastAPI() in favor of fastapi run --root-path) or 0.145.0 (which removed the deprecated openapi_prefix and unused **extra keyword arguments from FastAPI()), three intermediate releases on October 19, 2026 also changed how forms and defaults are validated (FastAPI 0.146.0 release notes, FastAPI Release Notes). Reviewing them together prevents surprises during a single version bump.
FastAPI 0.147.0: omitted boolean form fields default to False
Standard HTML forms omit unchecked <input type="checkbox"> elements entirely from the submitted payload (FastAPI 0.147.0 release notes, FastAPI Request Forms). FastAPI 0.147.0 interprets omitted bool and bool | None form fields as False (FastAPI 0.147.0 release notes).
- This applies to both standalone
Formparameters and Pydantic form models (Annotated[MyFormModel, Form()]), whether the field is required or defines a default ofTrueorNone(FastAPI 0.147.0 release notes). - The inferred
Falsevalue is treated as explicitly set, so callingmodel_dump(exclude_unset=True)on a form model includes the boolean field (FastAPI 0.147.0 release notes). - JSON request bodies and direct Pydantic validation are unaffected (FastAPI 0.147.0 release notes).
FastAPI 0.148.0: default handling for parameters and parameter models
FastAPI 0.148.0 stops treating default values on omitted Query, Header, Cookie, and Form parameters (and parameter model fields) as raw user input (FastAPI 0.148.0 release notes):
- Default values are no longer passed through Pydantic validators unless you explicitly opt in with
validate_default=True, such asField(validate_default=True)on a model field orQuery(validate_default=True)on a parameter (FastAPI 0.148.0 release notes).Body(validate_default=True)is now honored as well (FastAPI 0.148.0 release notes). - Applying a default value on an omitted field in a parameter model no longer marks that field as set, meaning
model_dump(exclude_unset=True)excludes it (FastAPI 0.148.0 release notes). Omitted boolean form fields remain an exception and still count as explicitly set toFalse(FastAPI 0.148.0 release notes). - A
default_factorynow runs once per omitted field, and validation error responses no longer leak injected defaults inside the errorinputfield (FastAPI 0.148.0 release notes).
FastAPI 0.149.0: empty scalar values in form models treated as missing
Browsers submit empty text inputs (<input type="text">) as empty strings ("") (FastAPI 0.149.0 release notes). FastAPI 0.149.0 treats empty scalar values ("") in Pydantic form models as missing, aligning form models with standalone Form parameters (FastAPI 0.149.0 release notes):
- Required scalar fields in a form model now fail validation with
422when submitted as""(FastAPI 0.149.0 release notes). - Optional scalar fields in a form model fall back to their
defaultordefault_factorywhen submitted as""and stay unset inmodel_dump(exclude_unset=True)(FastAPI 0.149.0 release notes). - Empty strings inside list fields (
list[str]) are preserved (FastAPI 0.149.0 release notes).
Here is a complete example combining a Pydantic form model with an optional UploadFile and a boolean checkbox under the 0.147.0–0.150.0 rules (FastAPI Form Models, FastAPI Release Notes, FastAPI 0.150.0 release notes):
from typing import Annotated
from fastapi import FastAPI, Form, UploadFile
from pydantic import BaseModel
app = FastAPI()
class ProfileForm(BaseModel):
username: str
bio: str = "No bio provided"
subscribe_newsletter: bool = True
avatar: UploadFile | None = None
@app.post("/profile/")
async def update_profile(form: Annotated[ProfileForm, Form()]):
unset_excluded = form.model_dump(exclude_unset=True, exclude={"avatar"})
return {
"username": form.username,
"bio": form.bio,
"subscribe_newsletter": form.subscribe_newsletter,
"has_avatar": form.avatar is not None,
"explicitly_set_fields": list(unset_excluded.keys()),
}If a browser submits username="alice", bio="", omits the subscribe_newsletter checkbox, and leaves the avatar file input unselected:
usernameis"alice".biofalls back to"No bio provided"and is excluded fromexplicitly_set_fields(0.148.0 and 0.149.0) (FastAPI 0.148.0 release notes, FastAPI 0.149.0 release notes).subscribe_newsletterbecomesFalse(overriding theTruedefault because an omitted checkbox means unchecked) and is included inexplicitly_set_fields(0.147.0) (FastAPI 0.147.0 release notes).avatarbecomesNoneand is excluded frommodel_dump(exclude_unset=True)(0.150.0) (FastAPI 0.150.0 release notes).
Updating your test suite for 0.150.0
When testing file upload endpoints with FastAPI's TestClient (FastAPI Testing, TestClient reference), multipart file tuples passed via files={"file": ("", b"")} may behave differently after upgrading to 0.150.0 (FastAPI 0.150.0 release notes):
- Simulating an unselected browser input: Sending
files={"file": ("", b"")}to a requiredUploadFileendpoint now returns422 Unprocessable Entityfrom FastAPI's validation layer (FastAPI 0.150.0 release notes). If your test previously asserted a200 OKor a custom400 Bad Requestraised by your own handler, update the assertion to expect422(FastAPI 0.150.0 release notes). - Testing intentional zero-byte file uploads: If a test used
files={"file": ("", b"")}as shorthand to test how your endpoint handles an empty file, FastAPI will now reject it as an unselected input (FastAPI 0.150.0 release notes). Provide a non-empty filename in the tuple, such asfiles={"file": ("empty.txt", b"")}, so FastAPI recognizes it as a named zero-byte file (FastAPI 0.150.0 release notes).
Here is a runnable test suite verifying all three cases with TestClient (FastAPI Testing, FastAPI 0.150.0 release notes):
from fastapi import FastAPI, UploadFile
from fastapi.testclient import TestClient
app = FastAPI()
@app.post("/upload/")
async def upload_file(file: UploadFile):
content = await file.read()
return {"filename": file.filename, "size": len(content)}
@app.post("/upload-optional/")
async def upload_optional(file: UploadFile | None = None):
if file is None:
return {"uploaded": False}
content = await file.read()
return {"uploaded": True, "filename": file.filename, "size": len(content)}
client = TestClient(app)
def test_unselected_file_on_required_endpoint_returns_422():
response = client.post("/upload/", files={"file": ("", b"")})
assert response.status_code == 422
def test_named_zero_byte_file_succeeds():
response = client.post("/upload/", files={"file": ("empty.txt", b"")})
assert response.status_code == 200
assert response.json() == {"filename": "empty.txt", "size": 0}
def test_unselected_file_on_optional_endpoint_resolves_to_none():
response = client.post("/upload-optional/", files={"file": ("", b"")})
assert response.status_code == 200
assert response.json() == {"uploaded": False}To complete your upgrade to FastAPI 0.150.0, search your codebase for .filename checks inside route handlers and ("", b"") tuples in your test files, remove the manual workarounds, and run your test suite against the updated validation rules.
Sources
- FastAPI
- Python
- API
- Backend
Related articles
Upgrading to FastAPI 0.146: root_path deprecation and CLI changes
FastAPI 0.146 deprecates FastAPI(root_path=...). Move subpath configuration to the CLI or ASGI server and update your proxy settings and tests.
Python & Backend, 7 min read