Django — веб-фреймворк на Python

Django — мощный высокоуровневый веб-фреймворк на Python. Он «из коробки» даёт админку, ORM, аутентификацию, формы и безопасность, поэтому на нём строят интернет-магазины, порталы и сервисы. Этот курс проведёт тебя от установки до продакшена.

Уровень: начальный 18 разделов 3–4 часа

1. Что такое Django и паттерн MTV

Django появился в 2005 году как внутренний инструмент новостного сайта и стал одним из самых популярных фреймворков. Его идеология — «batteries included»: вместо десятка отдельных библиотек ты получаешь всё необходимое сразу, включая инструменты для создания сайтов и API.

Архитектура Django основана на паттерне MTV (Model-Template-View):

Плюс третий «невидимый» элемент — URLconf: таблица соответствий «адрес страницы → view».

Запрос → URLconf → View (Python) → Model (база данных)
                  ↕                    ↕
               Template (HTML) ← данных подставлены
Если ты знаешь базовый Python, Django дастся легко: почти всю тяжёлую работу — маршрутизацию, работу с базой, формы, защиту — фреймворк уже сделал. Тебе остаётся описывать данные и логику.

2. Установка и создание проекта

Django ставится через pip. Работать лучше в виртуальном окружении, чтобы не засорять системный Python.

# Windows (PowerShell)
python -m venv .venv
.venv\Scripts\activate        # активация окружения
pip install django

# Linux / macOS
python3 -m venv .venv
source .venv/bin/activate
pip install django
django-admin startproject mysite
cd mysite
python manage.py runserver

Открой http://127.0.0.1:8000 — увидишь стартовую страницу Django. Команда runserver — встроенный сервер для разработки.

Структура проекта

ФайлНазначение
manage.pyглавная точка входа всех команд
mysite/settings.pyнастройки: база, приложения, язык
mysite/urls.pyкорневой URLconf
db.sqlite3база данных по умолчанию (SQLite)
Встроенный runserver не предназначен для продакшена: он медленный и небезопасный. Для реального сайта понадобится WSGI-сервер (gunicorn) и обратный прокси. Об этом в разделе 10.

3. Приложения Django

Проект — это целый сайт, а приложение — самостоятельный модуль внутри него (блог, каталог, корзина). Приложения можно переиспользовать между проектами как готовые блоки.

python manage.py startapp shop

Зарегистрируй новое приложение в файле настроек:

# mysite/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    ...
    "shop",  # наше приложение
]

Что создаёт startapp

ФайлНазначение
models.pyописание моделей (таблиц)
views.pyпредставления — логика страниц
urls.pyсвой URLconf приложения (файл создаётся вручную)
admin.pyрегистрация моделей в админке
migrations/история изменений структуры базы
tests.pyавтотесты
Большие сайты разбиваются на много приложений: shop, users, blog, orders. Это делает код понятным и удобно переиспользуемым. Один проект может объединять сколько угодно приложений.

4. Модели и миграции

Модель в Django — это класс Python, который описывает таблицу в базе данных. Поля — столбцы таблицы.

# shop/models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    category = models.ForeignKey(
        Category,
        on_delete=models.CASCADE,
        related_name="products",
    )
    in_stock = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

Теперь нужно создать и применить миграции — Django сам сгенерирует SQL на основе моделей:

python manage.py makemigrations
python manage.py migrate

Основные типы полей

ПолеЧто хранит
CharFieldкороткая строка (до max_length)
TextFieldдлинный текст
IntegerFieldцелое число
DecimalFieldденьги с точной точностью
DateTimeFieldдата и время
BooleanFieldTrue или False
ForeignKeyсвязь «многие к одному»
Деньги храни в DecimalField, а не в FloatField! Плавающая точка накапливает ошибки округления (0.1 + 0.2 ≠ 0.3), что для цен недопустимо.
Миграции — это история изменений структуры базы. Их обязательно коммитят в Git: команда на другой машине выполнит migrate и получит ту же структуру баз.

