Overview
If you’re adding user avatars to a Django app, you’ve probably checked out the ImageField documentation only to find that it’s pretty vague about how things actually work in practice. Where do the image files go? How do you handle updates without filling your server with orphaned images?
Here's a complete, production-ready working implementation that will show you how to add avatars to your Django user model.
You'll learn to:
- Add an avatar
ImageFieldthat works in production - Automatically clean up old images when users upload new ones
- Organize your file storage without cluttering your media folder
- Display avatars with proper fallbacks
- Add the two media constants you need in your settings file
The complete working code is on GitHub with quickstart instructions: Filebaby Demo App

Prerequisites
A Custom User Model
The easiest way to add avatars is to add it to your custom user model. The demo app Filebaby uses a custom user model.
Filebaby organizes the avatar images by placing them in user folders. Since Filebaby has public user profile names (slugs) the avatars are stored in folders named after their slugs.
Finally, ImageField requires the Pillow image library.
The Filebaby app already takes care of these prerequisites, but let's talk briefly about Pillow because it can cause some confusion if you add the field before installing the library.
Install Pillow before using ImageField
If you installed the Filebaby requirements then Pillow is already installed. Skip to the next section.
Django runs a check which attempts to import Pillow. If that image library cannot be imported then it complains that you Cannot use ImageField because Pillow is not installed.
The solution is to install Pillow with this command: pip install Pillow
The library is required if you want to use ImageField so you must install it.
Put an ImageField on your User model
As I mentioned earlier, you should already have a custom User model. This swappable model is part of Django's standard user management system.
Put the avatar on your user model using ImageField:
# users/models.py
def avatar_upload_to(instance, filename):
return "avatars/{}/{}".format(instance.slug, filename)
class User(Timestamped, AbstractUser):
"""Our custom User model"""
avatar = models.ImageField(
upload_to=avatar_upload_to,
blank=True,
null=True,
help_text=_("Upload an image to personalize your profile"),
)
The ImageField allows a callable function to be used to determine where to place the uploaded images. Since the avatars are public the images are placed into the media folder. This location is configured in your settings under the MEDIA_ROOT setting. Let's assume that MEDIA_ROOT contains media.
Once your app becomes popular you will find that the uploaded file names collide in the MEDIA_ROOT folder unless you create a folder hierarchy. This is defined in the avatar_upload_to function. Our User model has a short unique string as a slug. The avatars for the user are placed into folders with the same name.
media
└── avatars
└── aSlugFromAUser1234
└── avatar_pic.jpg
Our avatar cleans up this folder by deleting the old images when a new one is uploaded. This happens in the overridden save method:
# users/models.py
class User(Timestamped, AbstractUser):
"""Our custom User model"""
# ...
def save(self, *args, **kwargs):
if self.username:
self.username = self.username.lower()
if self.pk:
try:
old = self.__class__.objects.get(pk=self.pk)
if old.avatar and old.avatar != self.avatar:
old.avatar.delete(save=False)
except self.__class__.DoesNotExist:
pass
super().save(*args, **kwargs)
Add These Important Media Settings
In your settings.py file you need to place two important constants: MEDIA_URL and MEDIA_ROOT.
The MEDIA_URL is what appears in the HTML sent to the user. It's usually set to something like media/ so the absolute path to an avatar might be something like: /media/avatars/my-avatar.png
The MEDIA_ROOT is the folder where you store the images on your local server. It is commonly a network drive attached to your server or a bucket on Amazon S3. Confusingly it often has a value of media.
In the Filebaby app, the settings file is configured to pull these keys from the environment. To set these values, create a dot-env file .env or set them in the container environment. The dotenv and environs packages will pull those keys into the app settings:
# filebaby/settings.py
from dotenv import find_dotenv, load_dotenv
from environs import Env
env = Env()
load_dotenv(find_dotenv())
# 5 min later...
MEDIA_URL = env.str("MEDIA_URL", "media/")
MEDIA_ROOT = env.str("MEDIA_ROOT", "")
if not MEDIA_ROOT:
MEDIA_ROOT = BASE_DIR / "media"
Do this so that your apps are ready for deployment using Docker. You should do this even if you are not currently using containers for deployment because eventually you will.
Add a file handler to your URLconf
Add this code to the bottom of your root urls.py file (the app's root URLconf):
# filebaby/urls.py
if settings.DEBUG:
from django.conf.urls.static import static
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
This will allow you to serve your media files via the development server while building the app. Requests for anything in /media/ get resolved to the media root folder. That's what this does.
Modify the User model's ModelForm & UpdateView
User ModelForm
Add the capability to change the image attached to the avatar property by changing the ModelForm that handles the updating of the User model properties.
# users/forms.py
class UpdateUserForm(forms.ModelForm):
class Meta:
model = User
fields = ("public_name", "about", "avatar", "is_public")
widgets = {
"public_name": forms.TextInput(attrs={"class": "form-control"}),
"about": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
"avatar": forms.FileInput(attrs={"class": "form-control"}),
"is_public": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
Usually this is quite simple. You just add the property name to the fields list. In the case of the Filebaby app, I wanted to add some CSS classes for the Bootstrap 5.3 form widget.
User UpdateView
In the UpdateView, I added a image resizing helper function named resize_uploaded_image. This helper makes a square out of whatever aspect ratio image that your user submits. There are other techniques that resize images when the image is first viewed (see django-imagefit, django-photologue, mezzanine).
# users/views.py
class ProfileUpdateView(SuccessMessageMixin, UpdateView):
model = User
form_class = UpdateUserForm
success_url = reverse_lazy("users:profile")
success_message = "Profile updated successfully."
def form_valid(self, form: Any):
"""
Resize the profile image before saving
"""
# image at this point is an django.core.files.uploadedfile.InMemoryUploadedFile
image = form.cleaned_data.get("avatar")
if image:
form.instance.avatar = resize_uploaded_image(image)
return super().form_valid(form)
Display the ImageField avatar
The avatar URL can be obtained from the avatar by accessing the url property:
# site_templates/files/file_detail.html
{% if file.owner.avatar %}
<img src="{{ file.owner.avatar.url }}" alt="{{ file.owner.best_name }}'s Avatar"
class="img-fluid rounded-circle">
{% else %}
<img src="{% static 'images/avatars/avatar_150x150.jpg' %}" alt="Default Avatar"
class="img-fluid rounded-circle">
{% endif %}
If a user does not have an avatar, they see a placeholder.
Conclusion
That's how you add user avatars to Django without the common pitfalls: files go into organized folders (not one giant mess), old images get deleted automatically when users upload new ones, and your templates handle missing avatars gracefully. You've got the model field, the form, the view logic, and the template code everything you need for a production-ready app.
Need Help With Your Django Project?
If you're building an MVP and need guidance on file handling, user management, or Django architecture, I offer:
- Code reviews - Get expert feedback on your Django codebase
- Consulting calls - 1-hour sessions to solve blocking issues
- MVP development - Full-stack Django development for startups
Let's Work Together
I'm based in Victoria, British Columbia and I work with clients across North America. If you're an agency that needs a reliable Django contractor, or a startup trying to get something built, I'd love to hear from you.
Schedule a free 30-minute consultation to talk about your needs. No commitment required.