Проверка типов в Python

Python — строго типизированный динамический язык программирования. Типы определяются динамически, поэтому вы можете задавать значения переменных напрямую, не определяя их тип, как в статически типизированных языках, таких как Java.

name = "Michael"
String name = "Michael";
Строгая типизацияСлабая типизация
Динамическая типизацияPython
Clojure
Ruby
Erlang
PHP
Perl
JavaScript
VB
Статическая типизацияC#
Java
Scala
Haskel
C
C++

Строгая и при этом динамическая типизация означает, что типы определяются во время выполнения программы, но смешивать их нельзя. Например, a = 1 + '0' в Python приведет к ошибке. С другой стороны, JavaScript является динамическим языком со слабой типизацией, поэтому типы определяются во время выполнения программы и при этом их можно смешивать. Например, a = 1 + '0' установит для a значение 10.

Хотя динамическая типизация обеспечивает гибкость, она не всегда желательна, поэтому в последнее время было предпринято множество попыток привнести в динамические языки статическое определение типов.

В этой статье мы рассмотрим, что такое подсказки типов и чем они могут быть полезны. Мы также рассмотрим, как можно использовать систему типов Python для статической проверки типов с помощью mypy и проверки типов во время выполнения с помощью pydantic, marshmallow и typeguard.

Содержание

Инструменты проверки типов

Существует множество инструментов, использующих подсказки типов (type hints) для проверки типов (как статической, так и во время выполнения программы).

Статическая проверка типов

Проверка типов во время выполнения программы

Инструменты, заточенные на определенные проекты

Полный список инструментов можно найти в репозитории Awesome Python Typing.

Подсказки типов

Подсказки типов были добавлены в Python в версии 3.5.

Они позволяют разработчикам аннотировать ожидаемые типы для переменных, параметров и возвратов функций. И хотя такие типы не навязываются интерпретатором Python, они дают ряд преимуществ.

Прежде всего, с помощью подсказок типов вы можете лучше выразить свое намерение относительно того, что должен делать ваш код и как его использовать. Лучшее понимание приводит к уменьшению количества ошибок.

Например, у вас есть следующая функция для расчета среднесуточной температуры:

def daily_average(temperatures):
    return sum(temperatures) / len(temperatures)

Если вы предоставите список температур в таком виде, функция будет работать, как и предполагалось, и вернет ожидаемый результат:

average_temperature = daily_average([22.8, 19.6, 25.9])
print(average_temperature)  # => 22.76666666666667

Что произойдет, если вы вызовете функцию со словарем, где ключами будут временные метки измерений, а значениями — показатели температуры?

average_temperature = daily_average({1599125906: 22.8, 1599125706: 19.6, 1599126006: 25.9})
print(average_temperature)  # => 1599125872.6666667

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

Чтобы избежать подобной путаницы, можно добавить подсказки типов, аннотировав аргумент и возвращаемое значение:

def daily_average(temperatures: list[float]) -> float:
    return sum(temperatures) / len(temperatures)

Теперь определение функции говорит нам:

  • temperatures — это список значений с плавающей запятой: temperatures: list[float]
  • функция должна возвращать значение с плавающей запятой: -> float
print(daily_average.__annotations__)
# {'temperatures': list[float], 'return': <class 'float'>}

Подсказки типов позволяют использовать инструменты статической проверки типов. Редакторы кода и IDE также используют их, предупреждая вас, когда использование определенной функции или метода не соответствует ожиданиям в соответствии с подсказками типов.

Итак, подсказки типов — это всего лишь «подсказки». Другими словами, они не так строги, как определения типов в статически типизированных языках. Но несмотря на их гибкость, они все равно помогают улучшить качество кода, позволяя выразить намерения более четко. Кроме того, вы можете использовать множество инструментов, чтобы извлечь из них еще больше пользы.

Аннотации типов и подсказки типов

Аннотации типов — это просто синтаксис для аннотирования переменных, а также входящих и выходящих данных функций:

def sum_xy(x: 'an integer', y: 'another integer') -> int:
    return x + y


print(sum_xy.__annotations__)
# {'x': 'an integer', 'y': 'another integer', 'return': <class 'int'}

Подсказки типов строятся поверх аннотаций, чтобы сделать их более полезными. Подсказки и аннотации часто используются как взаимозаменяемые понятия, но это разные вещи.

Модуль typing

