Опирается на правила: PY-7.1PY-7.4, PY-7.X1, PY-7.X2 из Python Style Guide → раздел 7. Комментарии.

Важно знать

  • По умолчанию — не пишем комментарии. Имена, тип-хинты и структура говорят сами.
  • Inline-комментарий (#) запрещён в production и в тестах — WHY выражается именем или структурой.
  • Docstring уместен только когда добавляет неочевидный контракт: инвариант, единицы, побочный эффект.
  • Не цитируем коды правил в коде (R-AGG-1, PYTS-9, AUTH-15).
  • Не пишем «что тут было», «removed because», «TODO до ...» — git blame авторитет.
  • Закомментированный код — удалять немедленно.
  • # noqa/# type: ignore — только с кодом нарушения и кратким обоснованием.
  • Docstring пишется в императиве («Return», «Raise», «Save»), закрывающие """ на отдельной строке для многострочных.

Комментарии в Python — это не дополнение к коду, а признак того, что код не смог сказать сам. UCP занимает жёсткую позицию: меньше комментариев, никаких inline, docstring только там, где контракт действительно неочевиден. Python выигрывает у Java в краткости — код и так читается; добавлять поверх объяснения значит не доверять читателю.

По умолчанию — не пишем

PY-7.1: имя + структура + тип-хинты первичны.

def charge(order: Order) -> None:
    if order.is_paid:
        return
    payment_provider.charge(order)

Здесь ничего не нужно объяснять. is_paid, charge, guard clause — читается линейно. Добавление # проверяем статус перед if — шум.

Когда имя не справляется — не комментируем, а переименовываем или рефакторим:

def process(o):
    if o.s == "p":
        ...
def fulfill_order(order: Order) -> None:
    if order.status == OrderStatus.PAID:
        ...

Второй вариант не нуждается в комментарии — имя и тип говорят всё.

Docstring уместен только когда добавляет контракт

PY-7.4: docstring — не пересказ сигнатуры, не для каждой функции.

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

class OrderRepository(Protocol):
    async def find_by_customer(
        self,
        customer_id: UUID,
        *,
        include_cancelled: bool = False,
    ) -> list[Order]:
        """Return orders sorted by created_at desc, newest first.

        Cancelled orders are excluded by default; pass include_cancelled=True
        to include them. Does not load line items — use find_with_items for that.
        """

Здесь docstring добавляет три вещи, которые не видны из сигнатуры: порядок сортировки, поведение по умолчанию для include_cancelled и предупреждение про lazy-load.

Docstring не нужен, если он пересказывает сигнатуру:

async def find_by_id(self, order_id: UUID) -> Order | None:
    """Find order by id. Returns None if not found."""

Order | None уже говорит «вернёт None если не найдено». Docstring — шум.

Формат docstring

PEP 257: первая строка — краткий императив, закрывающие """ отдельно для многострочных.

async def calculate_total(order: Order) -> Decimal:
    """Return total price including VAT, rounded to 2 decimal places."""


async def reserve_stock(
    product_id: UUID,
    quantity: int,
    *,
    timeout_seconds: int = 30,
) -> None:
    """Reserve product stock for a pending order.

    Raises StockReservationError if stock is insufficient.
    Reservation expires after timeout_seconds if not confirmed.
    Side effect: publishes StockReservedEvent to the outbox.
    """

Что входит в docstring как неочевидный контракт:

  • Инвариант («sorted by created_at desc»).
  • Единицы («timeout_seconds», «rounded to 2 decimal places»).
  • Побочный эффект («publishes StockReservedEvent»).
  • Условия исключений, если они не очевидны из имени.

Не цитируем коды правил

PY-7.2: коды живут в гайде и commit, не в коде.

# AVOID
class OrderAggregate:
    def confirm(self) -> None:
        # R-AGG-1: только из DRAFT
        if self.status != OrderStatus.DRAFT:
            raise InvalidStatusError(self.status)
        ...

Причины:

Дублирует source-of-truth. Правило живёт в спеке или style-guide. Если код противоречит — правило авторитет. Комментарий с кодом — дублирующая запись, которая устаревает молча.

Хрупко. При следующей ревизии гайда нумерация уезжает: R-AGG-1 стало R-AGG-3. Комментарии в коде становятся ложью — тысячи мест, никто не обновляет.

Шум. Соответствие правилу выражается структурой. @dataclass(frozen=True, slots=True) — это уже PY-8.2. Protocol для порта — уже PY-6.3. Явная пометка «соответствует PY-8.2» избыточна.

Где живут коды правил:

  • Commit messages: PY-7.1: remove inline comments from order handler.
  • PR description: «Применяет PY-8.2 — переводит carrier'ы на frozen dataclass».
  • Style-guide: ссылается на собственные коды.

Не «что тут было», «убрано для X»

PY-7.3: git blame — авторитетный источник.

# AVOID
class ProductService:
    async def create(self, command: CreateProduct) -> Product:
        # previously used sync session
        # removed redis cache because of consistency issues
        # TODO: restore after migration to v2
        product = Product.create(command)
        await self._repository.save(product)
        return product

Все три комментария — шум:

  • «previously used sync session» — видно из git blame, коммит объяснит почему.
  • «removed redis cache» — то же.
  • «TODO: restore after migration to v2» — если нужен, он должен быть задачей в issue tracker, не строкой в коде.
class ProductService:
    async def create(self, command: CreateProduct) -> Product:
        product = Product.create(command)
        await self._repository.save(product)
        return product

Закомментированный код

PY-7.X1: удалять немедленно.

# AVOID
async def find_active_customers(self) -> list[Customer]:
    # customers = await self._session.execute(
    #     select(CustomerModel).where(CustomerModel.active == True)
    # )
    # return [to_domain(c) for c in customers.scalars()]
    return await self._cache.get_active_customers()

Закомментированный код — не «на всякий случай». Он:

  • Шумит в review и diff.
  • Гниёт — через месяц не работает с новыми типами.
  • Говорит читателю «кто-то не был уверен» — дестабилизирует.

Git помнит удалённое — git log -p покажет и код, и commit, и объяснение.

noqa и type: ignore — с обоснованием

PY-7.X2: # noqa/# type: ignore без кода и обоснования запрещены.

# AVOID
result = some_external_function()  # type: ignore
long_url = "https://very-long-url-that-exceeds-line-length-limit.example.com/api/v2/endpoint"  # noqa
# PREFER
result: SomeType = some_external_function()  # type: ignore[return-value]  # justify: third-party stub incomplete
long_url = (
    "https://very-long-url-that-exceeds-line-length-limit"
    ".example.com/api/v2/endpoint"
)

Правила:

  • # type: ignore[<code>] — указывать конкретный код ошибки mypy, добавлять # justify: ... если причина неочевидна.
  • # noqa: <CODE> — указывать конкретный ruff-код, не голый # noqa.
  • Голый # noqa допускается только для E501 (длина строки), если строку объективно нельзя разбить (URL, бинарные данные).

Исключение из PY-7.1 — именно # type: ignore и # noqa это допустимые комментарии-директивы инструменту, не пояснения для человека. Но только с кодом и обоснованием (PY-RUFF-3).

Когда WHY всё-таки нужен

PY-7.1 допускает нарушение, если оно улучшает читаемость (PY-1.1). Но ревьюер обязан объяснить, чем именно нарушение лучше. На практике это узкие случаи:

async def find_locked(self, order_id: UUID) -> Order | None:
    # FOR UPDATE SKIP LOCKED не работает с CTE в PostgreSQL < 14.
    # При апгрейде до 14 заменить на selectinload + with_for_update(skip_locked=True).
    stmt = (
        select(OrderModel)
        .where(OrderModel.id == order_id)
        .with_for_update(skip_locked=True)
    )
    ...

Это workaround для конкретного поведения базы данных. Имя метода и типы не могут это выразить — здесь комментарий оправдан. Обратите внимание: комментарий объясняет почему именно так, а не что делает код.

Что считается WHY:

  • Workaround для известного бага базы данных, библиотеки, платформы.
  • Compliance или security-рассуждение («401 vs 403: клиент должен сделать refresh, не relogin»).
  • Performance trade-off («linear scan быстрее для < 5 элементов»).
  • Hidden constraint («thread-safe только если caller держит lock»).

Что не WHY:

  • «Создаём объект» — видно по Product.create(...).
  • «Возвращаем результат» — видно по return.
  • «Перебираем список» — видно по for.

Что запрещено

АнтипаттернПравилоЧто взамен
# загружаем заказ перед await repo.find(...)PY-7.1имя метода говорит само
# R-AGG-1: только из DRAFT в кодеPY-7.2commit message / PR description
# removed because ..., # TODO до v2PY-7.3git blame / issue tracker
Docstring «Return order by id. Returns None if not found.»PY-7.4-> Order \| None уже это говорит
Закомментированный блок кодаPY-7.X1удалить, git помнит
# type: ignore без кода и justifyPY-7.X2# type: ignore[return-value] # justify: ...
# noqa без кода нарушенияPY-7.X2# noqa: E501

Куда дальше

  • Именование — хорошее имя устраняет необходимость в комментарии.
  • Форматирование — структура кода как форма документации.
  • Тайп-хинты — X | None, Protocol, аннотации как машиночитаемая документация.
  • Enforcement через ruff + mypyPY-RUFF-3 покрывает noqa/type: ignore без кода.