2025, Sep 30 17:16
Почему PyRight ругается на cleaned_data.get в ModelForm.clean и как это решить
PyRight в Django сообщает reportOptionalMemberAccess: clean может вернуть None по django-stubs. Объясняем причину и решение через assert и сужение типа.
В проектах на Django PyRight иногда помечает вызов cleaned_data.get внутри реализации ModelForm.clean предупреждением reportOptionalMemberAccess. Сам код работает, автодополнение по моделям ORM доступно, но именно эта строка вызывает предупреждение. Причина не в вашем редакторе; она в типах, на которых основан проверяющий инструмент.
Пример проблемы
Шаблон знаком: получить словарь, возвращаемый super().clean(), и прочитать из него значение.
from django.forms import ModelForm
class TicketForm(ModelForm):
    def clean(self):
        normalized_map = super().clean()
        kickoff_date = normalized_map.get("start_date")
PyRight сообщает: "get" — неизвестный атрибут у "None" (reportOptionalMemberAccess).
Почему так происходит
Типы, предоставляемые django-stubs, явно описывают clean как метод, который может вернуть None:
class BaseForm(RenderableFormMixin): # ... def clean(self) -> dict[str, Any] | None: ...
Такой необязательный тип возвращаемого значения существует, потому что подклассы формы вправе не возвращать словарь. В трекере обсуждалось: «Подклассы Form могут не возвращать словарь, поэтому тип в форме должен допускать None». FormSet — пример, где это поведение встречается. Исходя из этого контракта, PyRight предполагает, что результат super().clean() может быть None, и вызов .get на нём небезопасен без сужения типа.
Возможно, вы увидите разницу между PyRight CLI и тем, что наблюдаете в VS Code, но независимо от интерфейса предупреждение продиктовано информацией о типах из django-stubs.
Решение: явно учесть Optional и сузить тип
Самый быстрый способ удовлетворить проверку — сузить Optional в месте использования. assert одновременно даёт понять и читателю, и анализатору типов, что при обращении значение не равно None.
from django.forms import ModelForm
class TicketForm(ModelForm):
    def clean(self):
        normalized_map = super().clean()
        assert normalized_map is not None
        kickoff_date = normalized_map.get("start_date")
Это изменение согласует код с аннотированным API: clean может вернуть dict или None, а ваша реализация доказывает проверяющему, что вы используете его как dict только после проверки на None.
Почему это важно
Статический анализ работает ровно настолько хорошо, насколько точны контракты, которые он применяет. Здесь django-stubs кодируют реальное поведение: clean вправе вернуть None. PyRight указывает на эту возможность, чтобы вы случайно не разыменовали None. Делая эту возможность явной, вы получаете более понятный и безопасный код форм и избегаете несогласованностей между инструментами.
Выводы
Если PyRight предупреждает о reportOptionalMemberAccess при работе с cleaned_data, он следует контракту типов из django-stubs. Рассматривайте значение как Optional и сузьте тип перед вызовом .get. Такая простая проверка делает ваши формы одновременно корректными относительно стабов и надёжными во время выполнения.
Статья основана на вопросе на StackOverflow от guettli и ответе willeM_ Van Onsem.