Introduction to Django Media Handling

This article introduces Django media handling and explains how to manage user-uploaded files such as images, documents, and attachments. It covers MEDIA_ROOT, MEDIA_URL, FileField, ImageField, upload forms, request.FILES, storage backends, validation, private media, production storage, file cleanup, testing, and common mistakes.

Introduction to Django Media Handling

Django applications often need to work with files uploaded by users.

Examples include:

  • profile pictures
  • product photos
  • PDF documents
  • attachments
  • videos
  • audio files
  • spreadsheets
  • scanned documents

Django refers to these user-controlled files as media files.

Media files are different from static files.

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Static files
    Files provided by the application itself.

    Examples:
    CSS
    JavaScript
    logos
    icons
    fonts

Media files
    Files uploaded or generated while the
    application is running.

    Examples:
    profile images
    documents
    product photos
    attachments

Django provides file fields, upload handling, and a storage API for working with these files. By default, Django can store files on the local filesystem, but the storage system can also be replaced with remote or custom storage.

Basic Media Configuration

A basic local development setup uses two settings:

python

1
2
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

These settings have different purposes:

Setting Purpose
MEDIA_ROOT Directory where uploaded files are stored
MEDIA_URL URL prefix used to access uploaded files

Given:

python

1
2
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"

the project might contain:

text

1
2
3
4
5
6
7
project/
├── manage.py
├── media/
│   ├── avatars/
│   └── documents/
├── config/
└── accounts/

A stored file might exist at:

text

1
project/media/avatars/alex.jpg

and be available during development through a URL such as:

text

1
/media/avatars/alex.jpg

With the default local filesystem storage, Django uses filesystem-backed storage for uploaded files, while alternative storage systems can implement the same storage interface.

Media Files and Static Files

Static and media files should normally be kept separate.

A common project structure is:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
project/
├── manage.py
├── static/
│   ├── css/
│   ├── js/
│   └── images/
├── media/
│   ├── avatars/
│   ├── products/
│   └── documents/
└── config/

Typical settings:

python

1
2
3
4
5
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

A site logo might be:

text

1
/static/images/logo.svg

A user-uploaded profile picture might be:

text

1
/media/avatars/user-42.jpg

Do not store uploaded user content inside the static directory.

Static files are deployment assets. Media files are runtime data.

FileField

Django’s FileField represents a file associated with a model.

Example:

python

1
2
3
4
5
6
7
8
9
from django.db import models


class Document(models.Model):
    title = models.CharField(max_length=200)

    file = models.FileField(
        upload_to="documents/",
    )

If a user uploads:

text

1
report.pdf

Django may store it as:

text

1
media/documents/report.pdf

The database does not normally contain the complete contents of the file.

Instead, the model field stores the file’s name or storage-relative path, while the storage backend manages the actual file. Django exposes the stored file through a FieldFile object.

upload_to

The upload_to argument determines where files are stored relative to the configured storage location.

Example:

python

1
2
3
file = models.FileField(
    upload_to="documents/",
)

Possible location:

text

1
media/documents/report.pdf

For images:

python

1
2
3
image = models.ImageField(
    upload_to="products/",
)

Possible location:

text

1
media/products/laptop.jpg

Organizing uploads into directories keeps media easier to manage.

Date-Based Upload Paths

upload_to can contain date formatting.

Example:

python

1
2
3
image = models.ImageField(
    upload_to="articles/%Y/%m/",
)

An uploaded image might be stored under:

text

1
media/articles/2026/08/header.jpg

This can help prevent one directory from accumulating a very large number of files.

Dynamic Upload Paths

upload_to can also be a callable.

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def user_upload_path(instance, filename):
    return (
        f"users/{instance.user_id}/{filename}"
    )


class Profile(models.Model):
    user = models.OneToOneField(
        "auth.User",
        on_delete=models.CASCADE,
    )

    avatar = models.ImageField(
        upload_to=user_upload_path,
    )

For user 42, the path might become:

text

1
users/42/avatar.jpg

The function receives:

text

1
2
3
4
5
instance
    The model instance being saved.

filename
    The original uploaded filename.

It must return the path that the storage system should use.

Be Careful With Unsaved Primary Keys

A new model instance may not yet have a primary key when Django determines the upload path.

For example:

python

1
2
def upload_path(instance, filename):
    return f"articles/{instance.pk}/{filename}"

If the object has not been saved yet:

python

