Стандарты и сертификаты · тема 3
5

PEP 8: оформление
кода на Python

PEP 8 — стандарт оформления кода на Python. Его соблюдают стандартная библиотека языка и большинство проектов, а линтеры проверяют его автоматически. В главе правила разобраны по разделам документа, к каждому — пара примеров: как пишут без стандарта и как по нему. В конце — разбор настоящего файла и вывод линтера.

Документ
PEP 8 «Style Guide for Python Code», 2001, обновляется до сих пор
Уровень
Базовый
Где встречается
Любой проект на Python, ревью кода, проверки в CI
Зачем это знать
Чтобы код читался одинаково и проходил проверку линтера

§1Что такое PEP и зачем PEP 8

PEP (Python Enhancement Proposal) — документ с предложением по развитию языка Python. У каждого PEP есть номер, автор и статус. PEP 8 написали Гвидо ван Россум, Барри Уорсо и Ник Коглан.

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

Интерпретатор PEP 8 не проверяет: программа с нарушениями запустится. Обязательным его делает проект — правилом в README, настройкой линтера и проверкой в CI, которая не пропускает изменение с нарушениями.

PEP 8 на сайте peps.python.org.
PEP 8 на сайте peps.python.org. Слева — оглавление по разделам. Порядок разделов этой главы повторяет документ.

Сам PEP 8 разрешает отступать от правил, если правило ухудшает читаемость или ломает единообразие с уже написанным кодом. Раздел об этом называется «A Foolish Consistency is the Hobgoblin of Little Minds».

§2Отступы

Один уровень отступа — 4 пробела. Табуляцию не используют; смешивать табуляцию и пробелы в одном файле Python 3 запрещает.

Отступ тела функции

так не надо
def total(prices):
  result = 0
  for p in prices:
        result += p
  return result
так надо
def total(prices):
    result = 0
    for price in prices:
        result += price
    return result

Слева отступы в два и шесть пробелов: уровни вложенности на глаз не различить. Справа каждый уровень — четыре пробела.

Если строка не помещается, её продолжают внутри скобок. Стандарт допускает два способа: выровнять продолжение по открывающей скобке или сделать висячий отступ — первая строка заканчивается скобкой, остальные сдвинуты на уровень.

Продолжение строки внутри скобок

так не надо
result = send_report(user_id, period_start,
    period_end, include_archived)
так надо
# выравнивание по открывающей скобке
result = send_report(user_id, period_start,
                     period_end, include_archived)

# висячий отступ
result = send_report(
    user_id, period_start,
    period_end, include_archived,
)

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

Висячий отступ в заголовке функции

так не надо
def create_ticket(
    title, description,
    priority, author):
    validate(title)
    return save(title)
так надо
def create_ticket(
        title, description,
        priority, author):
    validate(title)
    return save(title)

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

§3Длина строки и перенос

Строка кода — не длиннее 79 символов, строка комментария или документации — не длиннее 72. Стандарт разрешает команде договориться о пределе до 99 символов для кода. Форматировщик black по умолчанию использует 88.

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

Двоичный оператор ставят в начале новой строки. В конце предыдущей строки его не оставляют. Так все слагаемые видны в одном столбце, как в математической записи.

Где переносить оператор

так не надо
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)
так надо
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

Пример взят из самого PEP 8. Справа знак стоит перед числом, и сразу видно, какое слагаемое прибавляется, а какое вычитается.

Длинное условие

так не надо
if user.is_active and user.email_confirmed and not user.is_blocked and user.role in ('teacher', 'admin'):
    grant_access(user)
так надо
if (user.is_active
        and user.email_confirmed
        and not user.is_blocked
        and user.role in ('teacher', 'admin')):
    grant_access(user)

Условие заключено в скобки и разбито по одному требованию на строку. Дополнительный отступ отделяет условие от тела if.

§4Пустые строки и кодировка

Функции и классы верхнего уровня отделяют двумя пустыми строками, методы внутри класса — одной. Внутри функции пустой строкой разделяют логические шаги, но не после каждой строки.

Пустые строки между определениями

так не надо
import os
def load(path):
    return open(path).read()
class Report:
    def __init__(self, title):
        self.title = title
    def render(self):
        return self.title.upper()
так надо
import os


def load(path):
    return open(path).read()


class Report:
    def __init__(self, title):
        self.title = title

    def render(self):
        return self.title.upper()

Справа границы функций и классов видны сразу, даже при быстрой прокрутке файла.

