Upload files in Django (avatar)

Video thumbnail

Uploading files in Django might seem complicated the first time, but it's really not... if you know what steps to follow and what errors to avoid.

We're going to learn how to upload a file using Django, specifically the avatar, but the steps outlined and implemented here can be used and adapted for uploading any type of file, such as PDFs.

The file upload process is not a difficult process, but it does consist of several steps that we must keep in mind; we will cover each of these steps step-by-step to make the implementation as understandable as possible for the reader.

⚙️ Introduction: Uploading files in Django isn't complicated (if you know this beforehand)

Before starting, it's worth clarifying something: Django is already prepared to handle files; you just need to configure it properly.

This tutorial will serve you for both uploading a profile image and for uploading PDF documents or other formats.

When I first tried this, I ran into silly errors—like the classic "Pillow is not installed"—but I'll show you how to solve them quickly.

Initial Setup: Prepare Your Django Environment

Install dependencies and define paths

First, install Pillow, the library Django uses to handle images:

$ pip install Pillow

In your settings.py file, define the paths where files will be saved:

MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

Create the /media folder in the project's root directory and ensure you have write permissions.

I forgot to create it the first time, and Django didn't automatically generate the directory.

Typical Error: Pillow is not installed

If you see this error:

Cannot use ImageField because Pillow is not installed.

simply install the dependency and run again:

$ python manage.py makemigrations $ python manage.py migrate

Model Creation: Where to Save the Uploaded File

To adapt the user model so that it includes the avatar field, we could modify the current model, but to avoid modifying a system model, the most recommended approach would be to create a separate one-to-one relationship containing the additional fields, which in this example is the avatar.

For this example, we'll use a model that extends the user through a OneToOneField relationship.

This way we avoid modifying the system's base model, which I always recommend:

account\models.py

# Create your models here.
class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(upload_to='user/avatar')

✨ I chose to create a separate model to modify the Django User entity; it's cleaner and safer in the long run.

We create and run the migrations:

$ python manage.py makemigrations
$ python manage.py migrate

Now we create a Django ModelForm that handles both regular data (request.POST) and files (request.FILES):

account\forms.py

from django import forms
from .models import UserProfile
class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ('avatar', 'user')
        widgets = {'user': forms.HiddenInput()}

We will apply several configurations for the user field in the following sections, as by default it is a mandatory selection type, which is not the behavior we want.

Uploading a file, minimum implementation

To avoid a form error indicating that the user is required, we set the model to allow that field to be null:

account\models.py

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True)

We could also modify the form as shown previously; the next point to address is setting the field type to hidden:

account\forms.py

class UserProfileForm(forms.ModelForm):
    class Meta:
        ***
        widgets = {'user': forms.HiddenInput() }

View and logic to process the upload

To handle the form, the implementation is similar to previous cases:

account\views.py

@login_required
def profile(request):
    form = UserProfileForm()
    if request.method == "POST":
        form = UserProfileForm(request.POST, request.FILES)
        if form.is_valid():
            userprofile = form.save(commit=False)
            userprofile.user = request.user
            userprofile.save()
    return render(request, 'profile.html', {'form':form})

Important considerations include:

UserProfileForm(request.POST, request.FILES)

To set the files sent by the user, which in this example is only one, the request with flat data is also retained:

request.POST

Since the form above may have other text-type or similar fields.

As for:

commit=False

It is used only to generate the model instance and NOT save to the database, because, before saving to the database, the authenticated user must be set:

userprofile = form.save(commit=False) userprofile.user = request.user userprofile.save()

We define the template with the form, Bootstrap classes, and to show errors:

HTML Template to Display the Form

account\templates\profile.html

{% extends "base_user_account.html" %}
{% load widget_tweaks %}
{% block content %}
{% if form.errors %}
    <div class="alert alert-warning">
        {{ form.errors }}
    </div>
{% endif %}
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <div class="mt-3">
        {{ form.avatar|add_label_class:"form-label" }}
        {{ form.avatar|add_class:"form-control" }}
    </div>
    {{ form.user }}
    <button type="submit" class="mt-3 btn btn-primary">Send</button>
</form>
{% endblock %}

Remember the enctype="multipart/form-data" attribute; without it, files are not sent correctly.

With the previous process, files can be uploaded to the system; we will see that a folder is generated with the uploaded file in:

user\avatar

Which is the path defined in the image type field:

models.ImageField(upload_to='user/avatar')

And the corresponding record in the database; the problem we currently have is that, being a one-to-one relationship, data can only be processed once when the user profile record does not exist in the database, but we solve this in the next section.

Another way to modify the form attributes is directly from the class:

class UserProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ('avatar','user')
        
    def __init__(self,*args,**kwargs):
        super(UserProfileForm, self).__init__(*args,**kwargs)
        self.fields['user'].widget = forms.HiddenInput()
        self.fields['user'].required = False
        self.fields['avatar'].widget.attrs['class'] = "custom-file-input"
        self.fields['avatar'].widget.attrs['id'] = "customFile"
      ***

Verify that the upload works

After submitting the form:

  • The file is saved inside /media/user/avatar/.
  • A UserProfile record is created in the database.
  • You can check it from the Django admin or by exploring the media folder.

I opened the media/user/avatar folder directly and confirmed that the image had been copied correctly.

Common errors when uploading files in Django

Error    Cause    Solution
request.FILES empty    Missing enctype="multipart/form-data"    Add to the form
“MEDIA_ROOT not defined”    Missing configuration    Define in settings.py
“Pillow is not installed”    Missing library    pip install Pillow
File not visible    Incorrectly defined path    Verify MEDIA_URL and MEDIA_ROOT

Tips and Best Practices

Keep paths clean (upload_to='user/avatar' is clear and organized).

Control the maximum file size.

Use custom validations in the form.

If uploading images, optimize them with Pillow before saving.

Protect media paths in production environments (Nginx/Apache).

❓ Frequently Asked Questions (FAQs)

Where are uploaded files saved in Django?
In the folder defined by MEDIA_ROOT within the project.

How to allow multiple files?
Use a FileField with multiple=True and process request.FILES.getlist().

What happens if the user already has a previous file?
You can overwrite it or delete the old one before saving the new one.

Conclusion

Uploading files in Django is much easier when you understand the logic behind request.FILES, the form, and the storage path.

In my experience, the most common errors come from small omissions (like forgetting the enctype or not installing Pillow).
By following this guide, you will have a functional and professional implementation ready for production.

The next step is to use the detail view.

I agree to receive announcements of interest about this Blog.

We will see how to upload files, specifically the avatar using Django 5 and Bootstrap 5 components.

| 👤 Andrés Cruz

🇪🇸 En español