5. Админка Django

Django автоматически даёт готовый административный интерфейс. Нужно лишь создать суперпользователя и зарегистрировать модели.

python manage.py createsuperuser
# введи логин, email и пароль
# shop/admin.py
from django.contrib import admin
from .models import Category, Product

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ("name",)

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ("name", "price", "category", "in_stock")
    list_filter = ("category", "in_stock")
    search_fields = ("name",)

Запусти сервер и открой http://127.0.0.1:8000/admin. Войди под суперпользователем — уже можно добавлять категории и товары через удобный интерфейс без единой строки представлений.

Админка — это не только для программистов. Менеджеры и контент-менеджеры могут вести наполнение сайта, пока ты строишь публичные страницы.
Доступ к /admin всегда защищай сильным паролем и по возможности ограничивай по IP. Стандартный URL и простые пароли — первая цель атак.
# для тонкой настройки списка можно добавить кастомные поля
class ProductAdmin(admin.ModelAdmin):
    list_display = ("name", "price", "sale_price", "in_stock")

    @admin.display(description="Цена со скидкой")
    def sale_price(self, obj):
        return f"{obj.price * 0.9:.2f}"

6. ORM-запросы

ORM (Object-Relational Mapping) позволяет работать с базой как с Python-объектами, не написав ни строчки SQL. Запросы выполняются в Django shell.

python manage.py shell
# создать объект
c = Category.objects.create(name="Обувь")

# массовое создание
Product.objects.create(name="Кроссовки", price="4990.00", category=c)
Product.objects.create(name="Ботинки", price="6990.00", category=c)

# выборка
Product.objects.all()                       # все
Product.objects.filter(price__gt=5000)      # дороже 5000
Product.objects.exclude(in_stock=False)     # кроме распроданных
Product.objects.order_by("-price")          # по убыванию цены
Product.objects.count()                     # количество

Синтаксис условии строится через двойное подчёркивание: price__lt, name__icontains, created__year.

# фильтры через связи
obuv = Category.objects.get(name="Обувь")
obuv.products.all()                  # товары категории (related_name)

Product.objects.filter(category__name="Обувь")  # через связь

# агрегация
from django.db.models import Avg, Sum
Product.objects.aggregate(avg=Avg("price"), total=Sum("price"))
Запросы в Django «ленивые»: база не опрашивается, пока данные реально не понадобятся. Поэтому фильтры можно навешивать цепочкой, но аккуратно — случайный полный обход таблицы (queryset.all()) на больших данных тормозит.
Для проверки числа запросов к базе используй SELECT-логирование или инструменты вроде django-debug-toolbar. Проблему N+1 (запрос на каждый объект в цикле) решают через select_related и prefetch_related.

7. Представления и URL

Представление (view) получает запрос, берёт данные из базы и возвращает ответ. Связь URL с представлением задаётся в URLconf.

# shop/views.py
from django.shortcuts import render, get_object_or_404
from .models import Product, Category

def home(request):
    products = Product.objects.filter(in_stock=True)[:8]
    return render(request, "shop/home.html", {"products": products})

def product_detail(request, pk):
    product = get_object_or_404(Product, pk=pk)
    return render(request, "shop/detail.html", {"product": product})

def by_category(request, category_id):
    category = get_object_or_404(Category, pk=category_id)
    products = category.products.all()
    return render(request, "shop/catalog.html", {
        "category": category,
        "products": products,
    })
# shop/urls.py — создаём вручную
from django.urls import path
from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("product/<int:pk>/", views.product_detail, name="product-detail"),
    path("category/<int:category_id>/", views.by_category, name="by-category"),
]
# mysite/urls.py — подключаем приложение
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("shop.urls")),
]

Конвертеры URL