1
instance.pk

may be:

text

1
None

This could create a path such as:

text

1
articles/None/image.jpg

If a stable identifier is needed before the first save, consider using something already available on the instance, such as:

  • a UUID
  • a user ID
  • another stable field

For example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import uuid

from django.db import models


class Document(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    file = models.FileField(
        upload_to="documents/",
    )

ImageField

ImageField is designed specifically for uploaded images.

python

1
2
3
4
5
6
7
from django.db import models


class Profile(models.Model):
    avatar = models.ImageField(
        upload_to="avatars/",
    )

It provides image-specific validation and can expose image dimensions.

Django’s image handling uses Pillow, so projects using ImageField normally need Pillow installed. Django also warns that uploaded media must still be treated as untrusted content; image validation alone does not make arbitrary uploaded files safe to serve.

Install Pillow with:

bash

1
python -m pip install Pillow

Optional File Fields

An upload can be optional:

python

1
2
3
4
avatar = models.ImageField(
    upload_to="avatars/",
    blank=True,
)

A nullable database value may also be used when appropriate:

python

1
2
3
4
5
document = models.FileField(
    upload_to="documents/",
    blank=True,
    null=True,
)

For many file fields, blank=True is sufficient when an empty filename is an acceptable representation of “no file.”

Accessing an Uploaded File

Suppose a model contains:

python

1
2
3
4
class Profile(models.Model):
    avatar = models.ImageField(
        upload_to="avatars/",
    )

Then:

python

1
profile.avatar

returns a file-related object rather than a plain path string.

Useful properties include:

python

1
2
3
profile.avatar.name
profile.avatar.url
profile.avatar.size

For example:

python

1
print(profile.avatar.name)

might return:

text

1
avatars/alex.jpg

And:

python

1
print(profile.avatar.url)

might return:

text

1
/media/avatars/alex.jpg

The exact URL and storage behavior depend on the configured storage backend. Django’s file API abstracts access so code can work with local or custom storage.

File Paths Are Not Always Available

Local filesystem storage may provide a physical path:

python

1
profile.avatar.path

For example:

text

1
/home/app/media/avatars/alex.jpg

However, remote storage may not have a meaningful local filesystem path.

For example, files stored in object storage may exist only remotely.

Code that assumes this always works:

python

1
open(profile.avatar.path)

is less portable.

Prefer Django’s file interface:

python

1
2
3
profile.avatar.open("rb")
content = profile.avatar.read()
profile.avatar.close()

This allows the storage backend to decide how the file is retrieved.

Displaying Uploaded Images in Templates

Suppose a profile contains:

python

1
2
3
4
avatar = models.ImageField(
    upload_to="avatars/",
    blank=True,
)

A template can display it with:

django

1
2
3
4
5
6
{% if profile.avatar %}
    <img
        src="{{ profile.avatar.url }}"
        alt="{{ profile.user.username }}"
    >
{% endif %}

Do not use the {% static %} tag for uploaded media.

Incorrect:

django

1
2
3
4
5
6
{% load static %}

<img
    src="{% static profile.avatar %}"
    alt="Avatar"
>

Correct:

django

1
2
3
4
<img
    src="{{ profile.avatar.url }}"
    alt="Avatar"
>

{% static %} is for application-owned static assets.

.url is used for stored media files.

Creating an Upload Form

A model form can expose a file field automatically.

Model:

python

1
2
3
4
5
6
7
8
9
from django.db import models


class Document(models.Model):
    title = models.CharField(max_length=200)

    file = models.FileField(
        upload_to="documents/",
    )

Form:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django import forms

from .models import Document


class DocumentForm(forms.ModelForm):
    class Meta:
        model = Document
        fields = [
            "title",
            "file",
        ]

Template:

django

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<form
    method="post"
    enctype="multipart/form-data"
>
    {% csrf_token %}

    {{ form.as_p }}

    <button type="submit">
        Upload
    </button>
</form>

The important attribute is:

html

1
enctype="multipart/form-data"

Without it, the browser will not submit the uploaded file correctly.

Handling Files in a Function-Based View

Uploaded files are available through:

python

1
request.FILES

Django places uploaded file data in request.FILES when a request contains a correctly encoded file upload.

When using a Django form:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.shortcuts import (
    redirect,
    render,
)

from .forms import DocumentForm


def document_create(request):
    if request.method == "POST":
        form = DocumentForm(
            request.POST,
            request.FILES,
        )

        if form.is_valid():
            form.save()
            return redirect("document-list")
    else:
        form = DocumentForm()

    return render(
        request,
        "documents/document_form.html",
        {"form": form},
    )

Notice:

python

1
2
3
4
DocumentForm(
    request.POST,
    request.FILES,
)

If request.FILES is omitted, the uploaded file will not be passed to the form.

The Upload Flow

A basic model-form upload follows this sequence:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Browser selects file
        ↓
multipart/form-data request
        ↓
request.FILES
        ↓
Django form
        ↓
FileField / ImageField
        ↓
Storage backend
        ↓
Stored file
        ↓
File name saved on model

Django separates HTTP upload handling from long-term file storage. Upload handlers process incoming data, while the storage API determines where saved files live.

Inspecting request.FILES

Without a model form, the file can be accessed directly:

python

1
2
3
4
5
6
7
def upload(request):
    if request.method == "POST":
        uploaded_file = request.FILES["file"]

        print(uploaded_file.name)
        print(uploaded_file.size)
        print(uploaded_file.content_type)

Common properties include:

python

1
2
3
uploaded_file.name
uploaded_file.size
uploaded_file.content_type

The uploaded object also provides methods for reading its contents.

Avoid Reading Large Files All at Once

This may be acceptable for very small files:

python

1
content = uploaded_file.read()

For potentially large uploads, process the file in chunks:

python

1
2
for chunk in uploaded_file.chunks():
    process(chunk)

Django’s upload system supports handling uploaded data in memory or temporary files depending on upload size and configured upload handlers.

Using chunks() avoids unnecessarily loading an entire large file into application memory.

Saving a File Manually

Django provides a storage abstraction that can be used without a model.

Example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from django.core.files.storage import (
    default_storage,
)


def upload(request):
    uploaded_file = request.FILES["file"]

    filename = default_storage.save(
        uploaded_file.name,
        uploaded_file,
    )

    url = default_storage.url(filename)

The storage system decides where the file is written.

With local storage this may be:

text

1
media/report.pdf

With another backend, the same API can point to remote storage. Django’s storage API is specifically designed to abstract filesystem and custom storage implementations.

FileSystemStorage

Django includes FileSystemStorage for storing files on the local filesystem.

Example:

python

1
2
3
4
5
6
7
8
9
from django.core.files.storage import (
    FileSystemStorage,
)


storage = FileSystemStorage(
    location="/var/app/uploads",
    base_url="/uploads/",
)

Save a file:

python

1
2
3
4
filename = storage.save(
    uploaded_file.name,
    uploaded_file,
)

Get its URL:

python

1
url = storage.url(filename)

Delete it:

python

1
storage.delete(filename)

For most ordinary model uploads, you do not need to instantiate FileSystemStorage yourself.

Django’s configured default storage is usually enough.

The Default Storage

Code can access the configured default storage through:

python

1
2
3
from django.core.files.storage import (
    default_storage,
)

Use:

python

1
2
3
4
5
default_storage.save(...)
default_storage.open(...)
default_storage.exists(...)
default_storage.delete(...)
default_storage.url(...)

This is preferable to directly manipulating files with Python’s filesystem functions when the application might later move to remote storage.

Instead of:

python

1
2
3
4
import os


os.remove(file_path)

prefer:

python

1
default_storage.delete(file_name)

when working with a Django-managed stored file.

Storage Configuration

Modern Django versions use the STORAGES setting to configure storage aliases.

A basic configuration can define default and static-file storage separately:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
STORAGES = {
    "default": {
        "BACKEND": (
            "django.core.files.storage."
            "FileSystemStorage"
        ),
    },
    "staticfiles": {
        "BACKEND": (
            "django.contrib.staticfiles.storage."
            "StaticFilesStorage"
        ),
    },
}

The default storage is commonly used for uploaded media.

The staticfiles storage handles static assets.

Django’s current storage API exposes configurable storage backends and includes filesystem, in-memory, and custom-storage support.

Different Storage for One Field

A specific field can use its own storage backend.

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.core.files.storage import (
    FileSystemStorage,
)
from django.db import models


private_storage = FileSystemStorage(
    location="/srv/private-files",
)


class Contract(models.Model):
    document = models.FileField(
        upload_to="contracts/",
        storage=private_storage,
    )

Django’s file fields support passing a storage object so individual fields can use storage different from the project default.

This can be useful for separating:

  • public images
  • private documents
  • generated reports
  • archived files

Serving Media During Development

For local development, Django can expose media files through the development server.

Project URL configuration:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path


urlpatterns = [
    path("admin/", admin.site.urls),
]

if settings.DEBUG:
    urlpatterns += static(
        settings.MEDIA_URL,
        document_root=settings.MEDIA_ROOT,
    )

With:

python

1
2
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

a file such as:

text

1
media/avatars/alex.jpg

can be requested at:

text

1
http://127.0.0.1:8000/media/avatars/alex.jpg

This development helper is not a production file-serving strategy.

Media Files in Production

Production media handling is different from development.

Common options include:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Application server
    Django

Media storage
    local persistent filesystem
    object storage
    cloud storage

Media delivery
    web server
    storage service
    CDN

The important requirement is persistence.

If an application platform replaces or destroys its local filesystem during deployment or restart, uploaded files must not be stored only on that temporary filesystem.

A production storage system should match the deployment environment.

Django supports custom storage backends specifically so files can live outside the local filesystem when required.

Media on Object Storage

A common production architecture is:

text

1
2
3
4
5
6
7
Browser
   ↓
Django
   ↓
Storage backend
   ↓
Object storage

Later:

text

1
2
3
4
5
Browser
   ↓
Media URL
   ↓
Object storage or CDN

The model still works with:

python

1
document.file.url

while the storage backend may return a remote URL rather than:

text

1
/media/documents/report.pdf

This is one of the main advantages of using Django’s storage abstraction instead of hard-coding filesystem paths.

Public and Private Media

Not every uploaded file should be publicly accessible.

Public media might include:

  • product images
  • public avatars
  • article images

Private media might include:

  • invoices
  • medical documents
  • contracts
  • private messages
  • identity documents

Do not assume that hiding a media URL from a template makes the file private.

If a file is served publicly at:

text

1
/media/contracts/contract-123.pdf

anyone who obtains the URL may potentially request it.

Private files usually require a different access strategy, such as:

text

1
2
3
4
5
6
7
User requests file
        ↓
Django checks authentication
        ↓
Django checks permission
        ↓
Authorized download is returned

or:

text

1
2
3
4
5
6
7
User requests file
        ↓
Django checks permission
        ↓
Temporary signed storage URL
        ↓
User downloads from storage

Authorization should happen server-side.

A Protected Download View

For small files, Django can return a protected file response.

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.contrib.auth.decorators import (
    login_required,
)
from django.http import FileResponse
from django.shortcuts import (
    get_object_or_404,
)

from .models import Document


@login_required
def document_download(request, pk):
    document = get_object_or_404(
        Document,
        pk=pk,
        owner=request.user,
    )

    return FileResponse(
        document.file.open("rb"),
        as_attachment=True,
        filename=document.file.name,
    )

This allows Django to check ownership before returning the file.

For large files or high-traffic systems, letting Django stream every protected file may be less efficient than using a storage system or web server that supports secure delegated downloads.

File Validation

Uploaded files should be treated as untrusted input.

A browser-provided filename or content type should not be considered proof of what the file actually contains.

Possible validation includes:

  • maximum file size
  • allowed extensions
  • expected content type
  • file signatures
  • image validation
  • image dimensions
  • malware scanning
  • document-specific parsing

Django’s security documentation explicitly warns that uploaded media can be dangerous if served incorrectly. For example, a file may satisfy image checks while also containing content that becomes dangerous when interpreted by a browser.

Validating File Size

A form can reject large files:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django import forms

from .models import Document


class DocumentForm(forms.ModelForm):
    class Meta:
        model = Document
        fields = [
            "title",
            "file",
        ]

    def clean_file(self):
        uploaded_file = self.cleaned_data["file"]

        max_size = 5 * 1024 * 1024

        if uploaded_file.size > max_size:
            raise forms.ValidationError(
                "The file must be 5 MB or smaller."
            )

        return uploaded_file

This improves application-level validation.

Infrastructure may also need its own upload-size limits.

Validating File Extensions

Django provides file-extension validation.

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django.core.validators import (
    FileExtensionValidator,
)
from django.db import models


class Document(models.Model):
    file = models.FileField(
        upload_to="documents/",
        validators=[
            FileExtensionValidator(
                allowed_extensions=[
                    "pdf",
                    "docx",
                ]
            )
        ],
    )

This checks the filename extension.

It does not prove that the file contents actually match that extension.

A file named:

text

1
document.pdf

is not automatically a valid PDF simply because the name ends in .pdf.

Validating Images

ImageField performs image-specific validation, but uploaded files should still be treated carefully.

Example:

python

1
2
3
4
5
6
7
from django.db import models


class Profile(models.Model):
    avatar = models.ImageField(
        upload_to="avatars/",
    )

Additional requirements may include:

  • maximum dimensions
  • maximum file size
  • approved formats
  • re-encoding images after upload

For applications accepting uploads from untrusted users, security should not rely only on the filename or client-provided MIME type. Django’s security documentation recommends careful deployment and serving of uploaded media.

Filename Collisions

Two users might upload files with the same name:

text

1
photo.jpg

Django storage backends are responsible for determining an available stored name.

You should generally not write application logic that assumes the final filename will always equal the original filename.

Use:

python

1
instance.file.name

after saving to obtain the actual stored name.

Do Not Trust Original Filenames

An original filename should normally be treated as user-controlled data.

Avoid using it blindly for:

  • shell commands
  • operating-system paths
  • HTML output
  • authorization
  • determining file type

If the application needs predictable storage names, generate its own names.

For example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import uuid
from pathlib import Path


def upload_path(instance, filename):
    extension = Path(filename).suffix.lower()

    filename = f"{uuid.uuid4()}{extension}"

    return f"documents/{filename}"

A file might then be stored as:

text

1
2
documents/
5d9966a2-a7fa-4c49-a905-c2628a55fce7.pdf

Replacing Uploaded Files

Suppose a profile already has:

text

1
avatars/old.jpg

and the user uploads:

text

1
avatars/new.jpg

Saving the new model field does not necessarily mean the old physical file should automatically disappear.

Applications that allow file replacement should decide explicitly how obsolete files are cleaned up.

Possible strategies include:

  • delete the previous file after replacement
  • periodically remove orphaned files
  • keep old versions intentionally
  • use lifecycle rules in remote storage

Be careful not to delete a file that is still referenced elsewhere.

Deleting Model Objects and Files

Deleting a database object does not always mean its underlying storage file should be removed automatically.

For example:

python

1
document.delete()

may remove the database row while the stored file remains.

If cleanup is required, it must be implemented deliberately.

One approach is:

python

1
2
document.file.delete(save=False)
document.delete()

Another approach is application-level cleanup triggered from an explicit service.

Signals such as post_delete are sometimes used, but they can hide side effects and require careful handling of shared files and transactions.

Avoid File Operations Inside Transactions When Possible

A database transaction can roll back database changes.

A file-storage operation may not roll back with it.

For example:

text

1
2
3
4
5
Database:
    transaction rolls back

Storage:
    uploaded file still exists

or:

text

1
2
3
4
5
Database:
    object restored after rollback

Storage:
    file was already deleted

Database state and file-storage state are separate systems.

Critical file workflows should account for this difference.

Working With Existing Files

A model field can open a stored file:

python

1
document.file.open("rb")

Read:

python

1
content = document.file.read()

Close:

python

1
document.file.close()

A context manager is often clearer:

python

1
2
with document.file.open("rb") as file:
    content = file.read()

For large files:

python

1
2
3
with document.file.open("rb") as file:
    while chunk := file.read(8192):
        process(chunk)

Avoid loading large files fully into memory without a reason.

Saving Generated Files

Not every media file needs to come from a browser upload.

Applications may generate:

  • PDF reports
  • CSV exports
  • thumbnails
  • invoices
  • transformed images

A generated file can be saved with Django’s file API.

Example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django.core.files.base import ContentFile


content = ContentFile(
    b"Generated file contents"
)

document.file.save(
    "report.txt",
    content,
    save=True,
)

This lets the configured storage backend decide where the generated file is stored.

Direct Uploads Versus Django Uploads

For ordinary applications, files may travel through Django:

text

1
2
3
4
5
Browser
    ↓
Django
    ↓
Storage

For large uploads or cloud-heavy systems, another architecture may be preferable:

text

1
2
3
Browser
    ↓
Object storage

with Django first issuing an authorized upload instruction or signed URL.

This prevents large file data from passing through the Django application server.

That architecture is more advanced, but it can improve scalability for large files.

Media and Backups

Uploaded files are application data.

Backing up only the database may not be enough.

For example, the database might contain:

text

1
avatars/user42.jpg

but if the media storage is lost, the actual image is gone.

A complete backup strategy may need:

text

1
2
3
Database backup
        +
Media storage backup

or storage-level replication/versioning.

The database and media storage should be treated as related but separate persistent resources.

Do Not Commit Media Files to Git

The media directory should normally not be part of application source control.

A .gitignore might contain:

text

1
media/

Uploaded files are runtime data rather than source code.

Test fixtures or deliberately committed sample files are a different case and should live in an appropriate test or source directory.

Testing File Uploads

Django tests can create an uploaded file with SimpleUploadedFile.

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.core.files.uploadedfile import (
    SimpleUploadedFile,
)
from django.test import TestCase

from .models import Document


class DocumentTests(TestCase):
    def test_document_upload(self):
        uploaded_file = SimpleUploadedFile(
            "report.txt",
            b"Example contents",
            content_type="text/plain",
        )

        document = Document.objects.create(
            title="Report",
            file=uploaded_file,
        )

        self.assertTrue(document.file.name)

Tests should avoid writing permanent files into the real production media directory.

A temporary media directory or test-specific storage configuration is safer.

Testing Upload Forms

Example:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.core.files.uploadedfile import (
    SimpleUploadedFile,
)
from django.test import TestCase

from .forms import DocumentForm


class DocumentFormTests(TestCase):
    def test_valid_file_is_accepted(self):
        uploaded_file = SimpleUploadedFile(
            "report.pdf",
            b"example",
            content_type="application/pdf",
        )

        form = DocumentForm(
            data={
                "title": "Report",
            },
            files={
                "file": uploaded_file,
            },
        )

        self.assertTrue(form.is_valid())

Files are passed separately from ordinary form data.

Common Beginner Mistakes

Confusing Static and Media Files

Static:

text

1
2
3
CSS
JavaScript
site logo

Media:

text

1
2
3
user avatar
uploaded document
product photo

Keep the systems separate.

Forgetting multipart/form-data

Incorrect:

html

1
<form method="post">

Correct:

html

1
2
3
4
<form
    method="post"
    enctype="multipart/form-data"
>

Forgetting request.FILES

Incorrect:

python

1
form = DocumentForm(request.POST)

Correct:

python

1
2
3
4
form = DocumentForm(
    request.POST,
    request.FILES,
)

Using {% static %} for Media

Incorrect:

django

1
{% static profile.avatar %}

Correct:

django

1
{{ profile.avatar.url }}

Hard-Coding Filesystem Paths

Avoid:

python

1
2
3
4
path = (
    "/srv/app/media/"
    + document.file.name
)

Prefer the storage API:

python

1
document.file.open()

or:

python

1
document.file.url

Assuming .path Always Exists

Remote storage may not provide a local filesystem path.

Use storage-independent file methods when possible.

Trusting File Extensions

A .jpg filename does not prove that the uploaded contents are a safe JPEG.

Validate according to the application’s risk level.

Storing Sensitive Files Publicly

A private document should not become accessible merely because somebody knows its media URL.

Use server-side authorization or private storage.

Using Development Media Serving in Production

The development helper:

python

1
2
3
4
static(
    settings.MEDIA_URL,
    document_root=settings.MEDIA_ROOT,
)

is not a production media-delivery system.

Assuming Deleting a Model Deletes the File

Database and storage cleanup are separate concerns.

Implement cleanup intentionally.

Assuming Storage Operations Roll Back

A database transaction does not automatically undo remote or filesystem operations.

Design workflows accordingly.

Storing Uploads on an Ephemeral Filesystem

If the production platform replaces its filesystem during deployment, locally stored media can disappear.

Use persistent storage.

A Complete Basic Example

Model:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from django.db import models


class Photo(models.Model):
    title = models.CharField(
        max_length=200,
    )

    image = models.ImageField(
        upload_to="photos/%Y/%m/",
    )

    uploaded_at = models.DateTimeField(
        auto_now_add=True,
    )

    def __str__(self):
        return self.title

Settings:

python

1
2
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

Form:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from django import forms

from .models import Photo


class PhotoForm(forms.ModelForm):
    class Meta:
        model = Photo
        fields = [
            "title",
            "image",
        ]

View:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from django.shortcuts import (
    redirect,
    render,
)

from .forms import PhotoForm


def photo_create(request):
    if request.method == "POST":
        form = PhotoForm(
            request.POST,
            request.FILES,
        )

        if form.is_valid():
            photo = form.save()

            return redirect(
                "photo-detail",
                pk=photo.pk,
            )
    else:
        form = PhotoForm()

    return render(
        request,
        "photos/photo_form.html",
        {"form": form},
    )

Template:

django

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<h1>Upload Photo</h1>

<form
    method="post"
    enctype="multipart/form-data"
>
    {% csrf_token %}

    {{ form.as_p }}

    <button type="submit">
        Upload
    </button>
</form>

Detail template:

django

1
2
3
4
5
6
7
8
<h1>{{ photo.title }}</h1>

{% if photo.image %}
    <img
        src="{{ photo.image.url }}"
        alt="{{ photo.title }}"
    >
{% endif %}

Development URLs:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from django.conf import settings
from django.conf.urls.static import static
from django.urls import include, path


urlpatterns = [
    path(
        "",
        include("photos.urls"),
    ),
]

if settings.DEBUG:
    urlpatterns += static(
        settings.MEDIA_URL,
        document_root=settings.MEDIA_ROOT,
    )

Possible project structure:

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
project/
├── manage.py
├── media/
│   └── photos/
│       └── 2026/
│           └── 08/
│               └── example.jpg
├── photos/
│   ├── forms.py
│   ├── models.py
│   ├── urls.py
│   └── views.py
└── config/
    └── settings.py

For a basic application:

  1. Configure MEDIA_ROOT.
  2. Configure MEDIA_URL.
  3. Add FileField or ImageField to the model.
  4. Choose an appropriate upload_to path.
  5. Create a form that accepts the file.
  6. Add multipart/form-data to the HTML form.
  7. Pass request.FILES to the Django form.
  8. Validate the uploaded file.
  9. Use the field’s .url when displaying it.
  10. Serve files through Django only during development.
  11. Use persistent storage in production.
  12. Protect private files with server-side authorization.
  13. Plan how replaced and deleted files are cleaned up.
  14. Include media storage in backup planning.

Mini Reference

text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
MEDIA_ROOT
    Local directory for uploaded media

MEDIA_URL
    URL prefix for media files

FileField
    Model field for uploaded files

ImageField
    FileField with image-specific validation

upload_to
    Determines the storage-relative upload path

request.FILES
    Contains incoming uploaded files

multipart/form-data
    Required HTML form encoding for uploads

file.name
    Stored file name

file.url
    Public or storage-generated file URL

file.size
    File size

file.open()
    Opens the stored file

file.delete()
    Deletes the file from storage

default_storage
    Configured default Django storage

Basic settings:

python

1
2
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

Basic model field:

python

1
2
3
file = models.FileField(
    upload_to="documents/",
)

Basic image field:

python

1
2
3
image = models.ImageField(
    upload_to="images/",
)

Basic form:

django

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<form
    method="post"
    enctype="multipart/form-data"
>
    {% csrf_token %}
    {{ form.as_p }}

    <button type="submit">
        Upload
    </button>
</form>

Basic view handling:

python

1
2
3
4
form = DocumentForm(
    request.POST,
    request.FILES,
)

Basic template output:

django

1
2
3
<a href="{{ document.file.url }}">
    Download
</a>

Django media handling provides the tools needed to receive, store, retrieve, and manage files created or uploaded while an application is running.

The most important concepts are:

  • media files are different from static files
  • MEDIA_ROOT defines local media storage
  • MEDIA_URL defines the media URL prefix
  • FileField represents general uploaded files
  • ImageField provides image-specific behavior
  • upload_to controls storage organization
  • uploaded files arrive through request.FILES
  • HTML upload forms require multipart/form-data
  • Django’s storage API separates application code from the physical storage system
  • local development storage can later be replaced by remote storage
  • sensitive media requires real authorization
  • uploaded files must always be treated as untrusted data
  • database deletion and file deletion are separate operations
  • production media must live on persistent storage
  • backups need to account for both database records and stored media files

For a small project, start with MEDIA_ROOT, MEDIA_URL, and Django’s default filesystem storage. As the application grows, the same file and storage APIs allow the project to move toward private media, remote object storage, signed URLs, and more advanced upload workflows without redesigning every model that contains a file.

Join the Newsletter

Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.

Get updates when new guides, learning paths, cheat sheets, and field notes are published.

No spam. Unsubscribe anytime.



There is no third-party involved so don't worry - we won't share your details with anyone.