As a developer working on a Django project where users needed to upload profile pictures and product images. The challenge was implementing a robust image upload system that handles validation, storage, and display efficiently.
I’ve encountered various scenarios requiring file uploads. In this comprehensive guide, I’ll share the most effective methods I’ve used to handle image uploads in Django applications.
Method 1: Basic Image Upload Using Django Forms
This is the easy approach I use for simple image upload requirements. It leverages Django’s built-in form handling capabilities.
Set Up the Model
First, I’ll create a model to store image information. Here’s the model I typically use:
# models.py
from django.db import models
from django.contrib.auth.models import User
import os
def user_directory_path(instance, filename):
"""File will be uploaded to MEDIA_ROOT/user_<id>/<filename>"""
return f'user_{instance.user.id}/{filename}'
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
profile_image = models.ImageField(
upload_to=user_directory_path,
default='default_images/default_profile.jpg',
help_text='Upload your profile picture'
)
bio = models.TextField(max_length=500, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.username}'s Profile"
def delete(self, *args, **kwargs):
"""Delete the image file when the model instance is deleted"""
if self.profile_image and self.profile_image != 'default_images/default_profile.jpg':
if os.path.isfile(self.profile_image.path):
os.remove(self.profile_image.path)
super().delete(*args, **kwargs)Create the Upload Form
Next, I create a form to handle the image upload process:
# forms.py
from django import forms
from .models import UserProfile
from PIL import Image
class ProfileImageForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['profile_image', 'bio']
widgets = {
'profile_image': forms.FileInput(attrs={
'class': 'form-control',
'accept': 'image/*'
}),
'bio': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Tell us about yourself...'
})
}
def clean_profile_image(self):
image = self.cleaned_data.get('profile_image')
if image:
# Check file size (limit to 5MB)
if image.size > 5 * 1024 * 1024:
raise forms.ValidationError('Image file too large (maximum 5MB)')
# Check image dimensions
img = Image.open(image)
if img.width > 2048 or img.height > 2048:
raise forms.ValidationError('Image dimensions too large (maximum 2048x2048)')
# Validate file type
valid_extensions = ['.jpg', '.jpeg', '.png', '.gif']
ext = os.path.splitext(image.name)[1].lower()
if ext not in valid_extensions:
raise forms.ValidationError('Invalid file type. Please upload JPG, PNG, or GIF images only.')
return imageBuild the Views
Here’s how I handle the image upload in views:
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from .models import UserProfile
from .forms import ProfileImageForm
import json
@login_required
def upload_profile_image(request):
"""Handle profile image upload"""
profile, created = UserProfile.objects.get_or_create(user=request.user)
if request.method == 'POST':
form = ProfileImageForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
# Delete old image if exists
if profile.profile_image and profile.profile_image != 'default_images/default_profile.jpg':
try:
os.remove(profile.profile_image.path)
except OSError:
pass
form.save()
messages.success(request, 'Profile image updated successfully!')
return redirect('profile_view')
else:
messages.error(request, 'Please correct the errors below.')
else:
form = ProfileImageForm(instance=profile)
context = {
'form': form,
'profile': profile
}
return render(request, 'accounts/upload_image.html', context)
@login_required
def ajax_upload_image(request):
"""Handle AJAX image upload for better user experience"""
if request.method == 'POST':
profile, created = UserProfile.objects.get_or_create(user=request.user)
form = ProfileImageForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
profile = form.save()
return JsonResponse({
'success': True,
'image_url': profile.profile_image.url,
'message': 'Image uploaded successfully!'
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})
return JsonResponse({'success': False, 'message': 'Invalid request'})
def profile_view(request):
"""Display user profile with uploaded image"""
profile = get_object_or_404(UserProfile, user=request.user)
return render(request, 'accounts/profile.html', {'profile': profile})Create the Template
This HTML template provides a user-friendly interface for uploading and previewing profile images with drag-and-drop support and AJAX-based submission.
<!-- templates/accounts/upload_image.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Upload Profile Image</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.image-preview {
max-width: 300px;
max-height: 300px;
margin: 20px 0;
}
.upload-area {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 40px;
text-align: center;
margin: 20px 0;
}
.upload-area.dragover {
border-color: #007bff;
background-color: #f8f9fa;
}
</style>
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h3>Upload Profile Image</h3>
</div>
<div class="card-body">
<!-- Display current image -->
{% if profile.profile_image %}
<div class="text-center">
<img src="{{ profile.profile_image.url }}"
alt="Current Profile"
class="img-thumbnail image-preview"
id="currentImage">
</div>
{% endif %}
<!-- Upload form -->
<form method="post" enctype="multipart/form-data" id="uploadForm">
{% csrf_token %}
<div class="upload-area" id="uploadArea">
<i class="fas fa-cloud-upload-alt fa-3x mb-3"></i>
<h4>Drag & Drop or Click to Upload</h4>
<p class="text-muted">Supports JPG, PNG, GIF up to 5MB</p>
{{ form.profile_image }}
</div>
<div class="mb-3">
<label for="{{ form.bio.id_for_label }}" class="form-label">Bio</label>
{{ form.bio }}
{% if form.bio.errors %}
<div class="text-danger">
{% for error in form.bio.errors %}
<small>{{ error }}</small>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Preview area -->
<div id="imagePreview" class="text-center" style="display: none;">
<img id="previewImg" src="" alt="Preview" class="img-thumbnail image-preview">
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload"></i> Upload Image
</button>
<a href="{% url 'profile_view' %}" class="btn btn-secondary">Cancel</a>
</div>
</form>
<!-- Progress bar -->
<div class="progress mt-3" id="progressBar" style="display: none;">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// File input change handler
$('#id_profile_image').change(function(e) {
const file = e.target.files[0];
if (file) {
previewImage(file);
}
});
// Drag and drop functionality
const uploadArea = $('#uploadArea');
uploadArea.on('dragover', function(e) {
e.preventDefault();
$(this).addClass('dragover');
});
uploadArea.on('dragleave', function(e) {
e.preventDefault();
$(this).removeClass('dragover');
});
uploadArea.on('drop', function(e) {
e.preventDefault();
$(this).removeClass('dragover');
const files = e.originalEvent.dataTransfer.files;
if (files.length > 0) {
$('#id_profile_image')[0].files = files;
previewImage(files[0]);
}
});
// Image preview function
function previewImage(file) {
const reader = new FileReader();
reader.onload = function(e) {
$('#previewImg').attr('src', e.target.result);
$('#imagePreview').show();
};
reader.readAsDataURL(file);
}
// AJAX form submission
$('#uploadForm').submit(function(e) {
e.preventDefault();
const formData = new FormData(this);
const progressBar = $('#progressBar');
const progress = progressBar.find('.progress-bar');
progressBar.show();
$.ajax({
url: "{% url 'ajax_upload_image' %}",
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() {
const xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
const percentComplete = evt.loaded / evt.total * 100;
progress.css('width', percentComplete + '%');
}
}, false);
return xhr;
},
success: function(response) {
progressBar.hide();
if (response.success) {
$('#currentImage').attr('src', response.image_url);
alert('Image uploaded successfully!');
window.location.href = "{% url 'profile_view' %}";
} else {
alert('Error: ' + JSON.stringify(response.errors));
}
},
error: function() {
progressBar.hide();
alert('An error occurred during upload.');
}
});
});
});
</script>
</body>
</html>Setting Up URLs
Here’s the URL configuration I use:
# urls.py
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import views
urlpatterns = [
path('upload-image/', views.upload_profile_image, name='upload_profile_image'),
path('ajax-upload/', views.ajax_upload_image, name='ajax_upload_image'),
path('profile/', views.profile_view, name='profile_view'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)This example shows how to implement a user profile image upload in Django with both standard form submission and AJAX for a smoother user experience.
It also includes validation, previews, and automatic deletion of old images for a clean, professional setup.


Method 2: Multiple Image Upload with Validation
For projects requiring multiple image uploads, I use this enhanced approach. It’s particularly useful for e-commerce sites or galleries.
Enhanced Model for Multiple Images
Defines product and related image models with unique filenames and primary image management.
# models.py
from django.db import models
from django.contrib.auth.models import User
import uuid
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
def product_image_path(instance, filename):
"""Generate unique filename for product images"""
ext = filename.split('.')[-1]
filename = f"{uuid.uuid4()}.{ext}"
return f'products/{instance.product.id}/{filename}'
class ProductImage(models.Model):
product = models.ForeignKey(Product, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(upload_to=product_image_path)
alt_text = models.CharField(max_length=200, blank=True)
is_primary = models.BooleanField(default=False)
order = models.PositiveIntegerField(default=0)
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['order', 'uploaded_at']
def save(self, *args, **kwargs):
# Ensure only one primary image per product
if self.is_primary:
ProductImage.objects.filter(
product=self.product,
is_primary=True
).exclude(pk=self.pk).update(is_primary=False)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
# Delete the actual file
if self.image:
try:
os.remove(self.image.path)
except OSError:
pass
super().delete(*args, **kwargs)
Multiple Upload Form
Creates Django forms and formsets to handle multiple image uploads with validation.
# forms.py
from django import forms
from .models import Product, ProductImage
from django.forms import inlineformset_factory
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['name', 'description', 'price']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
'price': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'})
}
class ProductImageForm(forms.ModelForm):
class Meta:
model = ProductImage
fields = ['image', 'alt_text', 'is_primary']
widgets = {
'image': forms.FileInput(attrs={
'class': 'form-control',
'accept': 'image/*',
'multiple': True
}),
'alt_text': forms.TextInput(attrs={'class': 'form-control'}),
'is_primary': forms.CheckboxInput(attrs={'class': 'form-check-input'})
}
# Create formset for multiple images
ProductImageFormSet = inlineformset_factory(
Product,
ProductImage,
form=ProductImageForm,
extra=3,
can_delete=True
)
class MultipleImageUploadForm(forms.Form):
images = forms.FileField(
widget=forms.ClearableFileInput(attrs={
'multiple': True,
'class': 'form-control',
'accept': 'image/*'
})
)
def clean_images(self):
images = self.files.getlist('images')
if len(images) > 10:
raise forms.ValidationError('Maximum 10 images allowed')
for image in images:
if image.size > 5 * 1024 * 1024: # 5MB limit
raise forms.ValidationError(f'File {image.name} is too large')
return images
Enhanced Views for Multiple Upload
Manages product creation and bulk image uploads with resizing and transactional safety.
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from django.db import transaction
from .models import Product, ProductImage
from .forms import ProductForm, ProductImageFormSet, MultipleImageUploadForm
import os
from PIL import Image
import io
from django.core.files.uploadedfile import InMemoryUploadedFile
@login_required
def create_product_with_images(request):
"""Handle product creation with multiple images"""
if request.method == 'POST':
product_form = ProductForm(request.POST)
image_formset = ProductImageFormSet(request.POST, request.FILES)
if product_form.is_valid() and image_formset.is_valid():
try:
with transaction.atomic():
product = product_form.save(commit=False)
product.created_by = request.user
product.save()
image_formset.instance = product
images = image_formset.save(commit=False)
for i, image_obj in enumerate(images):
# Resize image if too large
resized_image = resize_image(image_obj.image)
if resized_image:
image_obj.image = resized_image
image_obj.order = i
image_obj.save()
messages.success(request, 'Product created with images successfully!')
return redirect('product_detail', pk=product.pk)
except Exception as e:
messages.error(request, f'Error creating product: {str(e)}')
else:
messages.error(request, 'Please correct the errors below.')
else:
product_form = ProductForm()
image_formset = ProductImageFormSet()
context = {
'product_form': product_form,
'image_formset': image_formset
}
return render(request, 'products/create_product.html', context)
@login_required
def bulk_image_upload(request, product_id):
"""Handle bulk image upload for existing product"""
product = get_object_or_404(Product, id=product_id, created_by=request.user)
if request.method == 'POST':
form = MultipleImageUploadForm(request.POST, request.FILES)
if form.is_valid():
images = request.FILES.getlist('images')
for image in images:
# Resize image
resized_image = resize_image(image)
ProductImage.objects.create(
product=product,
image=resized_image or image,
alt_text=f"{product.name} image"
)
return JsonResponse({
'success': True,
'message': f'{len(images)} images uploaded successfully!'
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})
return JsonResponse({'success': False, 'message': 'Invalid request'})
def resize_image(image_field, max_size=(800, 800)):
"""Resize image if larger than max_size"""
try:
img = Image.open(image_field)
if img.width > max_size[0] or img.height > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Save resized image
output = io.BytesIO()
img_format = img.format or 'JPEG'
img.save(output, format=img_format, quality=85)
output.seek(0)
return InMemoryUploadedFile(
output, 'ImageField',
f"{image_field.name.split('.')[0]}_resized.{img_format.lower()}",
f'image/{img_format.lower()}',
output.tell(), None
)
except Exception as e:
print(f"Error resizing image: {e}")
return NoneThis code provides a robust way to manage multiple image uploads with validation, resizing, and ordering, ideal for products or galleries. It ensures efficient storage and a smooth user experience by handling image size limits and enforcing one primary image per product.
Throughout this guide, I’ve shared three different approaches I use depending on project requirements. The basic method works perfectly for simple applications, while the multiple upload approach handles more complex scenarios like e-commerce platforms.
For production applications serving thousands of users, I always recommend the cloud storage method. It scales better and reduces server load significantly.
You may also like to read:
- Payment Gateway Integration with Django
- How to Add Items to Cart in Django in Python?
- Python Filter Not in Django
- Union Operation on Django Models

I am Bijay Kumar, a Microsoft MVP in SharePoint. Apart from SharePoint, I started working on Python, Machine learning, and artificial intelligence for the last 5 years. During this time I got expertise in various Python libraries also like Tkinter, Pandas, NumPy, Turtle, Django, Matplotlib, Tensorflow, Scipy, Scikit-Learn, etc… for various clients in the United States, Canada, the United Kingdom, Australia, New Zealand, etc. Check out my profile.