Вам может быть интересно, почему иногда вы видите такой код:

from typing import List


def daily_average(temperatures: List[float]) -> float:
    return sum(temperatures) / len(temperatures)

В нем используется встроенный float для определения возвращаемого типа функции, но List импортируется из модуля typing.

До версии Python 3.9 интерпретатор Python не поддерживал использование встроенных функций с аргументами для подсказки типов.

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

def daily_average(temperatures: list) -> float:
    return sum(temperatures) / len(temperatures)

Но определить ожидаемый тип элементов списка (list[float]) без модуля typing было невозможно. То же самое можно сказать о словарях и других последовательностях и сложных типах:

from typing import Tuple, Dict


def generate_map(points: Tuple[float, float]) -> Dict[str, int]:
    return map(points)

Кроме того, модуль typing позволяет определять новые типы, псевдонимы типов, тип Any и многое другое.

Например, вы можете захотеть разрешить использование нескольких типов. Для этого можно использовать Union:

from typing import Union


def sum_ab(a: Union[int, float], b: Union[int, float]) -> Union[int, float]:
    return a + b

Начиная с Python 3.9 можно использовать встроенные функции таким образом:

def sort_names(names: list[str]) -> list[str]:
    return sorted(names)

Начиная с Python 3.10, вы можете использовать | для определения объединений типов:

def sum_ab(a: int | float, b: int | float) -> int | float:
    return a + b

Статическая проверка типов с помощью mypy

mypy — это инструмент для проверки типов во время компиляции.

Вы можете установить его так же, как и любой другой пакет Python:

$ pip install mypy

Чтобы проверить ваш модуль Python, вы можете запустить его следующим образом:

$ python -m mypy my_module.py

Итак, давайте снова рассмотрим пример с daily_average:

def daily_average(temperatures):
    return sum(temperatures) / len(temperatures)


average_temperature = daily_average(
    {1599125906: 22.8, 1599125706: 19.6, 1599126006: 25.9}
)

При проверке такого кода с помощью mypy ошибок не будет, так как функция не использует подсказки типов:

Success: no issues found in 1 source file

Добавьте подсказки типов:

def daily_average(temperatures: list[float]) -> float:
    return sum(temperatures) / len(temperatures)


average_temperature = daily_average(
    {1599125906: 22.8, 1599125706: 19.6, 1599126006: 25.9}
)

Запустите mypy снова:

$ python -m mypy my_module.py

Вы должны увидеть следующее:

my_module.py:6: error: Argument 1 to "daily_average" has incompatible
type "Dict[int, float]"; expected "List[float]"  [arg-type]

Found 1 error in 1 file (checked 1 source file)

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

Также полезно добавить mypy в конвейер CI (конвейер непрерывной интеграции), чтобы проверять типизацию перед слиянием или развертыванием кода.

Хотя это большое улучшение качества кода, статическая проверка типов не обеспечивает принудительное соблюдение типов во время выполнения программы. Именно поэтому у нас есть средства проверки типов во время выполнения, которые мы рассмотрим далее.

mypy поставляется с typeshed, который содержит внешние аннотации типов для стандартной библиотеки Python, встроенных модулей Python, а также пакетов сторонних разработчиков.

mypy проверяет Python-программы практически без накладных расходов во время выполнения. Хотя он проверяет типы, утиная типизация все равно происходит. Поэтому mypy нельзя использовать для компиляции расширений CPython.

Проверка типов во время выполнения программы

Статические средства проверки типов не помогают при работе с данными из внешних источников, например, с поступающими от пользователей вашего приложения. Именно здесь на помощь приходят средства проверки типов во время выполнения.

pydantic

Одним из инструментов проверки типов во время выполнения является pydantic, который используется для проверки данных. Он выдает ошибки валидации, если предоставленные данные не соответствуют типу, определенному с помощью подсказки типа.

pydantic использует приведение типов для преобразования входных данных, чтобы заставить их соответствовать ожидаемому типу.

$ pip install pydantic

Инструмент довольно прост в использовании. Например, давайте определим класс Song с несколькими атрибутами:

from datetime import date

from pydantic import BaseModel


class Song(BaseModel):
    id: int
    name: str
    release: date
    genres: list[str]

При инициализации нового объекта Song с правильными данными все работает, как и ожидалось:

