File uploads and media handling — FileField, MEDIA_ROOT, and storage backends
Uploaded files are handled completely differently from every other model field — they're never stored IN the database, only a path to them is. Getting MEDIA_ROOT, MEDIA_URL, and upload_to right is what makes that path actually resolve to a real, servable file.
3 min read
FileField and ImageField store a path, not the file itself
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to="avatars/", blank=True)
resume = models.FileField(upload_to="resumes/", blank=True)Unlike every other model field, FileField (and ImageField, which is a FileField that additionally validates the upload is a real image and requires Pillow installed) doesn't store the file's bytes in the database at all — it stores a path string, and the actual file lives on disk (or in cloud storage, covered below). profile.avatar in Python code isn't raw bytes; it's a FieldFile object wrapping that path, with .url, .path, .size, and .name attributes for working with the file it points to.
MEDIA_ROOT and MEDIA_URL: where files live vs. how they're reached
# settings.py
MEDIA_ROOT = BASE_DIR / "media" # the actual filesystem folder files are saved into
MEDIA_URL = "/media/" # the URL PREFIX used to build a servable link
# urls.py — during development ONLY
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [...] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)MEDIA_ROOT is a filesystem path — where manage.py runserver and production code actually write uploaded files. MEDIA_URL is unrelated to the filesystem entirely — it's the URL prefix Django prepends when building .url for a file field (avatar.url might be /media/avatars/ada.jpg). Django's dev server does not serve MEDIA_ROOT automatically — the static() helper in urls.py is what wires it up, and it's explicitly documented as development-only; a real deployment needs a real web server (nginx, or a cloud storage backend) serving that path instead.
upload_to: organizing where files land, including per-instance dynamically
def user_avatar_path(instance, filename):
return f"avatars/user_{instance.user.id}/{filename}"
class Profile(models.Model):
avatar = models.ImageField(upload_to=user_avatar_path)upload_to can be a plain string (a fixed subfolder, as in the first example) or a function that computes the path dynamically per upload — receiving the model instance and the original filename, and returning the path to save under. This is the standard way to avoid dumping every user's files into one flat folder, and to avoid filename collisions between users who happen to upload files with the same name.
Uploaded files in a form: request.FILES, not request.POST
def upload_view(request):
if request.method == "POST":
form = ProfileForm(request.POST, request.FILES) # FILES passed SEPARATELY
if form.is_valid():
form.save()Uploaded files never arrive in request.POST — they arrive in a separate dict-like object, request.FILES, and a ModelForm handling file fields needs both passed in explicitly. Forgetting request.FILES is a common bug that fails quietly: the form still validates other fields fine, but the file field comes back empty, since Django never even looked at request.FILES for it. The template also needs enctype="multipart/form-data" on the <form> tag — without it, the browser never sends the file's bytes at all, regardless of what the view does.
Storage backends: the same API, a different destination
# settings.py — swapping local disk for cloud storage, no model code changes needed
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage", # e.g. django-storages, for S3
},
}FileField/ImageField don't talk to the filesystem directly — they go through a configurable storage backend, which defaults to local disk (FileSystemStorage) but can be swapped for S3, Google Cloud Storage, or any other backend implementing the same interface, via one settings change. This is precisely why application code that calls .save(), .url, .open() on a file field never needs to change when a project moves from local-disk development to cloud storage in production — the storage abstraction is the whole point.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does a FileField actually store in the database?
2. What's the difference between MEDIA_ROOT and MEDIA_URL?
3. Why does a form with a file field need request.FILES in addition to request.POST?
4. Why can application code that calls .save(), .url, .open() on a file field stay unchanged when moving from local disk to S3 in production?