КонвертерПример URLТип в view
<int:pk>/product/42/int
<str:name>/category/sport/str
<slug:slug>/news/kak-rabotaet-django/str (slugs)
<path:rest>/files/a/b/c.txtstr с разделителями
Атрибут name у path позволяет ссылаться на маршрут по имени из кода и шаблонов через {% url "product-detail" pk=1 %}. При изменении адреса ничего в шаблонах менять не нужно.

8. Шаблоны

Шаблоны Django — это HTML с тегами подстановки данных. Файлы шаблонов по умолчанию Django ищет в папке templates внутри приложения.

<!-- shop/templates/shop/catalog.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
  <title>{{ category.name }} — магазин</title>
</head>
<body>
  <h1>Категория: {{ category.name }}</h1>
  {% for product in products %}
    <div>
      <a href="{% url 'product-detail' product.pk %}">
        {{ product.name }}
      </a> — {{ product.price }} руб.
      {% if not product.in_stock %}
        <b>(нет в наличии)</b>
      {% endif %}
    </div>
  {% empty %}
    <p>Товаров пока нет.</p>
  {% endfor %}
</body>
</html>

Основные теги шаблонов

СинтаксисНазначение
{{ переменная }}вывод значения
{% if %}условие
{% for %}цикл по списку
{% url 'name' %}ссылка по имени маршрута
{% include %}вставка другого шаблона
{% extends %}наследование базового шаблона
{{ value|upper }}фильтр (upper, length, date)
<!-- shop/templates/base.html — общий каркас -->
<html lang="ru">
<head><title>{% block title %}Магазин{% endblock %}</title></head>
<body>
  <header>Магазин{% if user.is_authenticated %} · {{ user.username }}{% endif %}</header>
  <main>{% block content %}{% endblock %}</main>
</body>
</html>

<!-- страница-потомок: -->
{% extends "base.html" %}
{% block content %}
  <h1>Категория: {{ category.name }}</h1>
  <!-- содержимое страницы -->
{% endblock %}
Django автоматически экранирует вывод переменных ({{ name }}), защищая от XSS. Если нужно вывести сырой HTML — используй фильтр |safe только для проверенных данных.

9. Формы и аутентификация

Django умеет строить формы из моделей и валидировать данные. Аутентификация уже встроена: регистрация, вход, выход — пара строк кода.

# shop/forms.py
from django import forms
from .models import Product

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ["name", "price", "category", "in_stock"]
        widgets = {
            "name": forms.TextInput(attrs={"class": "form-input"}),
        }
# shop/views.py — обработка формы
from django.shortcuts import render, redirect
from .forms import ProductForm

def product_create(request):
    if request.method == "POST":
        form = ProductForm(request.POST)
        if form.is_valid():
            product = form.save()
            return redirect("product-detail", pk=product.pk)
    else:
        form = ProductForm()
    return render(request, "shop/form.html", {"form": form})
<!-- шаблон формы с выводом ошибок -->
<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Сохранить</button>
</form>

Аутентификация подключается готовыми маршрутами:

# mysite/urls.py
from django.contrib.auth import views as auth_views

