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

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

2 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.png

Common 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

ItemRole
MEDIA_ROOTAbsolute disk folder for uploads
MEDIA_URLPublic URL prefix for served media
upload_toSubfolder (string or function) per field
request.FILESWhere uploaded files arrive in a view
profile.avatar.urlThe 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

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.