song = Song(
    id=101,
    name='Bohemian Rhapsody',
    release='1975-10-31',
    genres=[
        'Hard Rock',
        'Progressive Rock'
    ]
)
print(song)
# id=101 name='Bohemian Rhapsody' release=datetime.date(1975, 10, 31)
# genres=['Hard Rock', 'Progressive Rock']

Однако, когда мы пытаемся инициализировать новый Song с недопустимыми данными ('1975-31-31'), возникает ошибка ValidationError:

song = Song(
    id=101,
    name='Bohemian Rhapsody',
    release='1975-31-31',
    genres=[
        'Hard Rock',
        'Progressive Rock'
    ]
)
print(song)
# pydantic_core._pydantic_core.ValidationError: 1 validation error for Song
# release
#   Input should be a valid date or datetime, month value is outside expected range
#     of 1-12 [type=date_from_datetime_parsing, input_value='1975-31-31', input_type=str]
#     For further information visit
#     https://errors.pydantic.dev/2.5/v/date_from_datetime_parsing

С помощью pydantic мы можем гарантировать, что в нашем приложении будут использоваться только те данные, которые соответствуют определенным типам. Это приводит не только к уменьшению количества ошибок, но и к написанию меньшего количества тестов. Благодаря использованию таких инструментов, как pydantic, нам не нужно писать тесты для случаев, когда пользователь отправляет совершенно неправильные данные. С этим справится pydantic: будет выдана ошибка ValidationError.

Например, FastAPI проверяет тела HTTP-запросов и ответов с помощью pydantic:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


@app.post('/items/', response_model=Item)
async def create_item(item: Item):
    return item

Обработчик create_item ожидает в качестве данных name (string) и price (float). Объект ответа должен выглядеть так же. Если с предоставленными данными что-то не так, сразу же возникает ошибка. Если бы подобная ошибка возникала позже, это усложнило бы отладку и определение того, откуда взялись данные неправильного типа. Кроме того, так как ошибка обрабатывается pydantic, вы можете держать свои обработчики маршрутов в чистоте.

Наряду с использованием подсказок типов для проверки данных вы также можете добавить пользовательские валидаторы, которые будут следить не только за правильностью типов. Добавить пользовательскую проверку для атрибута довольно просто. Например, чтобы предотвратить дублирование жанров в классе Song, вы можете добавить валидацию следующим образом:

from datetime import date

from pydantic import BaseModel, field_validator


class Song(BaseModel):
    id: int
    name: str
    release: date
    genres: list[str]

    @field_validator('genres')
    def no_duplicates_in_genre(cls, v):
        if len(set(v)) != len(v):
            raise ValueError(
                'No duplicates allowed in genre.'
            )
        return v


song = Song(
    id=101,
    name='Bohemian Rhapsody',
    release='1975-10-31',
    genres=[
        'Hard Rock',
        'Progressive Rock',
        'Progressive Rock',
    ]
)
print(song)
# pydantic_core._pydantic_core.ValidationError: 1 validation error for Song
# genres
#   Value error, No duplicates allowed in genre. [type=value_error,
#     input_value=['Hard Rock', 'Progressiv...ck', 'Progressive Rock'], input_type=list]
#     For further information visit https://errors.pydantic.dev/2.5/v/value_error

Итак, метод валидации no_duplicates_in_genre должен быть декорирован при помощи field_validator, который принимает в качестве аргумента имя атрибута. Метод валидации должен быть методом класса, поскольку валидация происходит до создания экземпляра. Для данных, не прошедших валидацию, должна возникать стандартная ошибка Python ValueError.

Вы также можете использовать методы валидатора для изменения значения перед проверкой. Для этого используйте mode='before':

@field_validator('genres', mode='before')

Например, вы можете преобразовать жанры в нижний регистр следующим образом:

from datetime import date

from pydantic import BaseModel, field_validator


class Song(BaseModel):
    id: int
    name: str
    release: date
    genres: list[str]

    @field_validator('genres', mode='before')
    def to_lower_case(cls, v):
        return [genre.lower() for genre in v]

    @field_validator('genres')
    def no_duplicates_in_genre(cls, v):
        if len(set(v)) != len(v):
            raise ValueError(
                'No duplicates allowed in genre.'
            )
        return v


song = Song(
    id=101,
    name='Bohemian Rhapsody',
    release='1975-10-31',
    genres=[
        'Hard Rock',
        'PrOgReSsIvE ROCK',
        'Progressive Rock',
    ]
)
print(song)
# pydantic_core._pydantic_core.ValidationError: 1 validation error for Song
# genres
#   Value error, No duplicates allowed in genre.
#     [type=value_error, input_value=['Hard Rock', 'PrOgReSsIv...CK', 'Progressive Rock'], input_type=list]
#     For further information visit https://errors.pydantic.dev/2.5/v/value_error

to_lower_case переводит каждый элемент списка genres в нижний регистр. Из-за mode='before' этот метод вызывается до того, как pydantic проверит типы. Все жанры преобразуются в нижний регистр, а затем проверяются с помощью no_duplicates_in_genre.

pydantic также предлагает более строгие типы, такие как StrictStr и EmailStr, чтобы сделать вашу валидацию еще лучше. Подробнее об этом читайте в документации «Типы полей».

Marshmallow

Еще один инструмент, заслуживающий упоминания, — marshmallow. Он помогает проверять сложные данные и загружать данные в нативные типы Python (и выгружать из них). Установка такая же, как и для любого другого пакета Python:

$ pip install marshmallow

Как и в случае с pydantic, вы можете добавить проверку типов в класс:

from marshmallow import Schema, fields, post_load


class Song:
    def __init__(
            self,
            id,
            name,
            release,
            genres
    ):
        self.id = id
        self.name = name
        self.release = release
        self.genres = genres

    def __repr__(self):
        return (
            f'<Song(id={self.id}, name={self.name}), '
            f'release={self.release.isoformat()}, genres={self.genres}>'
        )


class SongSchema(Schema):
    id = fields.Int()
    name = fields.Str()
    release = fields.Date()
    genres = fields.List(fields.String())

    @post_load
    def make_song(self, data, **kwargs):
        return Song(**data)


external_data = {
    'id': 101,
    'name': 'Bohemian Rhapsody',
    'release': '1975-10-31',
    'genres': ['Hard Rock', 'Progressive Rock']
}

song = SongSchema().load(external_data)
print(song)
# <Song(id=101, name=Bohemian Rhapsody), release=1975-10-31, genres=['Hard Rock', 'Progressive Rock']>

В отличие от pydantic, marshmallow не использует приведение типов, поэтому вам нужно определить схему (schema) и класс отдельно. Например, дата выпуска в external_data должна быть строкой ISO. С объектом datetime это не работает.

Чтобы включить десериализацию данных в объект Song, нужно добавить в схему метод, декорированный @post_load:

class SongSchema(Schema):
    id = fields.Int()
    name = fields.Str()
    release = fields.Date()
    genres = fields.List(fields.String(), validate=no_duplicates)

    @post_load
    def make_song(self, data, **kwargs):
        return Song(**data)

Схема проверяет данные и, если все поля валидны, создает экземпляр класса, вызывая make_song с проверенными данными.

Как и в pydantic, вы можете добавить пользовательские валидации для каждого атрибута из схемы. Например, вы можете предотвратить дубликаты следующим образом:

import datetime

from marshmallow import Schema, fields, post_load, ValidationError


class Song:
    def __init__(
            self,
            id,
            name,
            release,
            genres
    ):
        self.id = id
        self.name = name
        self.release = release
        self.genres = genres

    def __repr__(self):
        return (
            f'<Song(id={self.id}, name={self.name}), '
            f'release={self.release.isoformat()}, genres={self.genres}>'
        )


def no_duplicates(genres):
    if isinstance(genres, list):
        genres = [
            genre.lower()
            for genre in genres
            if isinstance(genre, str)
        ]

        if len(set(genres)) != len(genres):
            raise ValidationError(
                'No duplicates allowed in genres.'
            )


class SongSchema(Schema):
    id = fields.Int()
    name = fields.Str()
    release = fields.Date()
    genres = fields.List(fields.String(), validate=no_duplicates)

    @post_load
    def make_song(self, data, **kwargs):
        return Song(**data)


external_data = {
    'id': 101,
    'name': 'Bohemian Rhapsody',
    'release': '1975-10-31',
    'genres': ['Hard Rock', 'Progressive Rock', 'ProgressivE Rock']
}

song = SongSchema().load(external_data)
print(song)
# marshmallow.exceptions.ValidationError:
# {'genres': ['No duplicates allowed in genres.']}

Как видите, вы можете использовать либо pydantic, либо marshmallow, чтобы обеспечить правильный тип данных в процессе работы вашего приложения. Выберите тот, который больше подходит к вашему стилю.

Typeguard

В то время как pydantic и marshmallow сосредоточены на проверке и сериализации данных, у typeguard в фокусе проверка типов при вызове функций. В то время как mypy просто выполняет статическую проверку типов, typeguard проверяет типы во время работы программы.

$ pip install typeguard

Давайте рассмотрим тот же пример, что и раньше — класс Song. На этот раз мы определим его метод __init__ с аргументами с подсказкой типа:

from datetime import date

from typeguard import typechecked


@typechecked
class Song:

    def __init__(
            self,
            id: int,
            name: str,
            release: date,
            genres: list[str]

    ) -> None:
        self.id = id
        self.name = name
        self.release = release
        self.genres = genres


song = Song(
    id=101,
    name='Bohemian Rhapsody',
    release=date(1975, 10, 31),
    genres={
        'Hard Rock',
        'Progressive Rock',
    }
)
print(song)
# typeguard.TypeCheckError: argument "genres" (set) is not a list

Декоратор typechecked можно использовать как для классов, так и для функций, когда вы хотите обеспечить проверку типов во время выполнения. Запуск этого кода вызовет ошибку TypeError, поскольку жанры представляют собой множество, а не список. Аналогично можно использовать декоратор для функций, например:

from typeguard import typechecked

@typechecked
def sum_ab(a: int, b: int) -> int:
    return a + b

typechecked поставляется с плагином pytest. Чтобы проверить типы для пакета my_package во время выполнения тестов, можно выполнить следующую команду:

$ python -m pytest --typeguard-packages=my_package

При работе с pytest вам не нужно использовать декоратор @typechecked. Таким образом, вы можете декорировать свои функции и классы так, чтобы они проверяли типы во время выполнения программы или только во время выполнения тестов. В любом случае, typeguard может стать мощной защитой для вашего приложения.

Flask c pydantic

Итак, давайте соберем все это вместе в веб-приложение. Как упоминалось выше, FastAPI по умолчанию использует pydantic. Хотя во Flask нет встроенной поддержки pydantic, мы можем использовать связывания (bindings), чтобы добавить его в наши API. Поэтому давайте создадим новый проект Flask, чтобы увидеть все это в действии.

Сначала создайте новую папку:

$ mkdir flask_example
$ cd flask_example

Затем инициализируйте свой проект с помощью Poetry:

$ poetry init
Package name [flask_example]:
Version [0.1.0]:
Description []:
Author [Your name <your@email.com>, n to skip]:
License []:
Compatible Python versions [^3.12]:  >3.12

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Do you confirm generation? (yes/no) [yes]

После этого добавьте Flask, Flask-Pydantic и pytest:

$ poetry add flask Flask-Pydantic
$ poetry add --dev pytest

Создайте файл test_app.py для хранения наших тестов :

import json

import pytest

from app import app


@pytest.fixture
def client():
    app.config['TESTING'] = True

    with app.test_client() as client:
        yield client


def test_create_todo(client):
    response = client.post(
        '/todos/',
        data=json.dumps(
            {
                'title': 'Wash the dishes',
                'done': False,
                'deadline': '2020-12-12'
            }
        ),
        content_type='application/json'
    )

    assert response.status_code == 201


def test_create_todo_bad_request(client):
    response = client.post(
        '/todos/',
        data=json.dumps(
            {
                'title': 'Wash the dishes',
                'done': False,
                'deadline': 'WHENEVER'
            }
        ),
        content_type='application/json'
    )

    assert response.status_code == 400

Здесь у нас есть два теста для создания новых todo. Один проверяет, что когда все в порядке , возвращается статус 201. Другой проверяет, что если предоставленные данные не соответствуют ожиданиям, возвращается статус 400.

Далее добавьте файл для приложения Flask под названием app.py:

import datetime

from flask import Flask, request
from flask_pydantic import validate
from pydantic import BaseModel

app = Flask(__name__)


class CreateTodo(BaseModel):
    title: str
    done: bool
    deadline: datetime.date


class Todo(BaseModel):
    title: str
    done: bool
    deadline: datetime.date
    created_at: datetime.datetime


