Лучшие практики написания кода на Python (2026)

1. Общие принципы чистого кода на Python

При разработке на Python следует руководствоваться философией The Zen of Python (PEP 20) – набором принципов, подчеркивающих важность простоты и ясности кода. Например, “Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex.” («Красивое лучше, чем некрасивое. Явное лучше неявного. Простое лучше сложного.»). Один из ключевых афоризмов Zen of Python гласит: “Readability counts.” («Читаемость кода имеет значение»), и действительно, код читается гораздо чаще, чем пишется. Поэтому чистый код – это код, который легко читать и поддерживать.

Основные принципы чистого кода на Python включают:

  • Простота и ясность: Стремитесь к простым решениям и избегайте излишней сложности. Код должен быть понятен другим разработчикам без длительных разъяснений. Если реализацию сложно объяснить – это плохой признак, а если легко – скорее всего, идея хороша.
  • Единичность ответственности: Каждая функция или класс должны решать одну конкретную задачу. Разбиение сложной задачи на меньшие части делает код чище и упрощает тестирование.
  • Не повторяйся (DRY): Избегайте дублирования кода. Выделяйте повторяющуюся логику в функции или утилитарные модули. Это снижает количество мест, которые нужно менять при правках, и уменьшает риск ошибок.
  • Максимально используйте возможности Python: Пишите «питонообразный» (pythonic) код, пользуясь идиоматичными конструкциями. Например, вместо громоздких циклов и условных операторов используйте list/dict comprehensions, встроенные функции (sum, max, enumerate и др.), конструкцию with для работы с ресурсами, генераторы для отложенных вычислений и т.д. – всё это делает код чище и зачастую эффективнее.
  • Следуйте руководящим принципам языка: Руководство по стилю Python (PEP 8) и философия Zen of Python являются ориентирами для принятия решений в неоднозначных случаях. Когда есть несколько способов реализовать функциональность, обычно предпочтителен тот, что обеспечивает наибольшую ясность и простоту (в соответствии с девизом «Должен существовать один – и желательно только один – очевидный способ сделать это»)

Pythonl – с помощью понятных картинок и коротких видео авторы объясняют сложные концепции и учат профессиональному подходу в разработке.


🔝 А здесь мы собрали целый кладезь полезных Python ресурсов для прокачки.

2. Современные рекомендации по стилю (PEP 8 и актуальные изменения)

