File & Image Uploads
Reviewed & published by Brayan K
A file upload is the process of accepting a file from a visitor's browser, writing its bytes to disk under your media folder, and saving the resulting path in the database.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll add a FileField or ImageField to a model, configure MEDIA_ROOT and MEDIA_URL, read the upload from request.FILES, control where files land with upload_to, and serve the media back during development.
What You'll Learn in This Lesson
- Add FileField and ImageField to a model
- Configure MEDIA_ROOT and MEDIA_URL
- Read uploads from request.FILES in a view
- Bind a form with request.POST and request.FILES
- Control storage paths with upload_to
- Serve uploaded media in development
1 FileField, ImageField, and the Media Settings
A FileField stores a path in the database and writes the bytes to disk. An ImageField does the same but also confirms the upload is a real image. Both need two settings: MEDIA_ROOT (where files are written) and MEDIA_URL (the URL prefix they're served under).
# settings.py
import os
MEDIA_ROOT = os.path.join(BASE_DIR, "media") # absolute path on disk
MEDIA_URL = "/media/" # public URL prefix
# profiles/models.py
from django.db import models
class Profile(models.Model):
name = models.CharField(max_length=100)
# upload_to is a subfolder INSIDE media/
avatar = models.ImageField(upload_to="avatars/") # needs Pillow
resume = models.FileField(upload_to="resumes/", blank=True)# An uploaded file arrives in request.FILES. Each one carries a name, a size,
# and a content type. Saving it means choosing where it goes under MEDIA_ROOT
# and recording the relative path the database will store.
import os
MEDIA_ROOT = "/srv/app/media"
MEDIA_URL = "/media/"
class UploadedFile:
def __init__(self, name, size, content_type):
self.name = name
self.size = size
self.content_type = content_type
def save_upload(uploaded, upload_to):
# upload_to is the FileField's subfolder, e.g. "avatars/"
relative_path = os.path.join(upload_to, uploaded.name)
absolute_path = os.path.join(MEDIA_ROOT, relative_path)
# In real Django the bytes are written to absolute_path here.
return relative_path, absolute_path
photo = UploadedFile("ada.png", size=20480, content_type="image/png")
rel, abs_path = save_upload(photo, upload_to="avatars")
print("stored in DB:", rel)
print("written to disk:", abs_path)
print("served at URL:", MEDIA_URL + rel)
print("size (KB):", photo.size // 1024)
# EXPECTED:
# stored in DB: avatars/ada.png
# written to disk: /srv/app/media/avatars/ada.png
# served at URL: /media/avatars/ada.png
# size (KB): 20
# ✅ Expected output:
# stored in DB: avatars/ada.png
# written to disk: /srv/app/media/avatars/ada.png
# served at URL: /media/avatars/ada.png
# size (KB): 202 Handling the Upload in a Form and View
Files don't arrive in request.POST — they come in request.FILES. To handle them you bind your form to both, and the HTML form must declare enctype="multipart/form-data". Once form.is_valid() passes, form.save() writes the bytes and the row together.
# profiles/forms.py
from django import forms
from .models import Profile
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ["name", "avatar", "resume"]
# profiles/views.py
def upload_profile(request):
if request.method == "POST":
# Bind BOTH POST (text) and FILES (uploads)
form = ProfileForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect("profile_done")
else:
form = ProfileForm()
return render(request, "profiles/upload.html", {"form": form})<!-- profiles/templates/profiles/upload.html -->
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Upload</button>
</form># A form that handles an upload must be bound to BOTH request.POST (the text
# fields) and request.FILES (the file fields). is_valid() runs validation;
# only then do we save. We simulate that validation gate here.
def handle_upload(post_data, files_data):
# Mirror: form = ProfileForm(request.POST, request.FILES)
bound_post = dict(post_data)
bound_files = dict(files_data)
# A simple validity check: a caption is required and a file must be present
errors = []
if not bound_post.get("caption"):
errors.append("caption is required")
if "photo" not in bound_files:
errors.append("photo is required")
if errors:
return {"valid": False, "errors": errors}
# form.save() would write the row + file; we return what gets stored
return {
"valid": True,
"caption": bound_post["caption"],
"photo_path": "avatars/" + bound_files["photo"],
}
ok = handle_upload({"caption": "My dog"}, {"photo": "rex.jpg"})
bad = handle_upload({"caption": ""}, {})
print("valid upload:", ok)
print("invalid upload:", bad)
# EXPECTED:
# valid upload: {'valid': True, 'caption': 'My dog', 'photo_path': 'avatars/rex.jpg'}
# invalid upload: {'valid': False, 'errors': ['caption is required', 'photo is required']}
# ✅ Expected output:
# valid upload: {'valid': True, 'caption': 'My dog', 'photo_path': 'avatars/rex.jpg'}
# invalid upload: {'valid': False, 'errors': ['caption is required', 'photo is required']}3 Dynamic upload_to and Serving Media in Dev
upload_to can be a fixed string folder or a function that returns a path per file. A function lets you bucket uploads by user and date, which keeps directories small and avoids name collisions. Django calls it with the model instance and the original filename.
# profiles/models.py
def user_directory_path(instance, filename):
# files go to media/uploads/<username>/<year>/<month>/<day>/<filename>
return f"uploads/{instance.user.username}/%Y/%m/%d/{filename}"
class Document(models.Model):
user = models.ForeignKey("auth.User", on_delete=models.CASCADE)
file = models.FileField(upload_to=user_directory_path)In development you must wire up media serving yourself. Add a static() mapping in the project urls.py, guarded by DEBUG.
# project/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ... your app routes ...
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)# upload_to can be a string OR a function that returns a path per file.
# A dated path keeps folders small and avoids name clashes. Django passes
# the model instance and the original filename to that function.
import datetime
def user_directory_path(instance_username, filename):
# Real signature: upload_to=fn(instance, filename) -> str
today = datetime.date(2026, 6, 18) # fixed date so output is stable
return f"uploads/{instance_username}/{today:%Y/%m/%d}/{filename}"
print(user_directory_path("ada", "report.pdf"))
print(user_directory_path("ben", "photo.png"))
# A string upload_to is simpler — just a fixed subfolder:
def fixed_upload_to(subfolder, filename):
return f"{subfolder}/{filename}"
print(fixed_upload_to("documents", "notes.txt"))
# EXPECTED:
# uploads/ada/2026/06/18/report.pdf
# uploads/ben/2026/06/18/photo.png
# documents/notes.txt
# ✅ Expected output:
# uploads/ada/2026/06/18/report.pdf
# uploads/ben/2026/06/18/photo.png
# documents/notes.txt🎯 Your Turn: Build the Public URL
Django serves an uploaded file at MEDIA_URL joined with its relative path. Fill in the blank with the standard media URL prefix.
# 🎯 YOUR TURN: build the public URL Django serves an uploaded file at.
# The URL is MEDIA_URL joined with the file's relative path.
# Replace ___ with the MEDIA_URL prefix.
MEDIA_URL = "___"
relative_path = "avatars/ada.png"
public_url = MEDIA_URL + relative_path
print(public_url)
# EXPECTED OUTPUT:
# /media/avatars/ada.pngCommon Errors (And How to Fix Them)
❌ The file never arrives (request.FILES is empty)
The form tag is missing enctype, or you bound only request.POST.
✅ Fix: add enctype="multipart/form-data" and build Form(request.POST, request.FILES).
❌ Cannot use ImageField because Pillow is not installed
ImageField validates images using the Pillow library, which isn't bundled with Django.
✅ Fix: install it with pip install Pillow, then run your migrations.
❌ Uploaded images return 404 in development
The dev server doesn't serve MEDIA_URL unless you tell it to.
✅ Fix: append static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) in urls.py under if settings.DEBUG.
📋 Quick Reference
| Item | Role |
|---|---|
| MEDIA_ROOT | Absolute disk folder for uploads |
| MEDIA_URL | Public URL prefix for served media |
| upload_to | Subfolder (string or function) per field |
| request.FILES | Where uploaded files arrive in a view |
| profile.avatar.url | The full public URL of a stored file |
🏆 Mini Challenge: Validate an Image Upload
Write a validator that rejects files that are too big or have a disallowed extension, then returns the path the file would be saved to — exactly the kind of check you'd add in a form's clean_avatar().
# 🏆 Mini Challenge: validate an image upload before saving it.
# Reject files that are too big or have a disallowed extension, then
# build the path that would be stored on the FileField.
import os
ALLOWED = {".jpg", ".jpeg", ".png", ".gif"}
MAX_BYTES = 2 * 1024 * 1024 # 2 MB
def validate_and_path(filename, size, upload_to="images"):
ext = os.path.splitext(filename)[1].lower()
if ext not in ALLOWED:
return f"Rejected: {ext} is not an allowed image type"
if size > MAX_BYTES:
return f"Rejected: {size} bytes is over the 2MB limit"
return "Saved to " + os.path.join(upload_to, filename)
print(validate_and_path("cat.png", 500000))
print(validate_and_path("virus.exe", 1000))
print(validate_and_path("huge.jpg", 5 * 1024 * 1024))
# EXPECTED:
# Saved to images/cat.png
# Rejected: .exe is not an allowed image type
# Rejected: 5242880 bytes is over the 2MB limit❓ Frequently Asked Questions
Lesson complete — your app accepts uploads!
You can add a FileField or ImageField, set MEDIA_ROOT and MEDIA_URL, bind a form to request.POST and request.FILES, route files with upload_to, and serve media in development.
Practice quiz
Which model field stores an uploaded document like a PDF?
- TextField
- FileField
- CharField
- BinaryField
Answer: FileField. FileField stores the file on disk and saves its path in the database.
What does ImageField add over FileField?
- Automatic thumbnails
- Image validation plus width and height attributes
- Cloud storage
- Compression
Answer: Image validation plus width and height attributes. ImageField validates the upload is a real image and exposes width and height; it needs Pillow.
Which library must be installed to use ImageField?
- NumPy
- requests
- Pillow
- OpenCV
Answer: Pillow. ImageField requires the Pillow imaging library.
What does MEDIA_ROOT define?
- The public URL prefix for media
- A database table name
- The CDN domain
- The filesystem path where uploaded files are written
Answer: The filesystem path where uploaded files are written. MEDIA_ROOT is the absolute filesystem path where Django stores uploaded files.
What does MEDIA_URL define?
- The public URL prefix uploaded files are served under
- The disk path for uploads
- The database name
- The static files directory
Answer: The public URL prefix uploaded files are served under. MEDIA_URL is the public URL prefix, e.g. /media/, used to build a file's .url.
What enctype must an HTML form have to upload files?
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- application/json
Answer: multipart/form-data. Without enctype="multipart/form-data" the file never reaches request.FILES.
How must you construct a form to bind uploaded files?
- MyForm(request.FILES)
- MyForm(request.GET)
- MyForm(request.POST, request.FILES)
- MyForm(request.POST)
Answer: MyForm(request.POST, request.FILES). Pass both request.POST and request.FILES so file fields are bound.
Where does Django put uploaded files arriving in a request?
- request.POST
- request.GET
- request.body
- request.FILES
Answer: request.FILES. Uploaded files are available in request.FILES.
How do you get a file field's public URL in a template?
- obj.file.url
- obj.file.path_url
- obj.file.link
- obj.file.href
Answer: obj.file.url. The file field's .url attribute combines MEDIA_URL with the stored relative path.
How do you serve uploaded media in development with DEBUG=True?
- Use collectstatic
- Add a static() mapping of MEDIA_URL to MEDIA_ROOT in urls.py
- Set DEBUG=False
- Nothing — it works automatically in production mode
Answer: Add a static() mapping of MEDIA_URL to MEDIA_ROOT in urls.py. In development add static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) to urls.py.
Continue this course
- Previous: The Sessions Framework
- Next: Sending Email — Send plain-text and HTML email with send_mail and templated messages
- Quick reference: Django cheat sheet
Frequently asked questions
What is the difference between FileField and ImageField?
Both store a path to an uploaded file in the database and write the actual bytes to disk. ImageField is a subclass of FileField that additionally validates the upload is a real image and gives you width and height attributes. ImageField requires the Pillow library to be installed. Use FileField for documents like PDFs and CSVs, and ImageField for photos and avatars.
What are MEDIA_ROOT and MEDIA_URL for?
MEDIA_ROOT is the absolute filesystem path where Django writes uploaded files, for example /srv/app/media. MEDIA_URL is the public URL prefix those files are served under, for example /media/. The database stores only the relative path inside MEDIA_ROOT, such as avatars/ada.png, and Django builds the full URL by combining MEDIA_URL with that relative path through the file field's .url attribute.
Why is my uploaded file empty or missing after submitting the form?
The most common cause is the HTML form is missing enctype="multipart/form-data". Without it the browser sends only the text fields and the file never arrives in request.FILES. The second common cause is constructing the form with only request.POST. You must pass both: form = MyForm(request.POST, request.FILES) so the file fields are bound.
How do I serve uploaded media during development?
In development with DEBUG=True you add a static() entry to your project's urls.py that maps MEDIA_URL to MEDIA_ROOT, using django.conf.urls.static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT). This lets the development server return uploaded files. In production you do not use this; instead your web server or a storage service like S3 serves the media directly.