Статья mypy: основы и немного больше

Статический анализатор типов mypy предотвращает значительное количество возможных ошибок в коде на языке программирования Python. Главная его цель – предоставить возможность писать более надежный и легко поддерживаемый код.

mypy.png

Python:
def greet(name: str) -> str:
    return f"Hello, {name}!"

def add_numbers(x: int, y: int) -> int:
    return x + y

def get_length(s: str) -> int:
    return len(s)

def is_even(n: int) -> bool:
    return n % 2 == 0

Выше приведены простые примеры аннотаций типов функций. После имени аргумента через двоеточие указывается тип аргумента, а через стрелку после аргументов функции указывается тип возвращаемого значения.
Чтобы установить и протестировать mypy требуется сделать следующее:
  1. Устанавливаем mypy командой pip install mypy;
  2. Сохраняем код в файл, для примера, в mpy.py;
  3. Выполняем команду mypy mpy.py.
После того, как будет выполнена проверка кода из примера, mypy выведет в консоль следующее: Success: no issues found in 1 source file

Если же заменить тип возвращаемого значения greet на int, сделав программу некорректной, mypy выдаст следующую ошибку:
Bash:
mpy.py:2: error: Incompatible return value type (got "str", expected "int")  [return-value]
Found 1 error in 1 file (checked 1 source file)

Стандартные типы

К стандартным скалярным (не коллекциям) типам относятся следующие:

  • int – целочисленная переменная любой длины. Например: 42, -10, 0;
  • float – число с плавающей точкой. Например: 0.13, 5.0, math.pi;
  • bool – булево значение, True или False;
  • str – строка. Например: “Hello, world!”;
  • bytes – последовательность байт. Например: b”Hello”;
  • bytearray – изменяемый массив байт. Например: bytearray(b”Hello”);
  • None – аналог void типа в Си. Имеет None как единственное значение.
К стандартным дженерик типам относятся следующие:
  • List[T] – список, элементы которого имеют тип T;
  • Tuple[T1, T2, …, TN] – кортеж, первый элемент которого имеет тип T1, второго – T2 и так далее;
  • Dict[K, V] – словарь, ключи которого имеют тип K, а значения – тип V;
  • Optional[T, V] – тип T или None, аналогично Union[T, None];
  • Any – любой тип;
  • Union[T1, T2, …, TN] – любой тип из T1, T2, …, TN;
  • Callable[[T1, …], TResult] – функция, которая принимает аргументы типов T1, … и возвращает TResult.
Дженерик типы требуется импортировать из пакета typing. Используя вышеописанную информацию, мы можем написать функцию шифрования алгоритмом ROT13. Ниже будет приведена реализация, шифрующая только англоязычные тексты:
Python:
from typing import List

def rot13(b: bytes) -> bytes:
    result: List[int] = []
    for byte in b:
        if 65 <= byte <= 90:
            result.append((byte - 65 + 13) % 26 + 65)
        elif 97 <= byte <= 122:
            result.append((byte - 97 + 13) % 26 + 97)
        else:
            result.append(byte)
    return bytes(result)

def crypt(s: str) -> str:
    b: bytes = s.encode('ascii')
    roted: bytes = rot13(b)
    return roted.decode('ascii')

print(crypt("Hello, world!"))

Используя тип Union можно переписать в одну функцию следующим образом:
Python:
from typing import List, Union

def rot13(data: Union[bytes, str]) -> Union[bytes, str]:
    if isinstance(data, str):
        roted = rot13(data.encode('ascii'))
        if isinstance(roted, bytes):
            return roted.decode('ascii')
        else:
            return roted
    else:
        result: List[int] = []
        for byte in data:
            if 65 <= byte <= 90:
                result.append((byte - 65 + 13) % 26 + 65)
            elif 97 <= byte <= 122:
                result.append((byte - 97 + 13) % 26 + 97)
            else:
                result.append(byte)
        return bytes(result)

print(rot13("Hello, world!"))

Вывод такой же, “Uryyb, jbeyq!”. Но, как видно в коде, нам приходится явно проверять условие, что возвращаемое значение для байтов имеет тип bytes, а не str, хотя иначе, согласно нашему алгоритму, быть не может.

Перегрузки

Проблему из предыдущего примера можно решить, используя перегрузки:
Python:
from typing import List, Union, overload

@overload
def rot13(data: bytes) -> bytes: ...

@overload
def rot13(data: str) -> str: ...

def rot13(data: Union[bytes, str]) -> Union[bytes, str]:
   if isinstance(data, str):
       return rot13(data.encode('ascii')).decode('ascii')
   else:
       result: List[int] = []
       for byte in data:
           if 65 <= byte <= 90:
               result.append((byte - 65 + 13) % 26 + 65)
           elif 97 <= byte <= 122:
               result.append((byte - 97 + 13) % 26 + 97)
           else:
               result.append(byte)
       return bytes(result)

print(rot13("Hello, world!"))

Для перегрузок мы не указываем реализацию, вместо нее требуется писать многоточие. В финальной реализации мы можем обращаться к функции, предполагая, что ее возможные типы соответствуют перегрузкам. Это позволяет более точно обозначить зависимости между типами аргументов и типом результата. Однако, в реализации все равно придется писать Union’ы.

Настройки

У mypy имеется некоторое количество опций, полный список которых можно посмотреть в официальной документации ( ). Они могут быть полезны при разработке, так как позволяют задать "строгость" проверки.

Опции могут быть включены через явный параметр (пример: --strict), но удобнее содержать их в файлах. Mypy поддерживает следующие файлы конфигурации: mypy.ini, .mypy.ini, pyproject.toml и setup.cfg. Изначально он будет ожидать один из этих файлов в текущей директории.

Пример mypy.ini файла:
Код:
[mypy]
disallow_untyped_defs=True
no_implicit_optional=True
warn_return_any=True

Смысл опций из примера:
  • disallow_untyped_defs – запрещает объявлять функции без аннотации типов;
  • no_implicit_optional – без этой опции аргументы функции могут иметь тип None в качестве параметра по-умолчанию. С этой опцией такое неявное поведение запрещено, а гарантии типов сохранены;
  • warn_return_any – название говорит само за себя. С этой опцией mypy предупреждает, если в качестве возвращаемого типа было указано Any. Чаще всего Any ставится как временная заглушка, так что mypy поможет вам не забыть поменять заглушку на действительный тип.
Другие полезные опции:
  • check_untyped_defs – проверяет корректность типизации в функции без аннотаций. По-умолчанию mypy закрывает глаза на все, что происходит внутри таких функций. С этой опцией он проверяет корректность типизации всех операций и вызовов внутри;
  • strict – включает все опциональные дополнительные проверки;
  • warn_unreachable – сообщает, если какой-то кусок кода никогда не выполнается. Если так случилось, вероятно была допущена ошибка в проверке условий.

Заключение​

Mypy очень прост в изучение и использовании. Несмотря на простоту, он позволяет явным образом прописать используемые в проекте типы, предотвращая множество ошибок. Если вы случайно передали в функцию не тот аргумент, без mypy есть риск узнать об этом уже на стадии выполнения, возможно даже в продакшене.

Кроме того, для интеграции mypy с существующими средствами разработки уже существуют готовые плагины. Для PyCharm и VisualStudio Code они так и называются, их легко найти в маркетплейсе. Mypy может быть легко интегрирован в CI/CD и использоваться при сборке проекта.

Используйте статическую типизацию всегда, когда возможно – это довольно легко упростит вам жизнь с современными технологиями.
 
Последнее редактирование модератором:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!