Файлы сохраняют в UTF-8; строку с указанием кодировки в начале файла для UTF-8 не пишут. Имена в коде — латиницей, даже если комментарии на русском.

§5Импорты

Каждый модуль импортируют на отдельной строке. Импорты стоят в начале файла, после строки документации модуля, и идут тремя группами: стандартная библиотека, сторонние пакеты, модули своего проекта. Группы разделяют пустой строкой.

Порядок и группы импортов

так не надо
from .models import Ticket
import os, sys
import requests
from datetime import date
from fastapi import FastAPI
from .db import *
так надо
import os
import sys
from datetime import date

import requests
from fastapi import FastAPI

from .db import get_session
from .models import Ticket

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

Импорт со звёздочкой from module import * не используют: непонятно, какие имена появились в модуле, и одно из них может незаметно заменить другое. Абсолютные импорты предпочтительнее относительных. Порядок импортов расставляет isort или ruff с правилом I.

§6Пробелы в выражениях

Пробелов нет сразу внутри скобок, перед запятой, двоеточием и точкой с запятой, перед открывающей скобкой вызова и индекса. Вокруг присваивания и сравнений — по одному пробелу.

Пробелы внутри скобок и перед ними

так не надо
spam( ham[ 1 ], { eggs: 2 } )
print (x , y)
dct ['key'] = lst [index]
if x == 4 : print(x , y)
так надо
spam(ham[1], {eggs: 2})
print(x, y)
dct['key'] = lst[index]
if x == 4:
    print(x, y)

Слева пробелы стоят внутри скобок и перед ними, а тело if записано в той же строке. PEP 8 не рекомендует ни то, ни другое; справа тело if перенесено на отдельную строку.

Пробелы вокруг операторов

так не надо
i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x+y*y
c = (a+b) * (a-b)
так надо
i = i + 1
submitted += 1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)

Вокруг присваивания пробелы обязательны. В выражении с операторами разного приоритета пробелы ставят вокруг операторов с низшим приоритетом: x*2 - 1 читается как «x на 2, минус 1».

Знак равенства в аргументах

так не надо
def connect(host, port = 5432, timeout = 30):
    ...

connect(host = 'db', port = 6432)

def munge(sep:str='-', limit: int=10):
    ...
так надо
def connect(host, port=5432, timeout=30):
    ...

connect(host='db', port=6432)

def munge(sep: str = '-', limit: int = 10):
    ...

Для именованного аргумента и значения по умолчанию пробелы вокруг = не ставят. Исключение — параметр с аннотацией: там пробелы нужны, sep: str = '-'.

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

§7Висячая запятая

Если элементы списка, словаря или аргументы записаны по одному на строку, после последнего ставят запятую, а закрывающую скобку — на отдельной строке.

Запятая после последнего элемента

так не надо
FILES = [
    'setup.cfg',
    'tox.ini'
    ]
так надо
FILES = [
    'setup.cfg',
    'tox.ini',
]

Когда добавят третий файл, справа в истории изменений будет одна новая строка. Слева изменятся две: к старой строке придётся дописать запятую.

Кортеж из одного элемента запятой обязан заканчиваться: (name,). Без запятой это просто значение в скобках.

§8Комментарии

Комментарий объясняет, почему код написан так. Пересказывать, что делает код, не нужно: это видно из самого кода. Комментарий начинается с # и пробела. Комментарий в конце строки отделяют от кода минимум двумя пробелами. PEP 8 считает комментарий, который расходится с кодом, хуже отсутствия комментария, поэтому при изменении кода комментарий исправляют тоже.

Комментарий в конце строки

так не надо
x = x + 1 # прибавить единицу
retries=3#число попыток
так надо
x = x + 1  # компенсация рамки: ширина считается без неё
retries = 3  # провайдер разрывает соединение после 3 ошибок

Слева комментарии повторяют код. Справа они сообщают то, чего в коде нет: причину выбора значения.

Блочный комментарий

так не надо
#проверяем дубли
#потому что форма не проверяет
if exists(email):
    raise DuplicateEmail(email)
так надо
# Форма регистрации не проверяет, занята ли почта (задача #198),
# поэтому повтор отсекается здесь, до записи в базу.
if exists(email):
    raise DuplicateEmail(email)

Блочный комментарий пишут полными предложениями, с прописной буквы. Каждая строка начинается с # и пробела.

§9Имена

