Привет, Хабр! Меня зовут Роман, я программист в команде «Гравитон» и моя задача в компании — разработка API/CLI интерфейсов и приложений на языке Python.
Цель данной статьи в том, чтобы показать как через внедрение «типовых подходов» и вспомогательных библиотек/утилит очень сильно упрощается поддержка проекта в настоящем (при передаче другому человеку или разработке в команде) и в будущем (возобновили проект спустя время).
Первое, что нужно сказать — проект на Python это не только сам Python, но и множество технологий используемых вместе (Git, Docker, и т.д.). В этой статье мы сконцентрируемся на самом проекте, а именно с чего начать, что прикрутить, что учитывать при разработке.
Создание проекта
Начало начал
Все и вся начинается с создания проекта и для этого будем использовать менеджер проектов uv.
Вот несколько важных особенностей uv:
Кроссплатформенность. Никакой боли с установкой и последующей разработкой под
macOS,Linux,Windows;Установка различных версий интерпретаторов —
CPython,PyPy,GraalPy;Файл блокировки
uv.lock, наличие которого гарантирует отсутствие ситуации «на моей тачке все работало»;Подробная справка (да-да, такой справке позавидуют очень многие утилиты).
Чтобы инициализировать проект, в терминале выполняем команду:
uv init --name temp_project --vcs git --description "test app"
Где флаги:
--name— устанавливает имя проекта;--vcs— устанавливает систему контроля версий проекта (на момент создания статьи поддерживается 2 параметра —gitилиnone);--description— устанавливает описание проекта.
Далее нужно синхронизировать виртуальное окружение, в котором будет работать наш проект, и установить зависимости.
uv sync && \ uv add typer pydantic pydantic-settings && \ uv add ruff mypy --group lint
В результате содержимое корневой директории проекта:
├── .venv ├── .gitignore ├── main.py ├── pyproject.toml ├── .python-version └── README.md
Этого явно недостаточно. Допилим дерево, чтобы получить типовую структуру проекта.
├── .venv ├── .gitignore ├── pyproject.toml ├── .python-version ├── README.md ├── src │ └── temp_project │ ├── core │ │ ├── config.py │ │ ├── constants.py │ │ ├── __init__.py │ │ └── models │ │ ├── __init__.py │ │ └── settings.py │ ├── database │ │ └── __init__.py │ ├── __init__.py │ ├── main.py │ ├── models │ │ └── __init__.py │ ├── services │ │ └── __init__.py │ └── utils │ └── __init__.py └── uv.lock
Что мы видим:
.gitignore— файл определения путей (к файлам и директориям) в проекте, которые должны быть игнорированы системой контроля версий;pyproject.toml— файл определения конфигурации проекта;.python-version— файл указывающий версию Python для использования (интерпретатор CPython в нашем случае);README.md— файл описания проекта, знакомит с проектом, объясняет его назначение, структуру и использование;uv.lock— файл блокировки, содержащий точную информацию о зависимостях проекта;src— директория с исходным кодом проектов;temp_project— текущий проект;.venv— директория с виртуальным окружением проекта.
Дерево проекта
Пройдемся по дереву проекта, чтобы понять, что к чему.
src/temp_project/main.py— главный управляющий модуль (или модуль точки входа).Если разрабатываем
CLI, тогда модуль будет содержать функции, которые будут командами для использования в терминале. Если разрабатываемAPI, тогда модуль будет содержать функциюmain, в которой выполняются предварительные настройки и запуск.src/temp_project/core/config.py— модуль конфигураций.Здесь находится все, что относится к настройкам. Это может быть функция возврата объекта настроек (с чтением
.envфайла), функция инициализации объекта логера и т.д.src/temp_project/core/constants.py— модуль констант (неожиданно правда?).Хранит в себе все часто используемые конструкции, которые должны быть неизменяемыми. Например скомпилированные регулярные выражения (
re.compile), числовые значения и т.д. Ниже в статье поговорим подробно.src/temp_project/models/settings.py— модуль моделей настроек (ну вот это точно неожиданно).Тут храним все модели (классы), по смыслу относящиеся к настройкам.
src/temp_project/database— директория, агрегирующая в себе все, что связано с базой данных.Здесь находятся модели таблиц в базе данных, с которыми требуется взаимодействие, а также инициализации объекта соединения с базой данных.
src/temp_project/models— директория, агрегирующая в себе модели данных, которые требуются для работы.Например, сделали мы запрос к сервису и, чтобы работать с ответом эффективно (а главное контролируемо), нужно сопоставить ответ с моделью данных (то есть с каким-то образцом, в котором указаны поля, которые обязательно должны быть в ответе). В модели данных также может быть реализована валидация данных до и после инициализации объекта модели.
src/temp_project/services— директория, агрегирующая в себе модули прикладного функционала.В функционал может входить создание сделки, пользователя и т.д. Именно в данных модулях задействуются модели из
src/models.src/temp_project/utils— директория, агрегирующая в себе вспомогательные модули.Например, здесь хранится функция, реализующая повторное выполнение другой функции через заданный интервал времени при возникновении исключения. Такая техника (или стратегия) называется backoff и по сути это функция-декоратор для другой функции, у которой мы хотим реализовать повторный вызов. Такая функция очень нужна, но не несет прикладной функциональности.
Разработка
Здесь рассмотрим практики в разработке, чтобы уберечься от будущей боли в голове.
Константы
В процессе разработки неизбежно появляются константы, это могут быть регулярные выражения, числовые и строковые значения и т.д. Главное свойство констант (сейчас внимание) — их постоянство и неизменяемость (вот это да).
Казалось бы все просто, если тип данных у константы неизменяемый, тогда и сама константа будет неизменяемой. Так-то оно так, но вот только от переопределения это не защитит.
>>> CONSTANT_ONE = "qwe" >>> print(CONSTANT_ONE) qwe >>> CONSTANT_ONE = 1234 >>> print(CONSTANT_ONE) 1234
Вся проблема в том, что верхний регистр служит лишь соглашением для людей, что вот это константа, ее нельзя менять, а Python думает, что мы просто переопределили значение переменной.
Выход — хранение констант в специальном модуле (в нашем случае src/temp_project/core/constants.py).
"""Модуль констант приложения.""" # src/temp_project/core/constants.py CONSTANT_ONE = "qwe"
"""Главный управляющий модуль.""" # src/temp_project/main.py from core.constants import CONSTANT_ONE def main() -> None: """Главная управляющая функция.""" print(f"Значение константы CONSTANT_ONE до изменения: {CONSTANT_ONE}") CONSTANT_ONE = 1234 print(f"Значение константы CONSTANT_ONE после изменения: {CONSTANT_ONE}") if __name__ == "__main__": main()
В результате выполнения команды python3 src/temp_project/main.py будет вызвано исключение:
Traceback (most recent call last): File ".../src/temp_project/main.py", line 14, in <module> main() ~~~~^^ File ".../src/temp_project/main.py", line 9, in main print(f"Значение константы CONSTANT_ONE до изменения: {CONSTANT_ONE}") ^^^^^^^^^^^^ UnboundLocalError: cannot access local variable 'CONSTANT_ONE' where it is not associated with a value
Смысл в том, что имена, к которым выполняется присваивание (=) внутри функции, по умолчанию считаются локальными. И получается, что мы пытаемся отобразить значение локальной переменной, до того, как присвоили значение.
И это хорошо! Такое поведение по умолчанию сильно упрощает процесс разработки. Чтобы изменить поведение по умолчанию, нужно добавить в самом начале функции main строку global CONSTANT_ONE. В таком случае мы явно объявляем, что переменная, с который мы работаем, является глобальной.
И вот первая практика — не использовать global пока нет 100% уверенности, что без этого никак. В данной функции все очень просто, и сразу видно, где проблема, но вот если проект на 5к+ строк то становится, очень грустно.
И сразу вторая практика — для констант всегда использовать неизменяемые типы данных. А что если нужна константа словарь? Словарь является изменяемым типом данных и в классическом виде непригоден быть константой.
Выход — использовать тип MappingProxyType.
"""Модуль констант приложения.""" # src/temp_project/core/constants.py from types import MappingProxyType NUM_UNIT = MappingProxyType( { "B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4, } )
"""Главный управляющий модуль.""" # src/temp_project/main.py from core.constants import NUM_UNIT def main() -> None: """Главная управляющая функция.""" print(f"Значение константы NUM_UNIT до изменения: {NUM_UNIT}") print(f"Значение ключа KB: {NUM_UNIT.get("KB", None)}") NUM_UNIT["qwe"] = 1234 if __name__ == "__main__": main()
В результате выполнения команды python3 src/temp_project/main.py мы увидим:
Значение константы NUM_UNIT до изменения: {'B': 1, 'KB': 1024, 'MB': 1048576, 'GB': 1073741824, 'TB': 1099511627776} Значение ключа KB: 1024 Traceback (most recent call last): File ".../src/temp_project/main.py", line 15, in <module> main() ~~~~^^ File ".../src/temp_project/main.py", line 11, in main NUM_UNIT["qwe"] = 1234 ~~~~~~~~^^^^^^^ TypeError: 'mappingproxy' object does not support item assignment
Теперь константа NUM_UNIT поддерживает методы get, items, keys, но не поддерживает присваивание значений (также отсутствует метод update, можете проверить сами).
Если есть модель констант, защитить атрибуты от переопределения поможет класс Enum.
"""Модуль констант приложения.""" # src/temp_project/core/constants.py from enum import Enum class SomeModel(Enum): CONSTANT_THREE = 3 CONSTANT_FOUR = 4
"""Главный управляющий модуль.""" # src/temp_project/main.py from core.constants import SomeModel def main() -> None: """Главная управляющая функция.""" print(SomeModel._member_map_.items()) SomeModel.CONSTANT_FOUR = 1234 if __name__ == "__main__": main()
В результате выполнения команды python3 src/temp_project/main.py мы увидим:
dict_items( [ ('CONSTANT_THREE', <SomeModel.CONSTANT_THREE: 3>), ('CONSTANT_FOUR', <SomeModel.CONSTANT_FOUR: 4>) ] ) Traceback (most recent call last): File ".../src/temp_project/main.py", line 14, in <module> main() ~~~~^^ File ".../src/temp_project/main.py", line 10, in main SomeModel.CONSTANT_FOUR = 1234 ^^^^^^^^^^^^^^^^^^^^^^^ File ".../uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/enum.py", line 840, in __setattr__ raise AttributeError('cannot reassign member %r' % (name, )) AttributeError: cannot reassign member 'CONSTANT_FOUR'
Настройки приложения
Для работы проекта обычно требуются конфиденциальные и специфичные данные. К конфиденциальным данным можно отнести логины и пароли для подключения к базе данных, вспомогательным сервисам и т.д. К специфичным данным можно отнести глобальный флаг режима отладки, URL к вспомогательным сервисам и т.д.
Чтобы предотвратить компрометацию и реализовать изменение настроек без изменения самого кода проекта, используется файл .env для хранения данных. Также использование .env файла предотвращает попадание данных в репозиторий проекта, если добавить файл .env в .gitignore.
И вот третья практика — обязательно добавление в репозиторий файла .env.example, в котором указаны все переменные, которые будут использоваться приложением. Переменные, выполняющие роль управления проектом (флаг режима отладки и т.д.), заполнены значениями по умолчанию. Переменные, выполняющие роль хранения чувствительных данных, остаются пустыми (обычно заполняются на этапе CI/CD).
Для парсинга .env файла и формирования объекта настроек приложения будем использовать pydantic-settings. Модель настроек приложения находится в src/temp_project/core/models/settings.py.
"""Модуль модели настроек приложения.""" # src/temp_project/core/models/settings.py import logging from pathlib import Path from pydantic import Field, PrivateAttr from pydantic_settings import BaseSettings, SettingsConfigDict class AppSettings(BaseSettings): """Модель настроек приложения.""" # MODEL SETTINGS # case_sensitive - чувствительность к регистру # extra - что делать с полями в .env файле, которые не определены в модели, # в данном случае игнорирование model_config = SettingsConfigDict(case_sensitive=False, extra="ignore") # LOGS debug_mode: bool = Field(default=False) log_dir_base: Path = Field( default_factory=lambda: Path(__file__).resolve().parents[4].joinpath("log") ) # PrivateAttr() говорит о том, что атрибут: # 1) не будет участвовать в валидации/сериализации # 2) не задается через конструктор # 3) хранится в объекте модели для внутренних нужд _log_lvl: int | None = PrivateAttr(default=None) _full_path_log: Path | None = PrivateAttr(default=None) @field_validator("log_dir_base") @classmethod def validate_log_dir_base(cls, value: Path) -> Path: if not value.is_absolute(): raise ValueError( "Путь до директории логов должен быть абсолютным!" ) return value def model_post_init(self, context: object) -> None: """Функция post-init обработки объекта.""" self._log_lvl = logging.DEBUG if self.debug_mode else logging.INFO self._full_path_log = self.log_dir_base.joinpath("root.log")
Важно подсветить переопределение метода model_post_init, который изначально наследуется классом pydantic_settings.BaseSettings от класса pydantic.BaseModel. Изначально метод «пустой», но мы его переопределяем с целью выполнить дополнительную инициализацию после методов __init__ и model_construct (по-простому — после создания объекта модели). В методе могут определяться/переопределяться атрибуты модели, в нашем случае мы переопределяем атрибут self._log_lvl, который отвечает за уровень логирования.
Многие после нас могут редактировать .env файл и писать туда всякое, чего быть не должно. Поэтому можно реализовать специальный метод validate_log_dir_base, который будет проверять параметр log_dir_base, переданный в .env до создания объекта модели.
Для атрибутов, в которых должны находиться чувствительные данные, есть специальные классы данных pydantic.SecretStr, pydantic.SecretBytes, pydantic.Secret.
Функция возврата объекта настроек выглядит следующим образом.
"""Модуль конфигураций.""" # src/temp_project/core/config.py from pathlib import Path from core.models.settings import AppSettings from functools import lru_cache @lru_cache() def build_app_settings() -> AppSettings: """Создание и кэширование объекта настроек.""" return AppSettings( _env_file=Path(__file__).resolve().parent.joinpath(".env") ) def get_app_settings(*, force_reload: bool = False) -> AppSettings: """Получение настроек.""" if force_reload: build_app_settings.cache_clear() return build_app_settings()
Здесь отметим, что в параметр _env_file передаем абсолютный путь до файла .env, который определяем относительно расположения файла src/temp_project/core/config.py.
Есть нюанс, который заключается в том, что файл .env может быть очень большим и если функция будет часто вызываться, то это скажется на производительности. Поэтому мы добавили кеширование с возможностью принудительного сброса кеша. Кеширование помогает нам снизить трудозатраты на парсинг .env файла, а принудительный сброс кеша полезен при изменении .env файла «на горячую». Достаточно будет вызвать функцию с параметром force_reload равным True, чтобы принудительно начать с нуля создание объекта настроек.
Логирование
И вот сразу четвертая практика — логирование должно быть всегда, абсолютно всегда. Когда упадет приложение (а оно упадет), только логи помогут определить причину.
Для логирования мы будем использовать встроенную библиотеку logging. Функция возврата объекта логера находится в src/temp_project/core/config.py. Для корректного функционирования понадобится код из Настройки приложения.
Пример функции возврата объекта логера.
"""Модуль конфигураций.""" # src/temp_project/core/config.py import logging import logging.config def get_logger() -> logging.Logger: """Функция возврата объекта логера.""" config_logger = { "version": 1, "formatters": { "default": { "format": ( '[%(asctime)s] %(levelname)s File "%(pathname)s", ' 'line %(lineno)d, in %(funcName)s: %(message)s' ), }, }, "handlers": { "console": { "class": "logging.StreamHandler", "stream": "ext://sys.stdout", "formatter": "default", }, "file": { "class": "logging.handlers.RotatingFileHandler", "filename": str(get_app_settings()._full_path_log), "formatter": "default", "encoding": "UTF-8", "maxBytes": 1 * 1024 * 1024, "backupCount": 3, }, }, "root": { "level": get_app_settings()._log_lvl, "handlers": ["file", "console"] if get_app_settings()._log_lvl == logging.DEBUG else ["file"], }, } get_app_settings().log_dir_base.mkdir(parents=True, exist_ok=True) logging.config.dictConfig(config_logger) logger_root = logging.getLogger() return logger_root
Настройка логера выполняется с помощью словаря конфигурации. В данном случае у нас есть 2 обработчика: file и console. У обработчика file реализована запись в файл в кодировке utf-8, ротация файлов логов — каждый файл должен быть не больше 1 МБ, максимальное количество файлов логов равно 3. Обработчик console настроен на вывод логов в поток вывода stdout (стандартный вывод в консоль). Формат строки записи у обработчиков одинаковый.
Особенность — если флаг режима отладки False, то обработчик console исключается из списка обработчиков, а обработчик file будет игнорировать записи уровня DEBUG.
Пример использования (запуск REPL из корневой директории репозитория).
>>> from config import get_logger >>> logger_root = get_logger() >>> logger_root.debug(1234) [2026-03-10 16:38:24,575] DEBUG File "<stdin-5>", line 1, in <module>: 1234 >>> logger_root.info(5678) [2026-03-10 16:38:41,611] INFO File "<stdin-7>", line 1, in <module>: 5678 >>>
Eсть сторонние библиотеки, предоставляющие функционал логирования, например loguru.
И вот пятая практика — всегда нужно оборачивать в try/except точку входа в приложение с целью логирования всех исключений.
"""Главный управляющий модуль.""" # src/temp_project/main.py from config import get_logger from core.constants import SomeModel logger_root = get_logger() def main() -> None: """Главная управляющая функция.""" try: print(SomeModel._member_map_.items()) SomeModel.CONSTANT_FOUR = 1234 except Exception as error: logger_root.error(error, exc_info=False) if __name__ == "__main__": main()
Проблема заключается в том, что в процессе выполнения кода, может возникнуть множество исключений, и нам крайне важно не упустить ни одно из них, поэтому в данном случае мы отслеживаем общий базовый класс для всех исключений, не связанных с завершением работы программы.
После того, как исключение перехвачено, можно поднять исключение выше (с использованием raise) или завершить работу с кодом выхода отличного от нуля (raise typer.Exit(code=1)), в общем, все зависит уже от проекта и используемых библиотек и фреймворков.
Также обращаю внимание на параметр exc_info=True, который определяет требуется ли нам вывод Traceback при логировании ошибки. Это очень удобно, так как необходимость фиксировать Traceback не всегда нужна.
В данном случае получим сообщение:
dict_items( [ ('CONSTANT_THREE', <SomeModel.CONSTANT_THREE: 3>), ('CONSTANT_FOUR', <SomeModel.CONSTANT_FOUR: 4>) ] ) [2026-03-11 11:28:42,804] ERROR File "../src/temp_project/main.py", line 14, in main: cannot reassign member 'CONSTANT_FOUR' Traceback (most recent call last): File "../src/temp_project/temp_project/main.py", line 12, in main SomeModel.CONSTANT_FOUR = 1234 ^^^^^^^^^^^^^^^^^^^^^^^ File "../uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/enum.py", line 840, in __setattr__ raise AttributeError('cannot reassign member %r' % (name, )) AttributeError: cannot reassign member 'CONSTANT_FOUR'
Статический анализ кода
Есть большое количество инструментов для поиска потенциальных проблем в коде — наличие неиспользованных переменных, лишние импорты, стиль кода, ранняя отладка ошибок и т.д.
И вот шестая практика — всегда необходимо пользоваться инструментами статического анализа, всегда.
ruff
ruff — это чрезвычайно быстрый инструмент для линтинга и форматирования Python-кода, написанного на Rust.
Позволяет находить:
ошибки синтаксиса и стиля,
возможности упрощения кода,
неиспользуемые импорты,
отсутствие документации или некорректный формат документации,
ошибки безопасности (использование assert в коде приложения и т.д.).
Также в функционале присутствуют:
форматирование кода,
сортировка импортов или удаление неиспользуемых импортов,
исправление тривиальных ошибок.
ruff ищет файл конфигурации в директории, из которой произошел запуск, в следующем порядке (по возрастанию приоритета): pyproject.toml (в секции [tool.ruff]), ruff.toml, .ruff.toml. Если ни один из этих файлов не найден, Ruff использует настройки по умолчанию.
Пример конфигурации ruff в pyproject.toml.
# ======================================== # НАСТРОЙКИ RUFF # ======================================== [tool.ruff] # Список файлов и папок, которые Ruff будет игнорировать exclude = [ "Dockerfile*", ".dockerignore", "*.xlsx", "*.xls", "*.doc", "*.docx", "*.drawio", "*.Dockerfile", "Dockerfile", "**/*.rst", "**/*.uml", "**/*.puml", "**/*.wsd", "*.css", "*.sh", "*.html", "*.js", "*.tar", "*.tar.gz", "*.sql", "*.json", "*.yaml", "*.yml", "*.ico", "*.wsgi", "*.md", "*.env", "*.log", "*.txt", "*.conf", "*.ini", "*.toml", "*.example", ".python-version", ".env", ".init_env", ".gitkeep", ".gitignore", ".bzr", ".qmi_env", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".ipynb_checkpoints", ".mypy_cache", ".nox", ".pants.d", ".pyenv", ".pytest_cache", ".pytype", ".ruff_cache", ".svn", ".tox", ".vscode", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "site-packages", "venv", ".venv", "uv.lock", ] # =========================== # ОСНОВНЫЕ НАСТРОЙКИ RUFF # =========================== line-length = 120 # Максимальная длина строки (рекомендуется 80-120) indent-width = 4 # Размер отступа (4 пробела стандарт для Python) target-version = "py313" # Минимальная версия Python (3.13) # =========================== # НАСТРОЙКИ ЛИНТЕРА # =========================== [tool.ruff.lint] select = [ "E", "F", "D", "I", "ANN", "T20", "S", "SIM", "LOG", "G", ] ignore = ["D203", "D213", "D205"] # Игнорируемые конкретные правила fixable = ["ALL"] # Разрешить автоисправление для всех правил unfixable = [] # Правила без автоисправления [tool.ruff.lint.isort] combine-as-imports = true # Объединять импорты из одного модуля # =========================== # ПРАВИЛА ДЛЯ КОНКРЕТНЫХ ФАЙЛОВ # =========================== [tool.ruff.lint.per-file-ignores] "__init__.py" = ["D104"] # Игнорировать отсутствие docstring в __init__ # =========================== # НАСТРОЙКИ ФОРМАТТЕРА # =========================== [tool.ruff.format] quote-style = "double" # Стиль кавычек (как в Black) indent-style = "space" # Тип отступов (пробелы вместо табов) skip-magic-trailing-comma = false # Учитывать завершающие запятые line-ending = "auto" # Автоматически определять переносы строк docstring-code-format = true # Форматировать примеры кода в docstring docstring-code-line-length = 120 # Максимальная длина строки в docstring
Результат выполнения команды ruff check src/temp_project/main.py.
D202 [*] No blank lines allowed after function docstring (found 1) --> src/temp_project/main.py:9:5 | 8 | def main() -> None: 9 | """Главная управляющая функция.""" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10 | 11 | try: | help: Remove blank line(s) after function docstring T201 `print` found --> src/temp_project/main.py:12:9 | 11 | try: 12 | print(SomeModel._member_map_.items()) | ^^^^^ 13 | SomeModel.CONSTANT_FOUR = 1234 14 | except Exception as error: | help: Remove `print` Found 2 errors. [*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
Также очень полезна команда форматирования кода ruff format src/temp_project/core/config.py --diff
--- src/temp_project/core/config.py +++ src/temp_project/core/config.py @@ -20,6 +20,7 @@ build_app_settings.cache_clear() return build_app_settings() + def get_logger() -> logging.Logger: """Функция возвращает экземпляр логера приложения. @@ -32,8 +33,7 @@ "formatters": { "default": { "format": ( - '[%(asctime)s] %(levelname)s File "%(pathname)s", ' - 'line %(lineno)d, in %(funcName)s: %(message)s' + '[%(asctime)s] %(levelname)s File "%(pathname)s", 'line %(lineno)d, in %(funcName)s: %(message)s' ), }, }, @@ -63,4 +63,4 @@ logging.config.dictConfig(config_logger) logger_root = logging.getLogger() - return logger_root \ No newline at end of file + return logger_root 1 file would be reformatted
mypy
mypy — это инструмент для статической проверки соответствия типов.
Пример конфигурации mypy в pyproject.toml.
# ======================================== # НАСТРОЙКИ MYPY # ======================================== [tool.mypy] python_version = "3.13" strict = false exclude = [ ".*\\.rst$", ".*\\.wsd$", ".*\\.puml$", ".*\\.uml$", "README.md", "mypy.ini", ".pre-commit-config.yml", ".env.example", ".env", ".gitignore", "Dockerfile", "docker-compose.yml", "ruff.toml", ".dockerignore", ]
Результат выполнения команды mypy src/temp_project/core/config.py.
src/temp_project/core/config.py:14: error: Unexpected keyword argument "_env_file" for "AppSettings" [call-arg] Found 1 error in 1 file (checked 1 source file)
Итог
В итоге перечислим все практики, о которых необходимо помнить:
Не использовать
globalпока нет 100% уверенности, что без этого никак;Для констант всегда использовать неизменяемые типы данных;
Обязательно добавление в репозиторий файла
.env.example;Логирование должно быть всегда, абсолютно всегда;
Всегда нужно оборачивать в try/except точку входа в приложение с целью логирования всех исключений;
Всегда необходимо пользоваться инструментами статического анализа, всегда.
