Своя management-команда в Django: пишем шаг за шагом
За 20 минут вы соберёте рабочую команду manage.py, которая принимает аргументы, пишет в лог и запускается по расписанию. От пустого файла до вызова из cron.

К концу этого гайда у вас будет своя команда, которую можно запускать так: python manage.py cleanup_sessions --days 30. Она примет аргумент, пройдётся по базе, выведет понятный отчёт и корректно завершится с нужным кодом возврата. На всё уйдёт минут двадцать, если Django уже стоит и проект запускается. Проверено на Django 5.x, но API команд не менялся с версии 1.8, так что подойдёт и для 4.x, и для 3.x.
Что нужно иметь до начала
Перед первым шагом убедитесь, что у вас есть:
- рабочий проект Django, который запускается через
python manage.py runserverбез ошибок; - хотя бы одно приложение, добавленное в
INSTALLED_APPSв настройках. Команда живёт внутри приложения, а не сама по себе; - активированное виртуальное окружение — команды запускаются в том же окружении, где стоит Django;
- доступ к редактированию файлов проекта (не только через админку, а именно к коду).
Если python manage.py help выводит список команд вроде migrate и runserver — всё готово, можно начинать.
Шаг 1. Создаём структуру папок
Django ищет пользовательские команды в строго определённом месте: внутри приложения должна быть папка management/commands/. Пути другими быть не могут — это не настройка, а соглашение фреймворка.
Пусть ваше приложение называется core. Создайте такую структуру:
core/
__init__.py
models.py
views.py
management/
__init__.py
commands/
__init__.py
cleanup_sessions.py
Оба файла __init__.py — и в management/, и в commands/ — обязательны, даже пустые. Без них Python не увидит пакеты, а Django не найдёт команду. Имя файла cleanup_sessions.py станет именем команды.
Проверка: выполните python manage.py help. В самом низу, в разделе с именем вашего приложения ([core]), должна появиться строка cleanup_sessions. Если раздела нет — вернитесь и проверьте __init__.py.
Шаг 2. Пишем минимальную команду
Любая команда — это класс Command, унаследованный от BaseCommand, с методом handle. Начнём с самого простого варианта в файле cleanup_sessions.py:
# core/management/commands/cleanup_sessions.py
# Django 5.x
from django.core.management.base import BaseCommand
class Command(BaseCommand):
# Текст показывается в python manage.py help cleanup_sessions
help = "Удаляет истёкшие сессии из базы"
def handle(self, *args, **options):
self.stdout.write("Команда запущена")
Обратите внимание: вывод идёт через self.stdout.write, а не через print. Так Django сможет перенаправить вывод при тестах и корректно обработает кодировку.
Проверка: запустите python manage.py cleanup_sessions. В консоли появится строка «Команда запущена». Если видите Unknown command — файл лежит не в той папке.
Шаг 3. Добавляем аргумент
Команде нужен параметр — за сколько дней считать сессию истёкшей. Аргументы описывают в методе add_arguments. Django использует стандартный argparse из Python, так что синтаксис привычный.
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Удаляет истёкшие сессии из базы"
def add_arguments(self, parser):
parser.add_argument(
"--days",
type=int,
default=30,
help="Удалять сессии старше указанного числа дней",
)
def handle(self, *args, **options):
days = options["days"]
self.stdout.write(f"Порог: {days} дней")
Имя аргумента без дефисов (days) становится ключом в словаре options. Флаг с двумя дефисами (--days) — необязательный, у него есть default. Если написать имя без дефисов в add_argument, аргумент станет обязательным позиционным.
Проверка: python manage.py cleanup_sessions --days 7 выведет «Порог: 7 дней», а запуск без флага — «Порог: 30 дней».
Шаг 4. Делаем полезную работу
Теперь наполним handle реальной логикой. Django из коробки хранит сессии в модели Session, и у неё есть менеджер, умеющий чистить истёкшие записи. Воспользуемся им.
from django.contrib.sessions.models import Session
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
class Command(BaseCommand):
help = "Удаляет истёкшие сессии из базы"
def add_arguments(self, parser):
parser.add_argument("--days", type=int, default=30)
def handle(self, *args, **options):
days = options["days"]
cutoff = timezone.now() - timedelta(days=days)
# Сессии, срок жизни которых истёк раньше cutoff
expired = Session.objects.filter(expire_date__lt=cutoff)
count = expired.count()
expired.delete()
# SUCCESS красит вывод в зелёный в поддерживающих терминалах
self.stdout.write(
self.style.SUCCESS(f"Удалено сессий: {count}")
)
Метод self.style.SUCCESS — часть BaseCommand, он оборачивает текст в цветовые коды. Есть также self.style.ERROR и self.style.WARNING. Не используйте их для вывода, который потом парсится скриптом: в файл цветовые коды попадут мусором.
Проверка: запустите команду. Вы увидите «Удалено сессий: N». Число зависит от того, сколько записей в таблице django_session старше порога.
Шаг 5. Код возврата и обработка ошибок
Если команда запускается по расписанию, важно, чтобы при сбое она возвращала ненулевой код — иначе cron или CI будут считать провал успехом. Для управляемых ошибок в Django есть CommandError: он печатает сообщение в stderr и завершает процесс с кодом 1.
from django.core.management.base import BaseCommand, CommandError
# ... внутри handle:
if days < 0:
raise CommandError("Число дней не может быть отрицательным")
Не ловите свои ошибки голым except и не глотайте их: если внутри упадёт запрос к базе, пусть падает команда — вы увидите трейсбек и код возврата 1. Именно на это опирается мониторинг.
Команда — это точка входа, а не место для бизнес-логики. Держите в handle разбор аргументов и вывод, а саму работу выносите в функцию или метод модели. Тогда ту же логику можно вызвать из Celery-задачи или теста, не эмулируя запуск команды.
Шаг 6. Запуск по расписанию
Команда готова, теперь её надо вызывать регулярно. Через cron строка выглядит так (запуск каждый день в 4 утра):
0 4 * * * cd /path/to/project && /path/to/venv/bin/python manage.py cleanup_sessions --days 30 >> /var/log/cleanup.log 2>&1
Полные пути обязательны: cron запускается с минимальным окружением и не знает, где ваш venv и проект. 2>&1 направит ошибки в тот же лог, что и обычный вывод.
Если не получилось
Unknown command: 'cleanup_sessions'— файл лежит внеmanagement/commands/или пропущен один из__init__.py. Ещё вариант: приложениеcoreне добавлено вINSTALLED_APPS.ImportErrorпри запуске — опечатка в импортах или обращение к моделям внеhandle. Импорты моделей внутри команды лучше держать на верхнем уровне файла, но обращаться к базе — только внутриhandle, иначе запрос выполнится при загрузке.- Команда есть в
help, но не запускается — класс назван неCommandили не унаследован отBaseCommand. Имя класса должно быть строгоCommand. - В cron работает не так, как в консоли — почти всегда дело в путях или переменной
DJANGO_SETTINGS_MODULE. Проверьте, что запускаете тем же интерпретатором изvenv. type=intругается на аргумент — вы передали не число:--days abc. argparse проверяет тип до вызоваhandleи сам выводит понятную ошибку.
Что делать дальше
Базовая команда работает. Следующие шаги, когда понадобятся:
- добавьте флаг
--dry-run, чтобы команда показывала, что удалит, но ничего не трогала — полезно перед первым боевым запуском; - для команд, работающих с транзакциями, оберните изменения в
transaction.atomic; - чтобы уровень детализации вывода зависел от флага
--verbosity, читайте его изoptions["verbosity"]— Django добавляет этот аргумент ко всем командам автоматически; - если команда должна вызываться из кода, а не только из консоли, используйте
call_commandизdjango.core.management— это упрощает написание тестов.
Подробности по всем возможностям BaseCommand смотрите в разделе документации о пользовательских командах для вашей версии Django.
Сочинение: Тема, тезис, аргументы, готовый текст нужного объёма
Спросить за 12 ₽Источники: Django docs — Writing custom django-admin commands, Django docs — BaseCommand reference
Частые вопросы
У меня Django 4.2, а не 5.x. Всё сработает?
Команда не появляется в python manage.py help. Что проверить в первую очередь?
Можно ли положить команды в отдельное приложение только под них?
Почему self.stdout.write, а не обычный print?
Как сделать так, чтобы при ошибке команда завершалась с кодом 1?
Команда работает в консоли, но падает или ведёт себя иначе в cron. В чём дело?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.