ЧтоСтильПример
модулькороткие строчные, при необходимости с подчёркиваниемbackup.py, send_welcome.py
пакеткороткие строчные, без подчёркиванияreports
класскаждое слово с прописной, слитно (CapWords)TicketRepository
исключениекак класс, с окончанием ErrorDuplicateEmailError
функция, метод, переменнаястрочные через подчёркивание (snake_case)load_profile, user_id
константа модуляпрописные через подчёркиваниеMAX_RETRIES
внутреннее имяс одним подчёркиванием в начале_parse_row
первый аргумент методаself; метода класса — clsdef render(self)

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

так не надо
class ticket_repo:
    def LoadAll(self):
        l = []
        for O in self.Items:
            l.append(O)
        return l

MaxRetries = 3
так надо
class TicketRepository:
    def load_all(self):
        tickets = []
        for ticket in self.items:
            tickets.append(ticket)
        return tickets

MAX_RETRIES = 3

PEP 8 отдельно запрещает однобуквенные имена l, O и I: в некоторых шрифтах их не отличить от единицы и нуля.

Имя, совпадающее с встроенным

так не надо
def count(list, id, type):
    return len([x for x in list if x.type == type])
так надо
def count(items, item_id, kind):
    return len([x for x in items if x.kind == kind])

Слева параметры закрывают встроенные list, id и type: внутри функции их уже не вызвать. Если нужное имя занято, PEP 8 советует добавить подчёркивание в конце: class_.

§10Рекомендации по программированию

Последний раздел PEP 8 посвящён приёмам, при которых код ведёт себя предсказуемо.

Сравнение с None, True и пустотой

так не надо
if result == None:
    ...
if is_active == True:
    ...
if len(tickets) == 0:
    ...
так надо
if result is None:
    ...
if is_active:
    ...
if not tickets:
    ...

С None сравнивают через is: оператор == можно переопределить в классе. Логическое значение и пустой список проверяют самим значением.

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

так не надо
if type(price) == int:
    ...
так надо
if isinstance(price, int):
    ...

isinstance учитывает наследование: значение подкласса int тоже пройдёт проверку.

Начало и конец строки

так не надо
if filename[-4:] == '.csv':
    ...
if url[:8] == 'https://':
    ...
так надо
if filename.endswith('.csv'):
    ...
if url.startswith('https://'):
    ...

Срезы с числами ломаются, когда меняется длина образца. Методы читаются как обычная фраза.

Перехват исключений

так не надо
try:
    rows = load(path)
except:
    rows = []
так надо
try:
    rows = load(path)
except FileNotFoundError:
    rows = []

Голый except перехватывает всё, даже нажатие Ctrl+C и опечатку в имени переменной. Перехватывают только то исключение, которое умеют обработать.

Объём блока try

так не надо
try:
    value = collection[key]
    result = handle(value)
except KeyError:
    result = key_not_found(key)
так надо
try:
    value = collection[key]
except KeyError:
    result = key_not_found(key)
else:
    result = handle(value)

Слева KeyError изнутри handle будет ошибочно принят за отсутствие ключа. В блоке try оставляют только строку, которая может вызвать ожидаемое исключение.

Единообразный return

так не надо
def discount(user):
    if user.is_student:
        return 0.2
    if user.is_teacher:
        return 0.1
так надо
def discount(user):
    if user.is_student:
        return 0.2
    if user.is_teacher:
        return 0.1
    return 0.0

Слева для остальных пользователей функция молча вернёт None, и расчёт цены упадёт далеко от причины. Если функция что-то возвращает, она возвращает значение на любом пути.

Лямбда в переменной

так не надо
square = lambda x: x * x
так надо
def square(x):
    return x * x

У функции, созданной через def, есть имя: оно будет в сообщении об ошибке и в отладчике. Лямбду используют там, где функция нужна один раз, например в sorted(..., key=lambda t: t.date).

Работа с файлом

так не надо
f = open('report.csv')
data = f.read()
f.close()
так надо
with open('report.csv', encoding='utf-8') as f:
    data = f.read()

Конструкция with закроет файл даже при исключении. Кодировку указывают явно: по умолчанию в Windows она другая.

§11Разбор файла целиком

Ниже — файл, собранный «на скорую руку», и тот же файл после приведения к PEP 8. Поведение программы не изменилось.

до: report.py
import os,sys
from datetime import *
import requests
def MakeReport( Users,Date ):
    l=[]
    for u in Users :
        if u['active']==True and u['email']!=None:
            l.append( u['name']+' '+u['email'] ) # добавить
    try:
        r=requests.post('https://api.example/report',json={'rows':l,'date':str(Date)})
    except:
        return None
    return r.status_code
после: report.py
import requests

REPORT_URL = 'https://api.example/report'


