Строгий режим в Python: от типов до валидации

Раздел: Основы Python -> Продвинутые возможности

Основное решение: строгая статическая типизация с mypy -strict

Как сделать код полностью типизированным и избежать ошибок типов?

Наиболее эффективный способ включить строгий режим в Python - использовать статический анализатор типов mypy с флагом --strict. Этот флаг включает все возможные проверки: обязательные аннотации для всех функций, запрет на использование Any, проверку совместимости типов и многое другое.

Пример файла example.py:


def greet(name) -> str:
    return "Hello, " + name

age = 25
if age > 18:
    result = greet(age)  # Ошибка: ожидается str, передаётся int

Strict python (строгий режим (strict) в python)

Запуск mypy в строгом режиме:

mypy --strict example.py
example.py:5: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

Для исправления необходимо явно аннотировать аргумент и переменную:

def greet(name: str) -> str:
    return "Hello, " + name

age: int = 25
if age > 18:
    result = greet(str(age))  # Теперь корректно

Проблема: Сторонние библиотеки могут не иметь аннотаций типов, что вызовет ошибки mypy.

Решение: Добавить файл pyproject.toml или mypy.ini для игнорирования конкретных модулей:


[mypy]
strict = True
ignore_missing_imports = True

[mypy-numpy.*]
ignore_missing_imports = True

Вариант 1: zip(strict=True) для контроля длины итераций

Как избежать незаметного обрезания списков при объединении?

Начиная с Python 3.10, функция zip() поддерживает аргумент strict=True, который вызывает ValueError, если итерируемые объекты разной длины.


names = ["Alice", "Bob", "Charlie"]
ages = [30, 25]

for name, age in zip(names, ages, strict=True):
    print(f"{name}: {age}")
ValueError: zip() argument 2 is shorter than argument 1

Без strict ошибка прошла бы незаметно:


for name, age in zip(names, ages):
    print(f"{name}: {age}")
Alice: 30
Bob: 25
# Charlie пропущен без уведомления

Проблема: strict=True доступен только в Python 3.10 и новее.

Решение: Использовать условную проверку или обёртку для старых версий.

Вариант 2: Замороженные датаклассы и __slots__

Как предотвратить случайное изменение атрибутов и экономить память?

Декоратор @dataclass(frozen=True) создаёт неизменяемые объекты. Добавление __slots__ = True запрещает создание новых атрибутов и уменьшает потребление памяти.


from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
p.x = 3.0  # Ошибка: cannot assign to field 'x'
p.z = 0.0  # Ошибка: 'Point' object has no attribute 'z'

Проблема: Полная неизменяемость может быть неудобна для объектов, которые должны обновляться.

Решение: Использовать __slots__ без frozen, чтобы предотвратить динамические атрибуты, но разрешить изменения.

Вариант 3: Преобразование предупреждений в ошибки

Как заставить интерпретатор прекращать выполнение при наличии предупреждений?

Модуль warnings позволяет превратить все предупреждения в исключения с помощью filterwarnings('error'). Это усиливает контроль над кодом.


import warnings
warnings.filterwarnings('error')

import numpy as np
arr = np.arange(10)
arr[-5:]  # Предупреждение о срезе с отрицательным шагом? (не критично)
# Но, например:
warnings.warn("Test warning")
UserWarning: Test warning
Traceback (most recent call last):
  ...
UserWarning: Test warning

Можно также использовать флаг интерпретатора:

python -W error script.py

Проблема: Многие библиотеки генерируют безвредные предупреждения (например, DeprecationWarning), которые могут прервать выполнение.

Решение: Применять выборочные фильтры:

warnings.filterwarnings('error', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=UserWarning, module='numpy')

Вариант 4: Строгая валидация с Pydantic

Как гарантировать корректность данных при внешнем вводе?

Библиотека pydantic (начиная с v2) поддерживает параметр strict=True в моделях, что запрещает неявное преобразование типов. Например, строка "123" не будет автоматически превращена в число.


from pydantic import BaseModel

class User(BaseModel, strict=True):
    name: str
    age: int

user = User(name="Alice", age="25")  # Ошибка: age должен быть int, а не str
pydantic_core.ValidationError: 1 validation error for User
age
  Input should be a valid integer [type=int_type, input_value='25', input_type=str]

Проблема: Строгий режим pydantic может быть излишним для внутренних данных, где преобразование допустимо.

Решение: Использовать strict только для точек входа (API, файлы конфигурации).

Расширенные примеры строгого режима

1. Настройка mypy --strict через pyproject.toml с игнорированием неаннотированных библиотек

Пример

# pyproject.toml
[tool.mypy]
strict = true
ignore_missing_imports = true
# Игнорировать только определённые модули
[[tool.mypy.overrides]]
module = ["numpy.*", "pandas.*"]
ignore_missing_imports = true

Результат: mypy не ругается на импорты numpy, но проверяет весь остальной код.

2. zip(strict=True) с генераторами и разными типами последовательностей

Пример

# Генератор и список разной длины
gen = (x**2 for x in range(5))
lists = [10, 20, 30]
try:
    for a, b in zip(gen, lists, strict=True):
        print(a, b)
except ValueError as e:
    print(f"Ошибка: {e}")
0 10
1 20
2 30
Ошибка: zip() argument 2 is shorter than argument 1

Генератор выдал 3 элемента, список содержит 3 элемента, но после их исчерпания генератор продолжает? На самом деле после третьего элемента генератор выдаёт ещё один элемент (3**2=9), но strict=True обнаруживает, что список закончился раньше. Важно: строгость проверяется после каждого шага.

3. Замороженный датакласс с наследованием и пользовательскими методами

Пример

from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Vehicle:
    wheels: int

@dataclass(frozen=True, slots=True)
class Car(Vehicle):
    model: str

car = Car(wheels=4, model="Tesla")
# car.wheels = 3  # Ошибка: frozen
# car.color = "red"  # Ошибка: slots

Результат: объект полностью неизменяем, дополнительные атрибуты запрещены.

4. Превращение предупреждений в ошибки с фильтрацией по модулю

Пример

import warnings
warnings.filterwarnings('error', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=DeprecationWarning, module='.*\.legacy_module')

# Пример вызова устаревшей функции
import numpy as np
np.matrix([[1,2],[3,4]])  # В NumPy 1.25+ это вызывает DeprecationWarning, но он перехватывается?
# Если модуль numpy не входит в игнорируемый, будет ошибка

Результат: DeprecationWarning из numpy превращается в исключение, если не указан фильтр для модуля.

5. Pydantic strict с кастомными валидаторами и вложенными моделями

Пример

from pydantic import BaseModel, field_validator

class Address(BaseModel, strict=True):
    city: str
    zip_code: str

class Person(BaseModel, strict=True):
    name: str
    age: int
    address: Address

    @field_validator('age')
    @classmethod
    def check_adult(cls, v):
        if v < 18:
            raise ValueError('Must be adult')
        return v

# Проверка
try:
    person = Person(name="Alice", age=17, address=Address(city="NY", zip_code="10001"))
except Exception as e:
    print(e)
1 validation error for Person
age
  Value error, Must be adult [type=value_error, input_value=17, input_type=int]

Возраст 17 не проходит валидацию, хотя тип int корректен.

Строгий режим (strict) в Python - comments

En
Strict python (python)