@app.route('/todos/', methods=['POST'])
@validate(body=CreateTodo)
def todos():
    todo = Todo(
        title=request.body_params.title,
        done=request.body_params.done,
        deadline=request.body_params.deadline,
        created_at=datetime.datetime.now()
    )

    return todo, 201


if __name__ == '__main__':
    app.run()

Мы определили конечную точку для создания todo, а также схему запроса CreateTodo и схему ответа Todo. В результате, когда в API отправляются данные, не соответствующие схеме запроса, возвращается статус 400 с ошибками валидации в теле. Теперь вы можете запустить тесты, чтобы проверить, что ваш API действительно ведет себя так, как описано:

$ poetry run pytest

Запуск инструментов проверки типов

Теперь, когда вы познакомились с инструментами, нужно выяснить, когда их следует использовать.

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

  • Во время написания кода (в вашей IDE или редакторе кода)
  • Во время коммита (с помощью pre-commit хуков)
  • Когда код проверяется в системе контроля исходного кода (через конвейер CI)
  • Во время выполнения программы (инструменты проверки во время выполнения)

Запуск инструментов внутри IDE или редактора кода

Лучше всего искать ошибки, которые могут негативно повлиять на качество, как можно раньше и чаще. Поэтому рекомендуется проводить статическую проверку кода во время разработки.

Во многие популярные IDE встроен mypy или mypy-подобные статические программы проверки типов. Для тех, у кого их нет, скорее всего, есть плагин. Такие плагины в режиме реального времени предупреждают вас о нарушениях типов и потенциальных ошибках программирования.

Ресурсы:

  1. Adding type hints in PyCharm
  2. Type hints in Visual Studio Code
  3. Sublime Text Package Finder

Pre-commit хуки

Поскольку в процессе написания кода вы неизбежно пропустите какие-то предупреждения, хорошая практика — проверять код на проблемы с типами во время коммита с помощью pre-commit git-хуков. Так вы сможете избежать коммита кода, который не пройдет проверку типов в вашем CI-конвейере.

Для управления git-хуками рекомендуется использовать фреймворк pre-commit.

$ pip install pre-commit

После установки добавьте в проект файл конфигурации pre-commit под названием .pre-commit-config.yaml. Чтобы запустить mypy, добавьте следующий конфиг:

repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: 'v1.7.1'
    hooks:
      - id: mypy

Наконец, настройте скрипты git-хуков:

(venv)$ pre-commit install

Теперь каждый раз, когда вы запускаете git commit, mypy будет выполняться до фактической фиксации. И если возникнут проблемы, коммит будет прерван.

Конвейер CI

Имеет смысл запускать статические проверки типов в CI-конвейере, чтобы предотвратить попадание проблем с типами в кодовую базу. Это, вероятно, самый важный момент для запуска mypy или другого статического инструмента проверки типов.

Вы можете столкнуться с проблемами при выполнении статических проверок типов с помощью mypy, особенно при использовании сторонних библиотек, без подсказок типов. Это, вероятно, основная причина, по которой многие люди избегают запуска проверок mypy внутри CI-конвейера.

Запуск инструментов проверки типов во время выполнения программы

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

Как уже говорилось, использование таких инструментов требует меньшего количества тестов, приводит к меньшему количеству ошибок и помогает обнаружить их на ранней стадии. Вы можете использовать их для проверки данных (pydantic и marshmallow) и для принудительного приведения типов во время выполнения программы (typeguard).

Заключение

Проверка типов может показаться ненужной, когда база кода небольшая, но чем больше она становится, тем такие проверки важнее. Это еще один слой, который защищает нас от легко предотвратимых ошибок. Подсказки типов, хотя они и не навязываются интерпретатором, помогают лучше выразить смысл переменной, функции или класса.

Большинство современных IDE и редакторов кода предоставляют плагины, которые уведомляют разработчиков о несоответствии типов, ориентируясь на подсказки типов.

Чтобы обеспечить корректность типов, мы можем включить в наш рабочий процесс mypy для статической проверки. Но нужно учитывать, что наше программное обеспечение взаимодействует с внешним миром. Поэтому рекомендуется добавлять программы проверки типов во время выполнения, такие как pydantic или marshmallow. Они помогают проверять вводимые пользователем данные и выявлять ошибки на самых ранних стадиях. Ведь чем быстрее вы обнаружите ошибку, тем проще ее исправить и двигаться дальше.

Перевод статьи «Python Type Checking».