Загрузка картинки через форму в Django с нуля
Пошагово настраиваем форму с полем изображения, сохраняем файл в media и показываем его на странице. От модели до отображения — в Django 5.x за 30–40 минут.

К концу этой инструкции у вас будет рабочая страница: пользователь выбирает картинку через форму, нажимает «Загрузить», файл сохраняется на диск, а на той же странице появляется превью. Всё на чистом Django 5.x без сторонних пакетов, кроме одной обязательной библиотеки для работы с изображениями. По времени — 30–40 минут, если проект уже создан.
Что нужно иметь до начала
Проверьте это перед первым шагом, чтобы не упереться в середине:
- Python 3.10+ и установленный Django 5.x (
python -m django --versionдолжен показать 5.x). - Созданный проект и хотя бы одно приложение. Если приложения нет:
python manage.py startapp gallery. - Приложение добавлено в
INSTALLED_APPSв settings.py. - Установленная библиотека Pillow — без неё поле
ImageFieldвыбросит ошибку при валидации. Ставится командойpip install Pillow.
Pillow нужна именно для ImageField: Django через неё проверяет, что загруженный файл действительно картинка, а не переименованный exe. Для обычного FileField она не требуется.
Шаг 1. Настраиваем MEDIA в settings.py
Django должен знать, куда складывать загруженные файлы и по какому URL их отдавать. Откройте settings.py и добавьте в конец две строки:
# settings.py (Django 5.x)
from pathlib import Path
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_ROOT — папка на диске, где физически лежат файлы. MEDIA_URL — префикс URL, по которому браузер их запрашивает. Саму папку media создавать вручную не нужно — Django создаст её при первой загрузке.
Проверка: ошибок при python manage.py check быть не должно.
Шаг 2. Описываем модель с полем изображения
В models.py вашего приложения:
# gallery/models.py
from django.db import models
class Photo(models.Model):
title = models.CharField('Название', max_length=200)
# upload_to — подпапка внутри MEDIA_ROOT, куда ляжет файл
image = models.ImageField('Картинка', upload_to='photos/')
uploaded_at = models.DateTimeField('Загружено', auto_now_add=True)
def __str__(self):
return self.title
Параметр upload_to='photos/' означает, что файлы попадут в media/photos/. Можно передать и функцию для генерации имён по дате, но для начала строки достаточно.
Шаг 3. Применяем миграции
Модель есть — нужно создать под неё таблицу в базе:
python manage.py makemigrations
python manage.py migrate
Проверка: в выводе makemigrations должна появиться строка вроде Create model Photo, а migrate завершится без ошибок.
Шаг 4. Создаём форму
Создайте файл gallery/forms.py. Проще всего взять ModelForm — она сама возьмёт поля из модели:
# gallery/forms.py
from django import forms
from .models import Photo
class PhotoForm(forms.ModelForm):
class Meta:
model = Photo
fields = ['title', 'image']
Шаг 5. Пишем представление
Здесь легче всего ошибиться. Файлы приходят не в request.POST, а в request.FILES. Если забыть передать request.FILES в форму, картинка просто не сохранится, а ошибки не будет — форма молча посчитает поле пустым.
# gallery/views.py
from django.shortcuts import render, redirect
from .forms import PhotoForm
from .models import Photo
def upload_photo(request):
if request.method == 'POST':
# request.FILES — второй аргумент, без него файл не долетит
form = PhotoForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('upload_photo')
else:
form = PhotoForm()
photos = Photo.objects.order_by('-uploaded_at')
return render(request, 'gallery/upload.html', {
'form': form,
'photos': photos,
})
redirect после успешного сохранения нужен, чтобы обновление страницы браузером не отправило форму повторно.
Шаг 6. Шаблон с формой и превью
Создайте gallery/templates/gallery/upload.html. У тега form обязателен атрибут enctype="multipart/form-data" — без него браузер отправит только имя файла, а не сам файл.
<!-- gallery/templates/gallery/upload.html -->
<h1>Загрузка картинки</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Загрузить</button>
</form>
<h2>Загруженные</h2>
{% for photo in photos %}
<div>
<p>{{ photo.title }}</p>
<img src="{{ photo.image.url }}" alt="{{ photo.title }}" width="300">
</div>
{% endfor %}
photo.image.url отдаёт полный URL с учётом MEDIA_URL — вручную склеивать путь не нужно.
Три вещи ломают загрузку файлов у новичка чаще всего: забытыйenctype="multipart/form-data"в форме, забытыйrequest.FILESв представлении и ненастроенная раздача media в режиме разработки. Проверяйте их первыми.
Шаг 7. Настраиваем URL и раздачу media при разработке
Django в режиме разработки не раздаёт файлы из MEDIA_ROOT автоматически — это нужно включить вручную. В корневом urls.py проекта:
# urls.py (корневой)
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from gallery.views import upload_photo
urlpatterns = [
path('admin/', admin.site.urls),
path('upload/', upload_photo, name='upload_photo'),
]
# Раздача media только при DEBUG=True
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Конструкция static(...) работает только при DEBUG=True. На проде раздачей media должен заниматься Nginx или другой веб-сервер — это отдельная тема, здесь мы её не разбираем.
Проверка: запустите python manage.py runserver, откройте http://127.0.0.1:8000/upload/, выберите картинку, нажмите «Загрузить». Ниже формы должно появиться превью, а в папке media/photos/ — сам файл.
Если не получилось
Cannot use ImageField because Pillow is not installed— не установлена Pillow. Выполнитеpip install Pillowи перезапустите сервер.- Картинка не сохраняется, форма перезагружается без ошибок — в представлении забыт
request.FILESили в шаблоне забытenctype="multipart/form-data". Файл не долетает до сервера. - Превью не отображается, в консоли браузера 404 на
/media/...— не добавлен блокstatic(...)в urls.py илиDEBUG=False. Проверьте оба условия. SuspiciousFileOperationпри сохранении — обычно неверно заданMEDIA_ROOT(например, указывает за пределы проекта). Убедитесь, что путь абсолютный и внутриBASE_DIR.- Форма показывает «Enter a valid image» — файл повреждён или это не изображение. Pillow не смогла его открыть.
Что делать дальше
Базовая загрузка работает. Дальше стоит посмотреть в сторону нескольких вещей: ограничить размер и формат загружаемых файлов через валидатор на поле формы; добавить генерацию превью нужного размера (для этого понадобится обработка через Pillow при сохранении); настроить раздачу media на проде через Nginx или облачное хранилище, потому что static() в продакшене отключён намеренно. Прежде чем открывать загрузку публично, посмотрите раздел о валидации и безопасности файлов в официальной документации Django — приём файлов от анонимных пользователей требует ограничений.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Django documentation: Managing files, Django documentation: File Uploads
Частые вопросы
А если у меня Django 4.x, всё это сработает?
Нужна ли Pillow, если я использую FileField, а не ImageField?
Почему картинка не появляется, хотя файл в папке media есть?
Можно ли обойтись без модели и просто сохранить файл?
Что нужно поменять, чтобы это заработало на боевом сервере?
Как ограничить размер загружаемого файла?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.