Стиль кода в Python преимущественно регламентируется документом PEP 8 – Style Guide for Python Code. PEP 8 устанавливает единые соглашения, благодаря которым код разных разработчиков выглядит единообразно и легко читается. Ниже перечислены основные современные рекомендации по стилю (с учётом PEP 8 и нововведений последних лет):

  • Отступы: Используйте отступ в 4 пробела на каждый уровень вложенности кода. Табулирование не рекомендуется – PEP 8 прямо указывает, что предпочтительны пробелы, а табы допускаются только для совместимости со старым кодом. Никогда не смешивайте табы и пробелы в одном проекте.
  • Длина строк: Старайтесь ограничивать длину строк приблизительно 79 символами. Это облегчает чтение кода в текстовых редакторах и инструментах обзора кода. Многие современные проекты допускают чуть более длинные строки (например, 88 или 100 символов), однако превышение 79 символов должно быть обоснованным. Если выражение слишком длинное, разбейте его на несколько строк с помощью обратного слэша или скобок. PEP 8 также дает рекомендации по разрыву строк – например, производить разрыв перед бинарным оператором или использовать висячие отступы для улучшения читаемости.
  • Пробелы в выражениях: Соблюдайте аккуратность с пробелами. Перед запятой и перед точкой с запятой пробел не ставится, а после – ставится. Вокруг операторов присваивания и других бинарных операторов (например, =, +, -, == и т.д.) принято ставить пробелы с обеих сторон для отделения их от операндов. Исключение – ситуации, когда сниженная плотность кода повышает его понятность (например, параметры функции с default-значениями, о чём ниже). Не вставляйте лишних пробелов там, где они не нужны: внутри скобок, перед запятой или двоеточием, перед открывающей скобкой вызываемой функции и т.п. – эти “лишние” пробелы не улучшают, а только сбивают чтение кода.
  • Размещение скобок и запятых: Для длинных литералов контейнеров (списков, словарей) рекомендуется ставить запятую после каждого элемента, включая последний – тогда при добавлении нового элемента в конец дифф будет минимальным. Закрывающую скобку структур данных или вызова функции обычно размещают на новой строке, выровненной под начало выражения.
  • Комментирование: PEP 8 рекомендует писать комментарии на том же языке, на котором код будут читать разработчики (чаще всего – на английском). Комментарии не должны противоречить коду и должны оперативно обновляться при изменении кода. Отдавайте предпочтение полноценным docstring (о них ниже) для описания модулей, функций и классов, а не написанию объемных комментариев. Однострочные комментарии внутри функции должны быть короткими и по делу, без излишних очевидных пояснений (например, не нужно писать # increment x рядом с x = x + 1).
  • Автоматическое форматирование кода: В 2020-х годах широкое распространение получили автоформатеры кода. Например, инструмент Black позиционируется как “непримиримый форматер” и автоматически приводит код к единому стилю, соответствующему PEP 8. Использование автоформатеров стало лучшей практикой: при настройке Black (или аналогичных инструментов, таких как yapf или autopep8) на вашем проекте вы делегируете выравнивание отступов, разбиение длинных строк и расстановку пробелов инструменту. Это избавляет команду от споров о стиле и позволяет сосредоточиться на логике кода. В 2026 году такой подход является нормой для большинства профессиональных проектов – достаточно настроить форматирование как шаг в CI/CD или интегрировать в IDE, и весь код будет автоматически соответствовать принятому стилю.

Важно понимать, что PEP 8 – это не свод жёстких правил, а руководство. В самом начале PEP 8 отмечено: “Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is the most important.” («Соблюдение этого стиля важно. Ещё важнее – консистентность стиля внутри проекта. А самая важная – внутри одного модуля или функции.»). То есть, наиболее важно сохранять единообразие стиля внутри вашего кода. Иногда отклонение от рекомендаций допустимо, если оно улучшает читаемость или необходимо для поддержки существующего кода – “When in doubt, use your best judgment” (в сомнительной ситуации полагайтесь на здравый смысл).

3. Именование переменных, функций и классов

Грамотный выбор имён – залог понятного кода. PEP 8 устанавливает чёткие соглашения об именованиях в Python:

  • Имена переменных и функций пишутся в нижнем регистре и используются слова, разделённые символом подчёркивания (snake_case) для улучшения читабельности: например, total_sum, process_data(). Функции обычно называют глаголами или глагольными фразами, описывающими действие (например, calculate_total, send_email). Переменные – существительными или короткими фразами, отражающими содержимое.
  • Имена классов должны следовать стилю CapWords (также называемому CamelCase – каждое слово с заглавной буквы, без разделителей). Например, DataParser, UserAccount. Для исключений принято добавлять окончание "Error" к имени класса (например, ValueError), если класс действительно представляет ошибку.
  • Имена констант (глобальные переменные, значения настроек) записываются заглавными буквами с подчёркиваниями между словами. Например, MAX_CONNECTIONS, DEFAULT_TIMEOUT. Константами считаются значения, которые не должны изменяться в ходе работы программы.
  • Приватные переменные и методы: в Python нет строгого модификатора доступа, но по соглашению имена, начинающиеся с одного подчёркивания _, считаются «не публичными», внутренними для модуля или класса. Интерпретатор не запрещает доступ к таким атрибутам извне, но разработчики понимают, что они не предназначены для внешнего использования. Wildcard-импорт (from module import *) по умолчанию не импортирует имена, начинающиеся с подчёркивания. Если нужно явно указать публичный API модуля, определяют список __all__ внутри модуля.
  • Двойное подчёркивание в начале имени (__var) запускает механизм name mangling – к имени автоматически добавляется имя класса, что служит для предотвращения случайных конфликтов имен при наследовании. Однако не рекомендуется изобретать “магические” имена с обоих сторон двойным подчёркиванием (__like_this__) – такие имена зарезервированы для специальных методов и атрибутов Python (например, __init__, __len__ и т.д.) и внедрять свои не следует.
  • Аргументы методов и функций: Первый аргумент метода экземпляра класса обязательно называется self (что обозначает сам объект). Для методов класса (объявленных с декоратором @classmethod) первым аргументом должен быть cls. Несоблюдение этого соглашения будет как минимум озадачивать других разработчиков, а отсутствие self в методе приведёт к ошибке. Также, если имя аргумента совпадает с ключевым словом Python, добавляется подчёркивание на конце (class_, lambda_), вместо искажения слова.

Выбирайте имена, отражающие суть. Код, в котором переменные называются a, b, foo, data – затрудняет понимание. Лучше потратить время и подобрать выразительное имя, чем снабжать каждую строчку комментариями с объяснениями. Например, вместо:

# Плохой пример
d = {}  # словарь ключей и значений параметров
for k, v in items:
    if len(v) > 5:
        d[k] = v

можно написать:

# Хороший пример
params = {}
for key, value in items:
    if len(value) > 5:
        params[key] = value

Здесь вместо непонятного d используется params (коротко от parameters, понятно, что это словарь параметров). Имена key и value вместо k и v делают цикл самодокументируемым – без комментария ясно, что происходит.

Следование соглашениям об именах облегчает чтение кода без дополнительных комментариев. Когда встречаешь класс OrderProcessor или функцию send_email(), ее предназначение понятно сразу. Стиль именования в Python стал стандартом де-факто: он поддерживается большинством линтеров и проверяется автоматически. Например, PEP 8 указывает, что нельзя использовать имена переменных l, O, I (маленькая эль, большая о, большая и) – их легко спутать с цифрами 1 и 0. Такие нюансы тоже следует помнить.

4. Организация структуры проекта

Правильная структура проекта облегчает навигацию по коду и его поддержку. По мере роста проекта из одного файла в набор модулей и пакетов нужно организовывать код так, чтобы было чётко понятно, где что находится. Современные best practices предполагают следующие подходы к структуре Python-проекта:

  • Выделение пакета с исходным кодом: Код приложения или библиотеки обычно выделяют в отдельный пакет (директорию) внутри корневой папки проекта. Распространены два подхода:
    • Flat layout – пакет размещается на верхнем уровне. Например, если проект называется myproject, структура будет: myproject/ myproject/ <- пакет с кодом (модули и подмодули) tests/ <- директория с тестами README.md pyproject.toml ... При таком подходе название корневой директории и внутреннего пакета совпадают.
    • Src layout – исходники лежат в папке src/. Например: myproject/ src/ myproject/ <- пакет с кодом __init__.py module1.py ... tests/ pyproject.toml README.md Такой подход в последние годы набирает популярность, так как предотвращает случайный импорт “локальной” версии пакета вместо установленной (когда запускаются тесты или скрипты). Многие шаблоны проектов (включая официальные рекомендации PyPA) используют структуру с папкой src.
  • Пакетирование и модули: Разбивайте код на модули по функциональному признаку. Не складывайте весь код в один файл – лучше создать пакет с несколькими модулями, каждый из которых отвечает за свою часть логики (например, api.py, models.py, utils.py и т.п.), либо вложенные подпакеты для логически обособленных частей. Избегайте ситуаций, когда в одном модуле оказываются десятки несвязанных функций – это затрудняет поддержку. Вместо этого старайтесь проектировать структуру из небольших модулей, группируя схожую функциональность вместе. Это повышает переиспользуемость кода и делает зависимости явными.
  • Отделение кода и тестов: Храните автоматические тесты отдельно от основного кода, обычно в директории tests/ на верхнем уровне проекта. Внутри tests/ можете организовать подпапки, отражающие структуру пакета (например, tests/unit/test_module1.py, tests/integration/test_featureX.py). Такой подход не только упрощает запуск тестов, но и исключает попадание тестовых файлов в финальную сборку приложения.
  • Файлы конфигурации и метаданные: В корне проекта обычно располагаются файлы: README.md (описание проекта, примеры использования), LICENSE (лицензия), pyproject.toml (метаданные проекта: зависимости, конфигурация инструментов сборки и линтеров), а также возможны requirements.txt (список зависимостей для pip, если не используется pyproject), настройки CI (.github/workflows или .gitlab-ci.yml) и т.д. Pyproject.toml в 2026 году стал стандартным единым файлом конфигурации для описания пакета и инструментов разработки (согласно PEP 518/517).
  • Логическое разделение слоёв приложения: В крупных приложениях разделяйте «слои» – например, слой бизнес-логики, слой доступа к данным, слой веб-интерфейса или CLI – по разным подпакетам. Это делает архитектуру более явной. Пример: myproject/core/ (основная логика), myproject/db/ (взаимодействие с БД), myproject/api/ (REST API, если есть), myproject/gui/ (если присутствует графический интерфейс), и т.д. Такое разделение по каталожной структуре помогает избежать переплетения кода разных уровней и упрощает навигацию.
  • Имена модулей и пакетов: PEP 8 рекомендует называть файлы модулей короткими именами в нижнем регистре. В имени модуля могут использоваться подчёркивания, если так улучшается читаемость (например, socket_server.py), но в целом предпочтительны короткие имена без них. Имена пакетов – тоже в нижнем регистре, по возможности без подчёркиваний. Избегайте в именах модулей и пакетов дефисов, пробелов и других символов, кроме букво-цифр и подчёркивания.

Хорошо организованный проект сразу виден при взгляде на структуру каталогов. Например:

myproject/
├── pyproject.toml
├── README.md
├── src/
│   └── myproject/
│       ├── __init__.py
│       ├── __main__.py      # Точка входа (например, if __name__ == "__main__")
│       ├── data_models.py
│       ├── processing.py
│       └── utils.py
└── tests/
    ├── __init__.py
    ├── test_processing.py
    └── test_utils.py

В таком проекте сразу видно, где основной код (src/myproject/), где тесты, где настройки. Плохой пример – когда в корне перемешаны файлы с кодом, данными, тестами и документами, без ясного разделения. Структурированность облегчает понимание: где точка входа, где основные компоненты, как связаны тесты и код.

Кроме того, правильная структура помогает избежать ошибок с импортами. Например, при наличии отдельной папки src/ разработчики всегда импортируют пакет как import myproject (указывая PYTHONPATH на src), вместо неявного импорта из текущей директории, что предотвращает конфликты имен файлов с именами стандартных модулей.

Итог: инвестируйте время в организацию проекта с самого начала. Как отмечает RealPython, «предсказуемый layout облегчает понимание, как части проекта сочетаются друг с другом, и позволяет быстро принимать решения, куда добавлять новый код или ресурсы». Хорошая структура – это фундамент, на котором строится остальной код.

5. Документирование кода и использование docstrings

Документация – неотъемлемая часть качественного кода. В Python основной способ документирования – это docstring (строка документации), которая пишется в виде строкового литерала сразу под определением модуля, класса или функции. PEP 257 определяет соглашения по оформлению docstring. Краткие рекомендации:

  • Пишите docstring для всех публичных модулей, функций, классов и методов. Исключение можно сделать для негласных (внутренних) утилит, но даже для них полезно хотя бы кратко указать назначение комментариями.
  • Формат docstring: используйте тройные кавычки """ ... """ для оформления, даже если строка умещается в одну линию. Для многострочных docstring первая строка должна быть кратким обобщающим предложением, затем пустая строка, после – более развернутое описание при необходимости. Первая строка обычно формулируется как повелительное наклонение, описывающее что делает функция (например, "Calculate sum of all elements."), а не что она из себя представляет. В конце многострочной docstring закрывающие кавычки ставятся на отдельной строке.
  • Однострочные docstring: если документируемая вещь настолько проста, что укладывается в одно предложение, можно написать docstring в одну строку (начав и закончив тройными кавычками в одной строке). В таком случае не ставится дополнительный перенос строки ни перед, ни после строки.
  • Содержание docstring: Опишите назначение функции или класса, а не пересказывайте имя. Хорошая docstring отвечает на вопрос «что делает этот код и как его использовать?». Для функций стоит указывать, что возвращается (если не очевидно). Не нужно дублировать сигнатуру функции в docstring (типы и имена параметров можно узнать через аннотации и introspection). Лучше раскрыть нюансы: единицы измерения, допустимые диапазоны значений, побочные эффекты, исключения, которые могут быть брошены, и т.д. Например, вместо плохой docstring """function(a, b) -> list""" напишите """Combine lists a and b and return a new list.""" – то есть осмысленное описание действия.
  • Документирование классов: В docstring класса обычно указывают предназначение класса и описывают важные методы или использование. PEP 257 рекомендует после docstring класса оставлять пустую строку перед первой методой, чтобы визуально отделить докстринг от тела класса.
  • Форматы docstring: Строгого стандарта форматирования содержимого нет, но сообщество выработало несколько стилевых шаблонов:
    • reStructuredText (reST) – используется в Sphinx и многих проектах. Позволяет разметить разделы :param name: ..., :return: ... и т.д. для генерации красивой документации.
    • Google style docstrings – более лаконичный стиль, где описания параметров оформляются как секции с отступами.
    • NumPy style – похож на Google, но с секциями Parameters, Returns, Examples и т.д.
    Выбор стиля – дело вкуса проекта, важно лишь придерживаться единообразия.
  • Доступ к docstring: Помните, что строки документации доступны во время выполнения через атрибут __doc__ объекта или функцию help(). Это удобно: например, набрав в интерактивном режиме help(my_function), вы увидите ее docstring. Поэтому пишите docstring так, чтобы она была понятна и полезна даже без просмотра кода функции.
  • Комментарии vs docstring: Обычные комментарии (начинающиеся с #) не попадают в help() и служат, скорее, для пометок внутри реализации. Используйте их для пояснения сложных участков кода или временных решений. Но для внешнего описания интерфейсов (API) всегда предпочтительны docstring, т.к. они интегрируются с инструментами документации и IDE.

Пример оформления docstring для функции:

def scale_values(data: list[float], factor: float) -> list[float]:
    """Вернуть новый список, в котором каждое число из data умножено на factor.

    Аргументы:
        data (list of float): входной список чисел.
        factor (float): множитель для масштабирования.

    Returns:
        list of float: новый список, содержащий результаты умножения.
    """
    return [x * factor for x in data]

В этом примере docstring многострочный: первое предложение – сжатое описание, далее после пустой строки разъясняются параметры и результат. Обратите внимание, что благодаря аннотациям типов (о них далее) мы указали типы параметров и возвращаемого значения в сигнатуре, поэтому в скобках после названий параметров и Returns это дублируется только для ясности. Многие инструменты (например, IDE или Sphinx) могут автоматически извлечь эту информацию.

Документируя код, вы облегчаете жизнь и коллегам, и самому себе в будущем. Хорошо написанная документация делает использование вашего кода интуитивным и снижает количество вопросов «что делает эта функция?». Согласно PEP 20, “Explicit is better than implicit” – лучше явно объяснить поведение, чем полагаться на догадки.

6. Написание тестов и современные фреймворки (например, Pytest)

Тестирование – ключевой аспект качественной разработки. Написание автоматических тестов обеспечивает уверенность в том, что код работает правильно, и позволяет вносить изменения (рефакторить) без страха сломать существующий функционал. К 2026 году сообщество Python выработало следующие лучшие практики в тестировании:

  • Выбор фреймворка: В стандартной библиотеке Python есть модуль unittest (он же PyUnit), но наиболее популярным и удобным фреймворком стал Pytest. Pytest ценится за лаконичность и простоту: вместо создания классов тестов и использования специальных методов, как в unittest, вы пишете обычные функции, а проверки делаете простыми assert выражениями. По состоянию на конец 2024 года вышла версия Pytest 9.0, и он фактически стал стандартом де-факто для тестирования Python-проектов. Его популярность обусловлена мощной экосистемой плагинов (для тестирования БД, asyncio, mock-объектов и пр.) и более читаемыми сообщениями об ошибках.
  • Организация тестов: Как упоминалось в разделе о структуре проекта, держите тесты в отдельной папке (обычно tests). Pytest по умолчанию ищет файлы, начинающиеся с test_, и функции внутри них, названные test_*. Распространённый шаблон – отражать структуру основного пакета: например, для модуля myproject/utils.py сделать файл tests/test_utils.py с тестами его функций. Тесты должны быть независимы от окружения: не полагаться на внешние файлы или глобальное состояние (или, если требуется, явно настраивать окружение в коде теста).
  • Покрытие тестами ключевой логики: Стремитесь писать тесты для каждой функциональности, особенно для краевых случаев. Хорошая практика – применять методику TDD (Test-Driven Development), когда вы сначала пишете тесты на ожидаемое поведение, а затем реализацию. Это помогает четче сформулировать требования к коду.
  • Принципы написания тестов: Используйте понятные имена тестов, описывающие поведение. Существует шаблон AAA (Arrange-Act-Assert) – подготовка данных, выполнение действия, проверка результата – придерживайтесь его структуры в каждом тесте для ясности. Например: def test_division_with_positive_numbers(): # Arrange calc = Calculator() dividend = 10 divisor = 2 # Act result = calc.divide(dividend, divisor) # Assert assert result == 5.0 Такой подход облегчает чтение и понимание того, что проверяет данный тест.
  • Изоляция тестов: Каждый тест должен быть независимым. Ни один тест не должен зависеть от выполнения другого. Иначе появятся flaky tests – «мигающие» тесты, которые проходят или падают в зависимости от порядка запуска. Чтобы обеспечить изоляцию, используйте fixtures (фикстуры) для подготовки и очистки окружения для теста. В Pytest фикстуры объявляются через декоратор @pytest.fixture и могут возвращать подготовленные объекты, а после выполнения теста выполнять teardown (уборку) при помощи конструкции yield. Фикстуры делают код тестов чище и убирают дублирование при настройке повторяющихся состояний (например, подключение к БД для группы тестов).
  • Моки и стабы: В тестах, особенно модульных, может потребоваться изолировать код от внешних зависимостей – сетевых запросов, базы данных, файловой системы. Для этого применяют mock-объекты (в Python есть библиотека unittest.mock). Pytest облегчает работу с моками через плагины (например, pytest-mock) и позволяет быстро заменять части системы заглушками.
  • Покрытие и качество тестов: Используйте инструменты измерения покрытия кода тестами (coverage). Показатель покрытия (%) не самоцель, но он помогает найти неохваченные тестами участки. Старайтесь покрыть как минимум критическую бизнес-логику. Также обращайте внимание на качество проверок: тест должен не просто вызывать функцию, но и проверять, что она вернула правильный результат или вызвала ожидаемые побочные эффекты.
  • Интеграция в процесс разработки: Настройте запуск тестов при каждом коммите (в идеале – через CI). На локальной машине разработчики тоже должны регулярно прогонять тесты. Инструменты наподобие pytest --maxfail=1 -v ускоряют выявление ошибок, останавливаясь на первом падении. Следите, чтобы в репозитории не оставалось непройденных тестов – красные сборки CI недопустимы.

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

Подводя итог: покрытие кода тестами – лучший друг чистого кода. В 2026 году зрелые проекты имеют обширные тестовые наборы, и разработчики рассматривают написание тестов как неотъемлемую часть работы, а не как опциональное упражнение. Pytest и сопутствующие инструменты делают этот процесс достаточно удобным, чтобы автоматические тесты стали стандартом индустрии.

7. Использование аннотаций типов (Type Hints)

Аннотации типов – пожалуй, самое заметное изменение в стиле Python-кода за последнее десятилетие. Если раньше Python был строго динамически типизированным языком, то начиная с версии 3.5 (2015 год) в него постепенно были введены средства статической типизации в виде подсказок типов (PEP 484). К 2026 году применение type hints стало повсеместным и крайне рекомендуемым:

  • Зачем нужны type hints: Аннотации типов повышают читаемость и надежность кода. Указав типы параметров и возвращаемых значений функций, вы тем самым документируете контракт функции – что она ожидает на вход и что выдаёт. Это облегчает понимание кода (особенно новым участникам проекта) и служит дополнительной документацией. Кроме того, статические анализаторы и IDE способны ловить целый класс ошибок ещё до запуска программы: несоответствие типов, отсутствие необходимых атрибутов, неверное использование API и т.п. В большом кодовой базе наличие корректных type hints значительно упрощает рефакторинг – при изменении типа данных вы сразу видите, где код не соответствует новым ожиданиям.
  • Широкое принятие в сообществе: Если в конце 2010-х многие относились к аннотациям скептически, то сейчас ситуация иная. Согласно опросу 2025 года, 86% Python-разработчиков «всегда или часто» используют type hints в своём коде. Type hints превратились в неотъемлемую часть разработки для большинства инженеров. Это поддерживается и эволюцией языка: практически каждая новая версия Python добавляет улучшения в систему типов (например, объединение типов через оператор | в Python 3.10, введение TypeAlias и Self в 3.11, улучшение работы с Generics к 3.12 и т.д.).
  • Как использовать аннотации: Синтаксис аннотаций простой – двоеточие после имени параметра и указание типа, а для возвращаемого значения – -> Type перед двоеточием объявления функции. Например: def greet(name: str) -> str: return "Hello, " + name Здесь указано, что функция принимает строку и возвращает строку. Аннотации могут описывать сложные типы: list[int], dict[str, tuple[int, int]] и т.д. (Начиная с Python 3.9 можно использовать родные типы, а не typing.List). Также можно аннотировать переменные: например, count: int = 0 указывает тип переменной count.
  • PEP 8 о стилях аннотаций: PEP 8 адаптирован под аннотации типов. Основные правила оформления: не ставьте пробел перед двоеточием при аннотации, но ставьте пробел после двоеточия. В объявлении функции вокруг -> должны стоять пробелы с обеих сторон. Например, правильно: def func(x: int) -> None: ... (пробелы вокруг ->), неправильно: def func(x:int)->None:. Если у параметра есть значение по умолчанию и аннотация, пробелы ставятся вокруг = (например, x: List[int] = None), а вот у неаннотированного параметра пробелы вокруг = не ставятся (y=10 внутри сигнатуры).
  • Проверка типов и инструменты: Аннотации сами по себе никак не влияют на выполнение кода (интерпретатор их игнорирует), но их задействуют статические анализаторы – такие как mypy, Pyright, Pyre и др. Эти инструменты анализируют ваш код, не выполняя его, и выдают предупреждения, если типы не сходятся. Например, mypy сообщит об ошибке, если вы пытаетесь передать строку в функцию, ожидающую int. Интеграция mypy или Pyright в процесс CI помогает удерживать код в корректном состоянии. Кроме того, IDE (PyCharm, VS Code, etc.) используя аннотации, дают более умные подсказки и автодополнение кода.
  • Runtime-аспекты: Несмотря на статическую природу, есть библиотеки, которые могут использовать аннотации во время выполнения для валидации. Например, Pydantic или FastAPI читают аннотации, чтобы автоматически проверять типы входных данных (например, JSON в запросах) и сериализовать ответы. Библиотека beartype может декорировать функции и выполнять проверку типов при каждом вызове в рантайме. Однако повсеместно в код вставлять runtime-чекеры не требуется – достаточно договориться об использовании статических проверок на этапе разработки.
  • Советы по введению аннотаций: Если вы работаете с легаси-кодом без аннотаций, можно внедрять их постепенно. Начните с наиболее важных модулей или новых функций. Mypy позволяет постепенно настраивать строгость (например, опция strict optional или ignore missing imports). Можно также использовать stub-файлы .pyi для сторонних библиотек без аннотаций, либо устанавливать пакеты с типами из репозитория typeshed (они имеют названия вида types-имя_библиотеки).
  • Новые возможности типов к 2026 году: Python-типизация развивается: появились TypeAlias (именованные алиасы типов), TypedDict (для описания словарей с фиксированными полями), Protocol (структурная типизация для классов), литеральные типы, поддержка обобщённых типов контейнеров (Generic Types) без наследования от typing.Generic и многое другое. Эти возможности позволяют описать типы более точно. Например, с Python 3.10 можно писать int | None вместо Optional[int], что улучшает читаемость аннотаций.
  • Документирование vs аннотации: Когда в коде присутствуют аннотации типов, зачастую нет необходимости дублировать информацию о типах в docstring. Это позволяет сосредоточить документацию на описании логики и намерения функции, а механическая информация о типах будет доступна из сигнатуры. Разработчики отмечают, что type hints делают код самодокументируемым в большой степени.

В итоге, использование аннотаций типов стало лучшей практикой. В новом коде 2026 года считается хорошим тоном аннотировать все публичные функции и методы, а также сложные структуры данных. Исключения могут составлять очень простые вспомогательные функции (типа get_key при очевидных типах), но и их часто аннотируют – “на всякий случай”. Как говорится, “чем явно указанный тип хуже, чем неявный?”. Опыт показывает, что правильно расставленные type hints предотвращают множество багов и облегчают поддержку. Недаром Python-сообщество теперь массово их применяет, считая, что “Python’s type hinting system has become a core part of development” – неотъемлемая часть разработки.

8. Советы по производительности и читаемости

Грамотный код должен быть не только чистым, но и эффективным. Однако в Python (как и в любом высокоуровневом языке) всегда приходится искать баланс между производительностью и читаемостью. Несколько рекомендаций, актуальных на 2026 год:

  • Не жертвуйте читаемостью без крайней необходимости: Преждевременная оптимизация – зло. Этот известный принцип остается верным и в Python. Сначала напишите корректный и понятный код, и только если профилирование покажет узкие места по скорости, думайте об оптимизации. Практичность важнее чистой производительности – как гласит Zen of Python, “Although practicality beats purity.” (Практичность превыше чистоты). Читаемый код проще отлаживать и сопровождать. Оптимизировать же стоит только действительно критичные места, причём точечно.
  • Используйте встроенные возможности языка и стандартной библиотеки: Очень часто Python предоставляет готовое, оптимизированное решение, которое работает быстрее самописного. Например:
    • Используйте генераторы и списковые включения вместо вручную накопления списков в цикле – они не только короче, но и работают быстрее за счёт оптимизаций в Си.
    • Применяйте функции sum(), min(), max(), sorted() и т.п. вместо написания собственных циклов подсчёта – эти функции реализованы на C и обычно более эффективны.
    • Для проверки вхождения элемента в коллекцию используйте наборы (set) или словари (операция in для них – O(1) амортизированно) вместо списков (O(n)). Если нужно часто проверять принадлежность, преобразуйте список в set один раз.
    • Сравнивайте строки на префикс/суффикс методами str.startswith()/str.endswith(), а не через срезы – это и читается лучше, и менее ошибкоопасно.
    • Проверяйте тип объекта через isinstance(obj, SomeClass) вместо сравнения типа напрямую (type(obj) is SomeClass) – первый способ учитывает наследование и является более гибким.
  • Следите за алгоритмической сложностью: Python хоть и не компилируется в машинный код, но плохая алгоритмика ощутимо сказывается. Например, вложенные циклы на больших входных данных могут быть узким местом. Используйте профилировщики (как встроенный cProfile или инструменты вроде pyinstrument) чтобы найти, где программа тратит время, и оптимизируйте именно эти фрагменты.
  • Работа со строками: Конкатенация строк в цикле через + – известный анти-паттерн, т.к. в CPython она работает эффективно только благодаря некоторым внутренним оптимизациям, которые не гарантированы на других реализациях Python. Вместо накопления строк делайте ''.join(list_of_strings) – этот приём обеспечивает линейную сложность и зачастую быстрее, особенно на альтернативных реализациях Python.
  • Память и большие данные: Если вы работаете с крупными объёмами данных, обращайте внимание на память. Например, при чтении большого файла не загружайте всё содержимое в список строк, а итерируйтесь по файлу построчно. Используйте генераторы для ленивой обработки (вычисление «на лету» без хранения всего результата в памяти).
  • Параллелизм и асинхронность для производительности: В 2026 году уже стабилизировалась экосистема асинхронного Python (asyncio). Если ваша задача связана с большим числом IO-операций (запросы в сеть, дисковые операции), рассмотрите асинхронное выполнениеasync/await позволяют эффективно использовать время ожидания. Для CPU-связанных задач помните про ограничения GIL: может быть полезно распределить вычисления по процессам (модуль multiprocessing) или использовать библиотеки на C (numpy, etc.), чтобы задействовать все ядра.
  • Читаемость кода и производительность: Часто можно улучшить и читаемость, и производительность одновременно, просто выбрав удачное решение. Например, вместо: # Плохо: явный цикл, ручная проверка result = [] for x in data: if condition(x): result.append(x * 2) можно написать: # Хорошо: списковое включение, фильтрация по условию result = [x * 2 for x in data if condition(x)] Эта одна строка выполняет ту же логику. Она короче и понятнее, а за счёт реализации в C – зачастую и быстрее. Однако важно не злоупотреблять выражениями генераторов: если логика слишком сложная или вложенная, лучше вернуть к обычным циклам с комментариями, иначе читаемость пострадает. Как говорит один из принципов PEP 20, “If the implementation is hard to explain, it’s a bad idea.” («Если реализацию сложно объяснить – идея плоха»).
  • Профилируйте критичный код: Современные советы сводятся к тому, чтобы измерять, а не гадать. Интуиция по производительности может подвести. Используйте профайлеры и бенчмаркинг (модуль timeit) для критичных участков. Возможно, “узким местом” окажется не то, что вы думали. Оптимизировав код на основе фактов, вы избежите преждевременной оптимизации там, где в ней не было нужды, и сосредоточитесь на реальных проблемах.
  • Помните о PEP 8 Programming Recommendations: В конце PEP 8 есть раздел рекомендаций, многие из которых одновременно улучшают и качество, и производительность кода. Например:
    • Не используйте проверку типа через == для одиночных экземпляров None, True, False – всегда сравнивайте их через is или is not. Это быстрее и отражает намерения.
    • Избегайте пустых except: без указания исключения – это не про скорость, но про надёжность: такие блоки ловят все исключения, включая SystemExit/KeyboardInterrupt, что затрудняет прервать программу и может скрыть баги. Указывайте конкретные исключения, которые хотите перехватить.
    • Используйте менеджеры контекста (with) для работы с ресурсами (файлами, соединениями) – это гарантирует своевременное освобождение ресурсов даже при исключениях. Пример: with open('data.txt') as f: data = f.read() автоматически закроет файл, что и безопаснее, и читабельнее ручного f = open(...); ...; f.close().
    • Соблюдайте единообразие стилей возврата значений: если функция в одном месте возвращает объект, а в другом ничего (неявно None), это ухудшает понимание. PEP 8 рекомендует явно возвращать None, если в некоторых ветках нет значения, – это минимизирует неожиданные типы возврата и упрощает аннотирование типов.

В целом, оптимальный код в Python – это код «достаточно быстрый» для поставленной задачи и при этом максимально простой для понимания. В большинстве случаев читабельность важнее микроскопической экономии времени работы. Но когда вы точно знаете, что участок кода критичен (например, выполняется тысячи раз в секунду), – профилируйте, оптимизируйте, при необходимости переходите на более низкий уровень (написать расширение на Cython, использовать numpy для векторных вычислений, или даже вынести в С/С++).

И помните знаменитое правило: “Сначала сделай, чтобы работало, потом – чтобы понятно, и только потом – по возможности быстро”. В Python это особенно справедливо: зачастую «понятный» код благодаря усилиям разработчиков интерпретатора уже работает достаточно быстро, а вот «перехитривший» программистом интерпретатор код может оказаться и менее понятным, и вовсе не быстрее.

9. Частые ошибки и как их избегать

Даже опытные Python-разработчики иногда допускают типичные ошибки. Вот перечень наиболее распространённых “подводных камней” языка Python и советы, как их избежать:

  • Изменяемые объекты как значения по умолчанию: Классическая ловушка – задавать изменяемые объекты (list, dict и т.д.) в качестве значений параметров по умолчанию функции. Например, def add_item(item, lst=[]): .... В Python значение по умолчанию вычисляется один раз – в момент определения функции, а не при каждом вызове. Поэтому все вызовы без явного аргумента lst будут разделять один и тот же список!. Это приводит к неожиданному поведению: первый вызов add_item(5) создаст список [5], а второй вызов add_item(7) без аргумента добавит 7 в тот же самый список, получим [5, 7] вместо ожидаемого [7]. Решение: использовать None как сигнал отсутствия аргумента и создавать новый объект внутри функции: def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst Это обезопасит от накопления значений между вызовами.
  • Сравнение с None и булевыми значениями: Частая ошибка – использовать оператор равенства == для проверки на None или булевы литералы. В Python корректнее делать такие проверки через оператор is. Например, if x is None: вместо if x == None:. Это не только стиль (PEP 8 однозначно рекомендует is None), но и защита: перегруженный метод __eq__ объекта мог бы дать неожиданный результат при сравнении с None. Аналогично, не пишите if is_valid == True: – просто if is_valid: проверит истинность значения. Сравнение с True/False избыточно и может быть ошибочным (например, is_valid == True не поймает ситуацию, когда is_valid равно 1 или "True"). PEP 8 гласит: “Don’t compare boolean values to True or False using ==. И уж точно не стоит сравнивать через is True/False – такие проверки работают не так, как многие ожидают.
  • Использование is для строк и чисел: Обратная ситуация – злоупотребление оператором is там, где нужно сравнение равенства. Оператор is проверяет идентичность объектов (т.е. находятся ли они по одному адресу). В случае малых чисел и коротких строк Python может интернированно кешировать объекты, и a is b случайно даст True, хотя смысл сравнения – сравнить значение. Поэтому для сравнения содержимого строк, чисел и других значений используйте ==, а is только для None или одиночных объектов (синглтонов) вроде True/False или при намеренном сравнении идентичности.
  • Смешивание табуляции и пробелов: Ошибка форматирования может привести к IndentationError или, хуже, к логическим ошибкам без явного исключения. Убедитесь, что в проекте единообразные отступы – только 4 пробела на уровень, никаких символов табуляции. Современные редакторы умеют отображать символы табуляции, либо сразу вставлять пробелы при нажатии Tab. Настройте окружение так, чтобы не вносить случайно табы. Если редактируете код, доставшийся от других, и в нём использованы табы, лучше сразу прогнать автоформатер (например, autopep8 или black) для приведения к общему стилю.
  • Ошибки области видимости (scope): Новички иногда ожидают, что переменная, измененная внутри функции, изменит и внешнюю переменную. Например: count = 5 def increment(): count += 1 # UnboundLocalError! increment() Это вызовет UnboundLocalError, потому что присваивание внутри функции создаёт локальную переменную count, и интерпретатор не считает её связанной с внешней. Чтобы изменить глобальную переменную внутри функции, нужно объявить ее global count (что, однако, не является хорошей практикой). Совет: избегайте изменения глобальных переменных внутри функций – лучше возвращайте результат и присваивайте его снаружи, или инкапсулируйте состояние в объект. А если всё же нужно – не забудьте global. Аналогичная ситуация с замыканиями: для изменения переменной из внешней области видимости внутри вложенной функции используйте nonlocal.
  • Блок try/except “ловит всё”: Конструкция except: без указания конкретного исключения поймает абсолютно все исключения, включая системные (KeyboardInterrupt, SystemExit), что обычно нежелательно. Такой код может затруднить остановку программы по Ctrl+C или скрыть реальную ошибку. Правильнее перехватывать конкретные ожидаемые исключения. Если нужно перехватить вообще всё, используйте except Exception:, чтобы не ловить системные сигналы (базовый класс Exception не включает их). Еще одна рекомендация – держать минимум кода внутри блока try, чтобы не перехватывать исключения из нецелевых мест.
  • Игнорирование менеджеров контекста: Иногда пишут так: f = open('data.txt') data = f.read() # ... обработка ... f.close() Если при обработке случится исключение, close() может не выполниться, файл останется открытым. Правильнее: with open('data.txt') as f: data = f.read() Блок with гарантирует закрытие файла по выходу, даже при ошибке. Это касается и других ресурсов: сетевых соединений, блокировок, транзакций БД (для них тоже могут быть контекстные менеджеры). Всегда используйте with там, где он предусмотрен.
  • Wildcard-импорт (from module import *): Такой импорт подтягивает в пространство имён все глобальные объекты модуля. Это плохая практика в реальном проекте, так как создает неявные зависимости и может привести к конфликту имён (перекрытию функций/переменных). PEP 8 прямо утверждает: “Wildcard imports should be avoided, as they make it unclear which names are present in the namespace”импорт через * затрудняет понимание кода и работу инструментов. Используйте явный импорт: from module import func1, func2 или просто import module и обращайтесь как module.func1. Wildcard-импорт допустим разве что в интерактивной консоли для экономии времени или в некоторых редких случаях внутри пакета (когда в __init__.py нужно реэкспортировать API из подмодулей).
  • Опечатки и регистр символов: Python – регистрозависимый язык. Переменные data и Data – разные. Частая мелкая ошибка – опечататься в имени (например, использовать переменную my_list, а потом обратиться к mylist). Хороший редактор или IDE подсветят такое несоответствие. Стоит настроить инструменты типа pylint/flake8 – они предупреждают о неиспользуемых переменных, возможных опечатках и т.д. Например, если вы написали переменную и нигде не прочитали – линтер сообщит, что она не используется, возможно это орфографическая ошибка где-то.
  • Нарушение соглашений стиля (PEP 8): Это нельзя назвать ошибкой в смысле бага, но несоблюдение стиля (именование с неправильным регистром, неконсистентные отступы, отсутствие пробелов вокруг операторов) – частый недостаток кода, снижающий его качество. Решение – освоить и применять на практике PEP 8 (см. раздел 2) или, как минимум, пропускать код через инструменты форматирования. Многие ошибки стиля легко устраняются автоматически (например, автоформатер black расставит отступы и пробелы по стандарту, исчерпывая целый класс “ошибок”).

Как избегать ошибок: Лучший способ – учиться на чужом опыте и использовать инструменты. Регулярно читайте про распространённые ошибки в Python (статьи, чеклисты). Например, документация “Common Gotchas” в Hitchhiker’s Guide to Python описывает ряд таких подводных камней. Включите линтеры в процесс разработки: flake8, pylint, mypy – они поймают многое из вышеперечисленного до того, как код попадет в репозиторий. Пишите тесты, которые выявят логические ошибки. И, конечно, делайте code review: свежий взгляд коллеги может заметить оплошности, которые вы упустили.

В итоге, внимательность и дисциплина – залог того, что вы не попадётесь на типичные ловушки. А даже если попались – постарайтесь понять причину и запомнить. Со временем выработается чутьё, и подобные ошибки станут редкостью.

10. Примеры хорошего и плохого кода

Чтобы закрепить вышесказанные рекомендации, рассмотрим несколько пар примеров – неудачного (плохого) кода и его улучшенной, хорошей версии. Эти примеры демонстрируют применение лучших практик на практике.

Пример 1: Именование, форматирование и документация

Плохо:

# Пример неудачного стиля и имен
import math, sys

DATA = [1,2,3,4,5]
def ProcessData(d):
  """Process data list"""
  sum=0
  for x in d: sum+=x
  return sum

class sampleclass():
    def __init__(self, Value):
        self.Value=Value

Недостатки этого фрагмента:

  • Нарушены соглашения по стилю: имя функции ProcessData с заглавной буквы (не snake_case), имя класса sampleclass с маленькой буквы (нарушен CapWords), имя параметра Value с большой буквы (PEP 8 советует использовать lower_case для аргументов).
  • Есть импорт нескольких модулей в одной строке (import math, sys), что противоречит PEP 8 (импорты следует разделять построчно для ясности).
  • Отсутствуют пробелы после запятых в списке DATA = [1,2,3,4,5] – тяжело читать.
  • В теле функции ProcessData всё содержимое написано в одну строку, нет отступа на новой строке после for (нарушение правил форматирования, резко снижает читабельность).
  • Имя переменной sum затеняет встроенную функцию sum – плохая практика именования.
  • Docstring функции ProcessData бесполезен: “Process data list” не дает никакой новой информации, он просто перефразирует имя. Не описано поведение (что именно происходит с данными).
  • Класс sampleclass не следует стилю имён, и его атрибут Value записан с большой буквы, плюс присваивание атрибуту без пробелов вокруг =.

Хорошо:

import math
import sys

DATA = [1, 2, 3, 4, 5]

def process_data(data: list[int]) -> int:
    """Вычислить сумму элементов списка data."""
    total = 0
    for x in data:
        total += x
    return total

class SampleClass:
    """Пример класса с корректным стилем именования."""
    def __init__(self, value: float) -> None:
        self.value = value

Что исправлено и улучшено:

  • Имена приведены к соглашениям PEP 8: функция process_data в нижнем регистре с подчёркиванием, класс SampleClass с заглавной. Параметр value – в нижнем регистре. Глобальная константа DATA – заглавными буквами.
  • Импорты разделены на две строки, как рекомендует PEP 8 (каждый модуль отдельно).
  • В списке DATA добавлены пробелы после запятых, что соответствует стилю и улучшает видимость отдельных элементов.
  • В функции process_data каждая операция на своей строке с 4-пробельным отступом. Цикл и суммирование оформлены согласно стандартному стилю. Переменная total понятнее, чем sum (и не затеняет built-in sum).
  • Добавлена содержательная docstring: она кратко описывает, что делает функция. Не дублируется имя, а по сути объясняется результат.
  • В класс SampleClass добавлена docstring, пусть даже простая. Имена атрибута – self.value вместо self.Value. Пробелы вокруг = при присвоении. Добавлены аннотации типов для конструктора (например, value: float), что улучшает информативность.
  • Обратите внимание, аннотации -> int и -> None для функций process_data и __init__ соответственно – теперь сразу видно, что функция возвращает число, а конструктор не возвращает ничего (кроме неявного None).

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

Пример 2: Аннотации типов и обработка ошибок

Плохо:

def connect(db_config):
    # Подключение к базе, возвращает объект соединения или None
    import psycopg2
    try:
        conn = psycopg2.connect(db_config['dsn'])
        return conn
    except:
        # любое исключение просто глушится
        return None

conn = connect({"dsn": "dbname=test"})
if conn == None or conn == False:
    print("Connection failed")
else:
    print("Connected!")

Недостатки:

  • Функция connect не имеет аннотаций, неясно, что за db_config (словарь? строка? объект конфигурации?).
  • Импорт внутри функции (import psycopg2) – это допустимо, но обычно выносят импорты наверх модуля. В данном случае, возможно, сделано намеренно, но отмечаем.
  • Блок except: перехватывает любое исключение и превращает его в None, не логируя ошибку. Это затрудняет отладку (соединение может не установиться по разным причинам, а мы их не узнаем).
  • В коде проверки соединения используется сравнение if conn == None or conn == False. Тут сразу несколько проблем:
    • Для проверки None надо использовать is None, а не == None.
    • Сравнение с False вообще странное – объект соединения никогда не равен False (если psycopg2.connect провалится, мы вернем None, а False не вернем). Возможно, автор хотел учесть, что функция может вернуть False, но это не документировано.
    • Такая логика нечитаема – лучше явно проверить на None.
  • При удачном подключении печатается “Connected!”, но в случае неудачи мы теряем инфу о причине (см. выше).

Хорошо:

import psycopg2
from psycopg2 import OperationalError, DatabaseError

def connect(db_config: dict) -> psycopg2.extensions.connection | None:
    """Установить соединение с БД PostgreSQL.

    Args:
        db_config (dict): Конфигурация БД, должна содержать ключ 'dsn'.

    Returns:
        Объект соединения или None, если подключение не удалось.
    """
    dsn = db_config.get('dsn')
    if dsn is None:
        raise ValueError("DSN string is required in db_config")
    try:
        conn = psycopg2.connect(dsn)
        return conn
    except (OperationalError, DatabaseError) as e:
        # Логируем ошибку и возвращаем None
        print(f"Database connection error: {e}")
        return None

conn = connect({"dsn": "dbname=test"})
if conn is None:
    print("Connection failed")
else:
    print("Connected!")

Что сделано:

  • Импорты вынесены вне функции, и перечислены конкретные исключения, которые мы собираемся обрабатывать (OperationalError, DatabaseError). Это улучшает читаемость и слегка ускоряет (импорт не выполняется каждый раз при вызове функции).
  • Добавлены аннотации: параметр db_config ожидается словарём, возвращаемый тип – объект соединения или None (используем синтаксис | None, доступный с Python 3.10, что эквивалентно Optional[connection]).
  • Docstring функции поясняет требование (наличие 'dsn' в конфиге) и поведение (возвращает None при неудаче).
  • Перед попыткой соединения явно проверяется наличие dsn в конфиге. Если отсутствует – бросается исключение ValueError с понятным сообщением. Это лучше, чем получить KeyError где-то внутри psycopg2.
  • Блок except ловит только ожидаемые исключения подключения к базе. Мы перехватываем их, логируем сообщение об ошибке (через print, но в реальном коде лучше использовать logging). Важно: мы не перехватываем все исключения – если вдруг вылетит что-то непредвиденное (например, NameError внутри функции или KeyboardInterrupt), оно не будет заглушено.
  • Логика проверки результата соединения вне функции упрощена до if conn is None: – понятно, что если None, то подключение не удалось. Условие conn is None гораздо яснее, чем предыдущий вариант, и корректнее с точки зрения стиля.
  • В сообщении об ошибке мы хотя бы выводим текст исключения e. В боевом коде, возможно, стоило бы выбросить исключение дальше или записать в лог, но для примера достаточно печати.

Итог: улучшенная версия не подавляет все ошибки молча, а обрабатывает ожидаемые ситуации. Код снабжён аннотациями и документирован. Проверка результата соединения приведена в соответствие с рекомендациями (используется is None). Такой код и работать будет надёжнее, и поддерживать его значительно проще.

Пример 3: Ошибка с изменяемым default-значением

Плохо:

def add_to_collection(item, collection=[]):
    collection.append(item)
    return collection

print(add_to_collection(1))  # [1]
print(add_to_collection(2))  # [1, 2] - неожиданный результат?

Как обсуждалось ранее, этот код использует список [] как значение по умолчанию для параметра collection. В результате при втором вызове в коллекции уже лежит предыдущий элемент. Для функции, которая по смыслу должна каждый раз возвращать новый список с одним элементом (если не передан существующий), это ошибка. Фактически, collection здесь выступает как статическая переменная, сохраняя состояние между вызовами.

Хорошо:

def add_to_collection(item, collection=None):
    """Добавить item в коллекцию (список). Если коллекция не передана, будет создан новый список."""
    if collection is None:
        collection = []
    collection.append(item)
    return collection

print(add_to_collection(1))  # [1]
print(add_to_collection(2))  # [2]  - теперь каждый вызов независим

Исправлено с применением шаблона None-sentinel:

  • Если collection не передана, по умолчанию она равна None.
  • Внутри функции проверяем if collection is None: collection = [], тем самым создавая новый список для каждого вызова.
  • Теперь функция работает ожидаемо: каждый вызов без аргумента будет получать свой новый список.
  • Добавлена docstring, поясняющая поведение (особенно важно указать, что происходит, если коллекция не задана).
  • Такой код безопасен и соответствует рекомендациям (мы явно сравниваем с None, а не с == None).

Пример 4: Хороший vs плохой Python-код (суммарный пример)

Напоследок приведём сравнение двух фрагментов, иллюстрирующих множество аспектов сразу.

Вариант 1 (плохой):

import os, sys

def DoSomething(FileList):
    # Выполняет некую операцию над списком файлов
    res=[];
    for i in range(len(FileList)):
        file=FileList[i]
        f = open(file); data = f.read(); f.close()
        if data!=None and data!="":
            res.append(len(data))
    return res

print(DoSomething(["file1.txt","file2.txt"]))

Основные недостатки:

  • Импорты в одну строку (os, sys) – лучше раздельно, но здесь sys даже не используется.
  • Функция DoSomething названа с заглавной буквы и глагол+сущность слитно – не в стиле Python. Лучше do_something или, лучше, дать осмысленное название, отражающее суть операции.
  • Параметр FileList с заглавной буквы – не соответствует стилю, и по смыслу это iterable, лучше назвать files.
  • Документация: есть комментарий, но он дублирует имя. Неясно, например, что функция возвращает.
  • Реализация: for i in range(len(FileList)) – не питонисто. Для перебора элементов списка в Python есть прямой способ: for file in FileList:.
  • Переменная file затеняет встроенное имя file (тип). Лучше избегать таких совпадений.
  • Чтение файла сделано вручную с открытием и закрытием – явное закрытие можно заменить на with.
  • Проверка if data != None and data != "" – избыточно. Если файл пустой, data будет пустой строкой, которая в логическом контексте False, поэтому достаточно if data:. А на None data не может быть, потому что read() вернёт строку (пусть даже пустую). Так что проверка на None лишена смысла. К тому же, сравнение с None – опять через !=, а не is not.
  • Добавление в список результатов: ок, но res=[] лучше отделять пробелами, res = [].
  • Нет аннотаций типов, непонятно что принимает/возвращает функция без чтения кода.
  • После выполнения функции результат просто печатается. В реальном коде, наверное, что-то дальше бы делалось.

Вариант 2 (хороший):

import os

def count_file_lengths(files: list[str]) -> list[int]:
    """Вернуть список длин содержимого для каждого файла из списка files."""
    lengths: list[int] = []
    for filename in files:
        # Читаем файл, если он не пустой
        try:
            with open(filename, 'r') as f:
                data = f.read()
        except FileNotFoundError:
            print(f"Warning: file {filename} not found.")
            lengths.append(0)
            continue
        if data:
            lengths.append(len(data))
        else:
            lengths.append(0)
    return lengths

result = count_file_lengths(["file1.txt", "file2.txt"])
print(result)

Улучшения:

  • Импорт os оставлен, sys убран как неиспользуемый. (Предположим, os тут нужен для чего-то вне фрагмента – если нет, тоже бы убрали).
  • Функция переименована в count_file_lengths – теперь по имени понятно, что она делает (считает длины файлов). Имя параметра files – во множественном числе, отражает список.
  • Добавлена аннотация: функция принимает список строк (путей файлов) и возвращает список целых. Это сразу снимает часть вопросов.
  • Docstring описывает поведение (что возвращается). Кратко, но информативно.
  • Перебор файлов через прямой цикл for filename in files: – понятнее и идиоматичнее, чем индексы.
  • Используется менеджер контекста with open для чтения файла – гарантированно закрытие.
  • Добавлен обработчик исключения FileNotFoundError – на случай, если файл из списка не найден, мы не хотим прерывать всю функцию. Мы печатаем предупреждение и кладём 0 в результат для этого файла, затем продолжаем цикл (continue).
  • Проверка содержимого файла: if data: – пустая строка оценивается как False, None тут не появится (если файл существует, data всегда строка). Поэтому это верный питоничный способ проверки “непустости”.
  • Если файл пустой, в lengths добавляем 0, если не пустой – длину содержимого.
  • Итого, функция всегда возвращает список чисел той же длины, что и список файлов (для несуществующих или пустых файлов ставим 0). Это оговорено в доке.
  • В конце пример использования: вызываем функцию и печатаем результат. Функция не печатает внутри себя ничего (кроме предупреждений), а возвращает чистый результат, что правильно для функций. Мы отделили “логику” и “интерактив” печати.

Этот пример демонстрирует сразу несколько аспектов: улучшенное именование, обработку ошибок, использование with, логическую проверку на пустые данные, аннотации и документацию. Код стал длиннее на пару строк, но при этом явно надёжнее и понятнее. Такой код легче модифицировать (например, вместо print можно изменить логику обработки отсутствующих файлов без риска сломать всё остальное).


Подводя итог, принципы чистого кода на Python в 2026 году сводятся к: придерживайтесь согласованного стиля (PEP 8), пишите понятные имена, документируйте поведение, покрывайте код тестами, используйте возможности языка (аннотации, менеджеры контекста, генераторы) и учитесь на типичных ошибках. Это делает ваш код не только эстетически приятным, но и надёжным в долгосрочной перспективе. Как сказал Тим Питерс в своем «Дзен Python», “If the implementation is easy to explain, it may be a good idea.” – если реализацию легко объяснить, возможно, это хорошая идея. Следуя лучшим практикам, вы как раз и стремитесь писать такой код – простой, понятный и идиоматичный. Это окупается сторицей на этапах сопровождения и развития проектов.

Источники и ссылки:

  • PEP 8 – Style Guide for Python Code – официальный гид по стилю Python.
  • PEP 20 – The Zen of Python – философия Python, 20 афоризмов о дизайне кода.
  • PEP 257 – Docstring Conventions – соглашения по оформлению строк документации.
  • Hitchhiker’s Guide to Python – раздел Common Gotchas (неявные “подводные камни” Python).
  • Статья “Python Testing Best Practices: 2025” – о современных подходах к тестированию, Pytest.
  • Опрос “Python Typing Survey 2025” – результаты по применению типизации (86% используют часто).
  • Real Python – “Python Best Practices – Project Layout” – рекомендации по структуре проекта.
  • Обсуждение на Python.org: “Wildcard imports” – почему не рекомендуется import *
+1
0
+1
1
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *