FastAPI File Uploads

Learn to handle file uploads in FastAPI using python-multipart. Step-by-step guide with practical examples, troubleshooting, and next steps.

Focus: file uploads with fastapi and python-multipart

Sponsored

You've built endpoints that accept JSON, but what happens when your users need to send a resume, a profile photo, or a CSV for processing? Handling multipart/form-data requests can seem daunting — boundary strings, raw bytes, and parsing pitfalls. Fortunately, FastAPI and python-multipart make file uploads clean, type-safe, and surprisingly simple. This lesson gives you the exact pattern to accept files, validate them, and save them to disk, so you can stop fearing uploads and start building real-world features.

The problem this lesson solves

Modern web applications live on data, and not all of it arrives as JSON. Think about a user uploading an avatar, a CRM accepting CSV imports, or an API receiving PDFs for document processing. These are all file uploads, and they use a different HTTP encoding: multipart/form-data. As a backend developer, you need a reliable way to accept, validate, and store these files.

Without a proper approach, you'll face several painful problems:

  • Raw byte handling: Manually parsing the multipart body is complex and error-prone.
  • Security vulnerabilities: Uploading malicious files could compromise your server.
  • Poor user experience: Unclear error messages when the wrong file type or size comes in.

FastAPI, combined with the python-multipart library, solves these issues natively. It gives you automatic parsing, type validation, and clean dependency injection — so you can focus on the business logic, not the protocol. This lesson is step 24 in your FastAPI Backend Development track, and it's the foundation for later lessons on authentication, databases, and deployment.

Core concept / mental model

Think of multipart/form-data as a buffet table rather than a single plated meal. In a JSON request, you have one tidy payload. In multipart, the client sends multiple "dishes" (fields and files), each with its own label or "name". The browser or HTTP client automatically wraps these dishes with boundary strings — random separators that tell the server where one part ends and another begins.

FastAPI acts as the maître d'. It reads the boundary, splits the parts, and hands you each dish on a silver platter: a UploadFile object for files, or a regular str for form fields. You don't need to read the raw request body or handle the boundary yourself.

python-multipart is the kitchen staff. It's a small library that actually parses the multipart stream. While FastAPI can work without it for simple form fields, it is required for file uploads. When you install it, FastAPI automatically detects it and enables the File and Form features.

Here's the mental model in one sentence:

The client sends a multipart/form-data request. FastAPI parses it with python-multipart and exposes each part as a typed Python object — UploadFile for files, str for fields.

The key concept to remember is that UploadFile is a file-like object. It wraps the raw uploaded data and provides a file attribute (a Python file object), a filename, and a content_type. You can read its contents as bytes or write them directly to disk.

How it works step by step

Let's trace the journey of a file from the client to your server.

  1. Client sends the request: The client (a browser, a mobile app, or another server) constructs a multipart/form-data request. It includes the file data, a filename, and optional form fields.

  2. FastAPI receives the request: The web server (like Uvicorn) passes the raw request to FastAPI.

  3. Python-multipart parses the body: FastAPI detects the multipart/form-data content type and delegates to python-multipart. It splits the body by the boundary and extracts each part.

  4. FastAPI constructs typed objects: For each field, FastAPI creates a str or int. For each file, it creates an UploadFile instance with the filename, content type, and a file pointer. It also stores the raw bytes in a temporary file or in memory, depending on size.

  5. Your endpoint logic runs: Your function receives the UploadFile object. You can read its contents, validate its size or type, and save it to a permanent location.

  6. You return a response: After processing, you send back JSON with a success message or a file URL.

The critical step is number 3: without python-multipart, FastAPI will raise an error when you use File in an endpoint. It's a small dependency that does heavy lifting.

Hands-on walkthrough

Now let's get practical. We'll build a simple FastAPI app that accepts file uploads, saves them to disk, and returns the file's metadata. We'll also explore reading file content and adding optional fields.

Setup and installation

First, install FastAPI, Uvicorn, and python-multipart:

pip install fastapi uvicorn python-multipart

Pro tip: python-multipart is a lightweight library (~1KB) with no native dependencies. It's safe to add to any project.

Basic file upload endpoint

Create a file named main.py:

from fastapi import FastAPI, UploadFile, File

app = FastAPI()

@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
    contents = await file.read()
    return {
        "filename": file.filename,
        "content_type": file.content_type,
        "size": len(contents),
    }

Run with uvicorn main:app --reload, then test using the auto-generated docs at /docs, or with curl:

curl -F "file=@/path/to/your/file.txt" http://127.0.0.1:8000/upload/

Expected output:

{
  "filename": "file.txt",
  "content_type": "text/plain",
  "size": 12
}

Notice that file.read() returns the full contents as bytes. For large files, this works fine because FastAPI spools to disk automatically, but you can also stream to avoid loading everything into memory.

Saving the file to a permanent location

Often you'll want to store the file. Here's a robust example that saves using the original filename, with a check to avoid path traversal:

import os
from fastapi import FastAPI, UploadFile, File

app = FastAPI()
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)

@app.post("/save/")
async def save_file(file: UploadFile = File(...)):
    # Sanitize the filename to avoid directory traversal
    safe_filename = os.path.basename(file.filename)
    file_path = os.path.join(UPLOAD_DIR, safe_filename)

    with open(file_path, "wb") as buffer:
        # Read the file in chunks to handle large uploads efficiently
        while chunk := await file.read(1024 * 1024):  # 1 MB chunk
            buffer.write(chunk)

    return {"saved": file_path, "size": os.path.getsize(file_path)}

Test with:

curl -F "file=@/etc/hostname" http://127.0.0.1:8000/save/

The while chunk := await file.read(...) pattern streams the file in 1 MB chunks, so memory stays flat even for gigabytes of data.

Adding form fields and validation

Real uploads often come with extra data, like a description or a category. You can merge File and Form and add your own validation:

from fastapi import FastAPI, UploadFile, File, Form, HTTPException

app = FastAPI()

@app.post("/upload_with_desc/")
async def upload_with_desc(
    file: UploadFile = File(...),
    description: str = Form("..."),
):
    # Validate file type
    if file.content_type not in ["image/jpeg", "image/png"]:
        raise HTTPException(400, detail="Only JPEG or PNG allowed")

    contents = await file.read()
    return {
        "filename": file.filename,
        "description": description,
        "size": len(contents),
    }

The Form(...) parameter ensures that a description field is present in the multipart body. If it's missing, FastAPI returns a 422 validation error automatically.

Pro tip: Use Form(None) to make a field optional.

Compare options / when to choose what

FastAPI offers several ways to handle uploads. Here's a comparison to help you choose the right tool.

Approach Best for Pros Cons
UploadFile General uploads (images, PDFs, any file) Streaming, automatic temp file, works with files of any size Requires python-multipart
bytes with File() Small files (< a few MB) Simple, loads directly into memory Bad for large files, uses memory
Form fields Text inputs alongside files Standard, works with UploadFile Not for actual files

Recommendations:

  • Default to UploadFile — it's the most flexible and efficient.
  • Use bytes only for tiny, in-memory operations like processing CSV content on the fly.
  • Always use Form for extra data, not Query or Body — they don't work with multipart.

For storing files, you have further choices:

  • Local disk: Fast and simple, but doesn't scale across multiple servers.
  • Cloud storage (S3, GCS): Scalable and durable, but requires extra SDKs and configuration.
  • Database (BLOB): Good for small files, but hurts performance for large ones.

This decision affects the next lessons in this track, especially when we discuss deployment.

Troubleshooting & edge cases

Even with a clean library, you'll hit issues. Here are the most common ones and how to fix them.

"python-multipart is required" error

ImportError: Form data requires "python-multipart" to be installed.

Cause: You didn't install python-multipart. Fix: pip install python-multipart and restart the server.

Filename path traversal

If a malicious user sends a filename like ../../etc/passwd, you could overwrite system files.

# Bad — vulnerable
file_path = os.path.join("uploads", file.filename)

# Good — sanitize the name
safe_filename = os.path.basename(file.filename)
file_path = os.path.join("uploads", safe_filename)

Always sanitize the filename. os.path.basename strips any directory components.

Reading an empty file

contents = await file.read()
if not contents:
    # Handle empty upload

An empty file will return b''. Validate before saving.

Large files and memory

Avoid await file.read() for big files — it loads the entire file into memory. Use chunked reading as shown earlier. FastAPI spools to disk automatically, but you still need to stream to disk to avoid bloat.

Form fields not recognized

If you declare Form fields without python-multipart, you'll get an error. Also, remember that Query or Body won't work — use Form.

What you learned & what's next

You've mastered the core of file uploads with FastAPI and Python-Multipart. You can now accept files securely, save them to disk, validate content types, and handle large files efficiently. You also know how to combine file uploads with form fields for richer endpoints.

With this skill, you've covered a crucial part of real-world API design. The next lesson in this track will build on this foundation — likely covering background tasks to process uploaded files asynchronously or authentication to protect upload endpoints. You're one step closer to building production-ready FastAPI applications.

Keep this lesson as a reference when you implement your next upload feature, and remember: multipart is not scary when you have the right toolkit.

Practice recap

Try building a small endpoint that accepts a profile photo and saves it to an uploads/ directory with a unique name (e.g., using UUID). Then, extend it to also accept a description form field. Use curl -F to test both success and failure cases, like an incorrect content type. This hands-on exercise will cement the streaming and validation techniques you just learned.

Common mistakes

  • Forgetting to install python-multipart — FastAPI raises an ImportError when you use File or Form.
  • Using await file.read() on large files, loading everything into memory and crashing the server.
  • Not sanitizing the uploaded filename with os.path.basename — leaving your app vulnerable to directory traversal attacks.
  • Using Query or Body for extra form data — they don't work with multipart; you must use Form.

Variations

  1. Using bytes instead of UploadFile for tiny files — loads content directly into memory with simple code.
  2. Storing files in cloud storage (S3, GCS) instead of local disk for scalability.
  3. Validating file type and size manually before processing — combine with Pydantic models for structured metadata.

Real-world use cases

  • User profile endpoints that accept avatar image uploads, validate MIME type, and save to a CDN.
  • CSV or Excel import features in a data dashboard that parse uploaded files for analysis.
  • Document management systems that accept PDFs or DOCs and store them securely for later retrieval.

Key takeaways

  • File uploads use multipart/form-data encoding, and FastAPI requires python-multipart to parse them.
  • UploadFile is the idiomatic way to accept files — it supports streaming and gives you metadata like filename and content type.
  • Always sanitize filenames with os.path.basename to prevent path traversal attacks.
  • Stream large files in chunks with while chunk := await file.read(...) to keep memory usage low.
  • Combine File and Form to accept files alongside regular text fields, with automatic validation.
  • Test your endpoints with the auto-generated docs or curl -F to verify behavior.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.