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
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-datarequest. FastAPI parses it with python-multipart and exposes each part as a typed Python object —UploadFilefor files,strfor 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.
-
Client sends the request: The client (a browser, a mobile app, or another server) constructs a
multipart/form-datarequest. It includes the file data, a filename, and optional form fields. -
FastAPI receives the request: The web server (like Uvicorn) passes the raw request to FastAPI.
-
Python-multipart parses the body: FastAPI detects the
multipart/form-datacontent type and delegates to python-multipart. It splits the body by the boundary and extracts each part. -
FastAPI constructs typed objects: For each field, FastAPI creates a
strorint. For each file, it creates anUploadFileinstance 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. -
Your endpoint logic runs: Your function receives the
UploadFileobject. You can read its contents, validate its size or type, and save it to a permanent location. -
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
bytesonly for tiny, in-memory operations like processing CSV content on the fly. - Always use
Formfor extra data, notQueryorBody— 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
QueryorBodyfor extra form data — they don't work with multipart; you must useForm.
Variations
- Using
bytesinstead ofUploadFilefor tiny files — loads content directly into memory with simple code. - Storing files in cloud storage (S3, GCS) instead of local disk for scalability.
- 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-dataencoding, and FastAPI requires python-multipart to parse them. UploadFileis the idiomatic way to accept files — it supports streaming and gives you metadata like filename and content type.- Always sanitize filenames with
os.path.basenameto prevent path traversal attacks. - Stream large files in chunks with
while chunk := await file.read(...)to keep memory usage low. - Combine
FileandFormto accept files alongside regular text fields, with automatic validation. - Test your endpoints with the auto-generated docs or
curl -Fto verify behavior.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.