Upload images in Django

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 image

Build 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.

django upload image
django image upload

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 None

This 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:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.