urlpatterns = [
    path("login/", auth_views.LoginView.as_view(), name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]

Права в представлениях проверяются декораторами: @login_required — только для авторизованных, @user_passes_test — по условию.

from django.contrib.auth.decorators import login_required

@login_required
def profile(request):
    return render(request, "shop/profile.html", {"user": request.user})
Тег {% csrf_token %} обязателен в каждой POST-форме Django, иначе запрос будет отклонён. Защита от CSRF уже встроена — не отключай её без крайней необходимости.
Пароли в базе Django хранит с over-хешированием и обрабатывает все типовые уязвимости за тебя. Никогда не храни пароли в открытом виде — даже исходники никогда не должны их видеть.

10. Статика, медиа и продакшен

CSS, JS и картинки интерфейса — это статические файлы. Они складываются в STATIC и собираются командой collectstatic перед деплоем. Файлы, загружаемые пользователями, — это медиа (MEDIA_ROOT, MEDIA_URL).

# mysite/settings.py
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
<!-- в шаблоне -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">

Перед деплоем нужно выключить режим отладки, собрать статику и запустить gunicorn:

# mysite/settings.py
DEBUG = False
ALLOWED_HOSTS = ["mydomain.ru"]
python manage.py collectstatic --noinput
pip install gunicorn          # WSGI-сервер (Linux)
gunicorn mysite.wsgi:application --bind 0.0.0.0:8000
# конфиг Nginx: отдача статики и прокси на gunicorn
location /static/ { root /home/me/mysite/; }
location /media/  { root /home/me/mysite/; }
location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
}
При DEBUG = False Django перестаёт сам раздавать статику и медиа — их отдаёт веб-сервер Nginx. Игнорирование этого даёт «страницу без стилей» и небезопасное поведение.
Секретный ключ SECRET_KEY и данные базы вынеси в переменные окружения. Для упаковки в одинаковую среду удобно использовать Docker, а автоматическую проверку кода настраивают через GitHub Actions.

11. Кэширование в Redis

Redis — быстрая in-memory база, стандарт для кэша Django. Устанавливаем байнд:

pip install django-redis
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
    }
}

Кэшируем представления или куски шаблонов:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)        # 15 минут для анонимных запросов
def catalog(request): ...
{% load cache %}
{% cache 300 latest_news %}
  ... тяжелая выборка новостей ...
{% endcache %}
Кэшируй самые «дорогие» страницы и агрегаты, а не всё подряд: лишние данные в Redis — это расход памяти и риск устаревших ответов.
После изменения модели чисти кэш: python manage.py cache_clear или инвалидация по ключам.

12. Фоновые задачи: Celery

Долгие операции (отправка письма, генерация PDF, разбор загрузки) выносят из HTTP-запроса в очередь. Celery + Redis — распространённая пара:

pip install celery
# tasks.py
from celery import shared_task

@shared_task
def send_welcome_email(user_id):
    user = User.objects.get(pk=user_id)
    send_mail("Добро пожаловать", "…", user.email)
def register(request):
    user = User.objects.create_user(...)
    send_welcome_email.delay(user.id)   # в очередь, не блокирует ответ
    return render(request, "done.html")
celery -A mysite worker -l info    # запуск воркера
ПриёмЗачем
.delay()отправить задачу в очередь
task.retryповторить при временной ошибке
Celery Beatпериодические задачи (рассылки, отчёты)
Result backendсохранить статус/результат задачи
Для простых проектов хватает транспорта Redis или даже системного cron; Celery оправдан, когда задач много и нужна надёжность очереди.

13. Тестирование: pytest-django

Тесты защищают от регрессий при росте проекта:

pip install pytest pytest-django
# tests/test_orders.py
import pytest
from django.urls import reverse
from .models import Product

@pytest.mark.django_db
def test_catalog_page_shows_products(client):
    Product.objects.create(title="Ноутбук", price=1000)
    resp = client.get(reverse("catalog"))
    assert resp.status_code == 200
    assert "Ноутбук" in resp.content.decode()
Подробнее о подходах к тестированию — в курсе pytest, а об автоматизации проверок — в GitHub Actions.

14. Безопасность и хардненинг

Базовая защита Django включается настройками и маленькими привычками:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
Проверка уязвимостей инструментами OWASP и сканерами — системная работа. Начни с курса Безопасность веб-приложений и чеклиста OWASP Top 10.

15. Продвинутые миграции

Миграции меняют не только схему, но и сами данные. Data migration выполняется через RunPython:

def uppercase_names(apps, schema_editor):
    Product = apps.get_model("shop", "Product")
    for p in Product.objects.all():
        p.title = p.title.title()
        p.save()

class Migration(migrations.Migration):
    dependencies = [("shop", "0002_alter_product_price")]
    operations = [migrations.RunPython(uppercase_names)]
python manage.py squashmigrations shop 0005
Data migration должна быть идемпотентной: повторный запуск на уже обработанных данных не должен ломать.

16. REST API на Django REST Framework

DRF превращает модели в API за минуты: сериализатор описывает поля, viewsets — CRUD-действия:

# serializers.py
from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ["id", "title", "price"]
# views.py
from rest_framework.viewsets import ModelViewSet
from .serializers import ProductSerializer

class ProductViewSet(ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
Ограничивай N+1: select_related/prefetch_related в queryset сериализатора спасает API от медленных ответов.

17. Кастомные теги шаблонов

Когда штатного синтаксиса шаблонов не хватает, пиши свой тег в templatetags/:

# shop/templatetags/money.py
from django import template
register = template.Library()

@register.simple_tag
def format_price(value):
    return f"{value:,.2f} ₽".replace(",", " ")
# в шаблоне
{% load money %}
<span>Цена: {% format_price product.price %}</span>
# settings.py
"context_processors": [
    "cart.context_processors.cart_total",
]
Не дублируй бизнес-логику в шаблонах: вычисления — в моделях и сервисах, шаблон только показывает.

18. Логирование и мониторинг

Продакшен без логов — «чёрный ящик». Настройка через LOGGING в settings:

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "root": {"handlers": ["console"], "level": "INFO"},
}
# Sentry в entrypoint-коде
import sentry_sdk
sentry_sdk.init(dsn="https://...@sentry.io/1", traces_sample_rate=0.1)
Не логируй пароли и токены: логи — не хранилище секретов, их читают и автоматизированные сканеры.

19. Веб-сокеты: Django Channels

Долгие обновления «в лоб» короткими запросами — боль. Channels добавляет WebSocket-слой поверх Django:

# consumers.py
class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.channel_layer.group_add("room", self.channel_name)
        await self.accept()

    async def receive(self, text_data):
        await self.channel_layer.group_send(
            "room", {"type": "msg", "data": text_data})
Фоновую работу отправляй в Celery: веб-сокет держит соединение, а не считает.

20. Поиск и фильтры: Q-объекты и django-filter

Фильтры и поиск быстро превращают ORM-код в «стену if». Ставь на библиотеки:

class ProductFilter(filters.FilterSet):
    name = filters.CharFilter(lookup_expr="icontains")
    price_min = filters.NumberFilter(field_name="price", lookup_expr="gte")
qs = Product.objects.filter(Q(category="books") | Q(tags__name=term))
Свободный поиск по большой таблице без индексов — полный скан: комбинируй фильтры с пагинацией и ограничением по категории.

21. Миграции данных: RunPython

Данные эволюционируют вместе со схемой. Перенос значений — через data-миграцию:

def update_slugs(apps, schema_editor):
    Product = apps.get_model("shop", "Product")
    for p in Product.objects.all():
        p.slug = slugify(p.name)
        p.save(update_fields=["slug"])

operations = [
    migrations.RunPython(update_slugs,
                         reverse_code=migrations.RunPython.noop),
]
Прогоняй миграцию на копии продуктива: дешевле, чем убирать следы на боевой базе.

22. Оркестр запросов: prefetch и индексы

Медленный Django-сайт в 90% случаев — N+1 и отсутствующие индексы:

products = Product.objects \
    .select_related("category") \
    .prefetch_related(Prefetch("reviews",
        queryset=Review.objects.order_by("-created")))
with connection.cursor() as c:
    c.execute("EXPLAIN ANALYZE SELECT ...")
Не вешай prefetch на всё подряд: для пагинации и личных данных отдельные запросы часто выгоднее.

Что дальше

Ты освоил путь от «Привет, мир» до рабочего сайта: модели, админку, ORM, представления, шаблоны, формы, аутентификацию и продакшен. Для роста попробуй REST API для Django по курсу REST API, освежи синтаксис в Python и разберись с базами в SQL. Деплой в деталях — в Хостинг и деплой.

← Вернуться к курсам