To let users upload an avatar, a product photo, or an attachment in Django, you only need three pieces: a FileField (or ImageField) on the model, a form sent with enctype=”multipart/form-data”, and the MEDIA_ROOT/MEDIA_URL settings. Miss any one of them and the file either won’t save, or will save but never be viewable. This post puts all three together in 5 minutes.

The Problem

A familiar situation: you add a FileField to the model, write the form, hit submit — and the file vanishes. The cause is usually in two places. First, the HTML form lacks enctype, so the browser sends only the file name, not the file contents. Second, the view reads only request.POST and forgets request.FILES — where Django puts uploaded file data. And if MEDIA_URL isn’t configured, a file saved to disk has no URL to access it.

# views.py — SAI: quên request.FILES
def upload_avatar(request):
form = AvatarForm(request.POST) # thiếu request.FILES
if form.is_valid():
form.save()
<!-- template — SAI: thiếu enctype -->
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Tải lên</button>
</form>

The Solution

Step 1 — model: declare an ImageField for images (requires Pillow) or a FileField for any file. The upload_to argument decides the subfolder inside MEDIA_ROOT; pass a callable to group files by user or by date, avoiding name collisions and folders stuffed with thousands of files.

# models.py
from django.db import models
def avatar_path(instance, filename):
return f"avatars/user_{instance.user_id}/{filename}"
class Profile(models.Model):
user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=avatar_path, blank=True)
resume = models.FileField(upload_to="resumes/%Y/%m/", blank=True)

Step 2 — settings and URLs: point MEDIA_ROOT at the folder where files really live, and MEDIA_URL is the URL prefix. In development, add static() to urls.py so Django serves media files; in production, let Nginx or a storage service handle it.

# settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# urls.py (chỉ khi DEBUG=True)
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Step 3 — form and view: use a ModelForm so you don’t map each field by hand. In the view, pass both request.POST and request.FILES to the form. The template must have enctype=”multipart/form-data”, or the browser won’t send the file contents.

# forms.py
from django import forms
from .models import Profile
class AvatarForm(forms.ModelForm):
class Meta:
model = Profile
fields = ["avatar"]
# views.py
def upload_avatar(request):
form = AvatarForm(request.POST or None, request.FILES or None)
if request.method == "POST" and form.is_valid():
profile = form.save(commit=False)
profile.user = request.user
profile.save()
return redirect("profile")
return render(request, "upload.html", {"form": form})
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Tải lên</button>
</form>

To block the wrong format or an oversized file, attach a validator to the field: FileExtensionValidator([“jpg”, “png”]) and a function that checks file.size in clean_avatar(). Validate at the form or model layer instead of trusting the file extension on the client.

# models.py — validate ở tầng model
from django.core.validators import FileExtensionValidator
avatar = models.ImageField(
upload_to=avatar_path,
validators=[FileExtensionValidator(["jpg", "jpeg", "png"])],
blank=True,
)

Apply It to Your Project

Attach an avatar to the user profile page, allow file attachments on support tickets, or build an internal document store. Remember three rules: always use a callable upload_to or one with %Y/%m so files don’t pile into a single folder; limit size and extensions from the start; and never serve the media folder with Django in production — use Nginx, S3, or an equivalent.

Related Posts

Three related posts on the blog, on the same form and file-upload topic:

Frequently Asked Questions

How do FileField and ImageField differ?

ImageField is a FileField plus a check that the file is a valid image, and it needs Pillow. Use ImageField when you only accept images; use FileField for PDFs, CSVs, ZIPs, or any other file.

Do I need to install anything extra for ImageField?

You need Pillow: pip install Pillow. Without Pillow, Django reports an error at the system check step and image upload forms won’t run.

Where are uploaded files stored on disk, and how do I get a link to view a file?

Files live under MEDIA_ROOT plus the path in upload_to. In a template, use {{ profile.avatar.url }} to get the full link starting with MEDIA_URL.

Why does accessing /media/… return 404 in development?

Usually because static() is missing from urls.py, or DEBUG=False while you still expect Django to serve media files. static() only works when DEBUG=True.

How do I stop two people uploading files with the same name?

Use a callable upload_to to rename files by user, time, or uuid. Also, when a file name already exists in the same folder, Django automatically appends a random suffix so it never overwrites.

💬 Have you tried this in your project? If you hit an error or have a neater way, leave a comment below — I read them all and will add the best ideas to the post.

References