Skip to content
Tanvir Ahamed

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:

  1. Required file parameters (file: UploadFile or file: Annotated[bytes, File()]) passed request validation even when the user never chose a file, because FastAPI saw an UploadFile(filename="") instance or b"" rather than a missing field (FastAPI 0.150.0 release notes).
  2. Optional file parameters (file: UploadFile | None = None) received an UploadFile object with an empty filename instead of None when 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: UploadFile or file: Annotated[bytes, File()] now reject unselected browser file inputs automatically with a 422 Unprocessable Entity validation error (FastAPI 0.150.0 release notes).
  • Optional file fields and defaults: Endpoints declaring file: UploadFile | None = None, or fields configured with default or default_factory in File(...) 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 calling model_dump(exclude_unset=True) excludes the field (FastAPI Release Notes).
  • Lists of files: When accepting files: list[UploadFile] or files: Annotated[list[bytes], File()], unselected file inputs inside the list are ignored and filtered out instead of producing a single-element list containing UploadFile(filename="") or b"" (FastAPI 0.150.0 release notes). Whether a required list[UploadFile] that receives only unselected inputs resolves to an empty list [] or returns a 422 validation 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.txt containing 0 bytes (filename="empty.txt", content b""), 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 Form parameters and Pydantic form models (Annotated[MyFormModel, Form()]), whether the field is required or defines a default of True or None (FastAPI 0.147.0 release notes).
  • The inferred False value is treated as explicitly set, so calling model_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 as Field(validate_default=True) on a model field or Query(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 to False (FastAPI 0.148.0 release notes).
  • A default_factory now runs once per omitted field, and validation error responses no longer leak injected defaults inside the error input field (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):

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:

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

  1. Simulating an unselected browser input: Sending files={"file": ("", b"")} to a required UploadFile endpoint now returns 422 Unprocessable Entity from FastAPI's validation layer (FastAPI 0.150.0 release notes). If your test previously asserted a 200 OK or a custom 400 Bad Request raised by your own handler, update the assertion to expect 422 (FastAPI 0.150.0 release notes).
  2. 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 as files={"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