def make_report(users, report_date):
    """Отправить список активных пользователей с почтой; вернуть код ответа."""
    rows = [
        f"{user['name']} {user['email']}"
        for user in users
        if user['active'] and user['email'] is not None
    ]
    payload = {'rows': rows, 'date': report_date.isoformat()}
    try:
        response = requests.post(REPORT_URL, json=payload, timeout=10)
    except requests.RequestException:
        return None
    return response.status_code
Что изменилосьПравило PEP 8
убраны неиспользуемые os, sys и импорт со звёздочкойимпорты
MakeReport → make_report, l → rowsимена
== True и != None замененырекомендации по сравнениям
адрес вынесен в константу, длинная строка разбитадлина строки, имена констант
голый except заменён конкретным исключениемрекомендации по исключениям
пробелы внутри скобок убраны, вокруг присваиваний добавленыпробелы в выражениях
добавлена строка документацииPEP 257, следующая глава

§12Как проверяют соответствие

Вручную PEP 8 не проверяют: это делают программы. Линтер находит нарушения и называет их кодом. Форматировщик сам переписывает оформление.

ИнструментЧто делаетКоманда
pycodestyleпроверяет оформление по PEP 8; коды E и Wpycodestyle report.py
flake8pycodestyle плюс поиск ошибок (неиспользуемые импорты); коды E, W, Fflake8 .
ruffошибки (F) и правила PEP 8 об импортах, сравнениях и именах; работает быстро и умеет исправлять; правила пробелов в нём пока в предварительном режимеruff check . --fix
black, ruff formatпереписывает оформление файла целикомblack . · ruff format .
Настоящий вывод pycodestyle и ruff для файла «до» из предыдущего параграфа.
Настоящий вывод pycodestyle и ruff для файла «до» из предыдущего параграфа. Каждая строка — файл, строка и столбец, код правила и сообщение. pycodestyle находит нарушения пробелов и пустых строк, ruff — неиспользуемые импорты, сравнения и голый except. Пометка [*] означает, что ruff исправит нарушение сам. Файл «после» оба инструмента пропускают без замечаний.
КодЧто означает
E201, E202пробел сразу после «(» или перед «)»
E225нет пробелов вокруг оператора
E231нет пробела после запятой
E261меньше двух пробелов перед комментарием в конце строки
E302ожидались две пустые строки
E501строка длиннее допустимого
E711сравнение с None через == или !=
E712сравнение с True или False через ==
E722голый except
E741неоднозначное имя: l, O, I
F401импорт не используется
F403импорт со звёздочкой

Проверку включают в CI: изменение, которое не проходит линтер, не сливают в основную ветку. Настройки хранят в pyproject.toml, чтобы у всех участников они были одинаковыми.

pyproject.toml
[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N"]   # оформление, ошибки, импорты, имена

§13Частые ошибки

Считать PEP 8 правилом интерпретатораПрограмма запустится; соблюдение обеспечивает проект и линтер.
Спорить об оформлении на ревьюОформление исправляет форматировщик; на ревью обсуждают смысл изменения.
Переносить оператор в конец строкиPEP 8 рекомендует ставить его в начале новой строки.
Сравнивать с None через ==Сравнивают через is и is not.
Перехватывать всё голым exceptПерехватывают конкретное исключение, которое умеют обработать.
Называть переменную l, list или idНеоднозначные и занятые имена мешают читать и ломают встроенные функции.

§14Проверьте себя

  1. Сколько пробелов в одном уровне отступа и какова наибольшая длина строки кода и комментария?
  2. Где по PEP 8 ставят двоичный оператор при переносе длинного выражения? Почему?
  3. В каком порядке и какими группами записывают импорты?
  4. Когда вокруг знака = в аргументах ставят пробелы, а когда нет?
  5. Как по PEP 8 называют класс, исключение, функцию и константу? Приведите по примеру.
  6. Почему с None сравнивают через is и почему голый except считается ошибкой?
  7. Что сообщают коды E501, E711 и F401 в выводе линтера?
Оформление4 пробела79 / 72 символаоператор в начале строки2 пустые строки — верхний уровеньвисячая запятая
Именаsnake_case — функцииCapWords — классы…Error — исключенияUPPER_CASE — константыне l, O, I
Приёмыis Noneif not itemsisinstanceexcept конкретноеwith open(...)
Проверкаruff check --fixblack · ruff formatpyproject.tomlпроверка в CI

Справочник «Стандарты и сертификаты» · глава 5Макаров Максим Николаевич

Кадр