Перейти к основному содержимому

Среда и инструменты

Единый тулчейн курса:

  • uv - окружение и зависимости
  • ruff - стиль и ошибки
  • mypy - типы
  • pre-commit - авто-проверка перед коммитом

Тот же тулчейн гоняет CI - значит «зелёно локально» = «зелёно в грейдере»
Стандарт стиля - PEP 8, но следить за ним вручную не нужно - это работа ruff, а не твоей памяти

Теория​

PEP 8 - стандарт стиля​

PEP 8 - официальный style guide для Python (Python Enhancement Proposal №8)
Один и тот же код можно оформить десятком способов - без общих правил в командном проекте начинается разнобой, из-за которого сложнее читать чужой код и легче пропустить баг на ревью
Код читают чаще, чем пишут - и читают его другие (а ещё - ты сам через месяц)

Основные правила:

  • 4 пробела на уровень отступа, не таб
  • snake_case для переменных и функций, PascalCase для классов, UPPER_CASE для констант
  • ограничение длины строки (у нас - 88 символов, см. pyproject.toml)
  • импорты вверху файла, сгруппированы: стандартная библиотека -> сторонние пакеты -> локальные модули
  • 2 пустые строки между функциями/классами верхнего уровня, 1 - между методами класса

Пример - тот же код до и после (ruff format + ruff check --fix сделают это за тебя автоматически):

# до
import os
def CalculateTotal(a,b , c=10):
result=a+b+c
if result>100:
return True
else:
return False
import sys
# после
import os
import sys


def calculate_total(a, b, c=10):
result = a + b + c
if result > 100:
return True
else:
return False

Разница: имя функции snake_case, а не PascalCase; один пробел вокруг ,/=/>, а не произвольное количество; отступ везде ровно 4 пробела, а не "на глаз"; импорты - оба вверху файла, а не один в середине кода; 2 пустые строки перед функцией верхнего уровня

Правил много, все не упомнишь - и не нужно: их проверяет ruff (см. ниже), а не твоя память
Понимать ЗАЧЕМ (консистентность, читаемость) важнее, чем держать в голове каждое конкретное число

uv - окружение и зависимости​

Виртуальное окружение - изолированная копия Python со своим набором пакетов, отдельная от системного Python и от окружений других проектов
Без изоляции: одному проекту нужен django==4.2, другому - django==5.0, и они не могут ужиться в одном общем (системном) окружении - конфликт версий
Поэтому у каждого проекта - своё окружение, обычно папка .venv внутри его же репозитория

uv - быстрый менеджер окружений и зависимостей (замена python -m venv + pip + poetry/pyenv разом)
Сам ставит нужную версию Python, создаёт .venv, фиксирует точные версии в uv.lock

  • uv init - новый проект (создаёт pyproject.toml)
  • uv add <пакет> - добавить зависимость (обновит pyproject.toml + uv.lock)
  • uv add --dev <пакет> - dev-зависимость (линтеры/тесты; в прод-образ не попадает)
  • uv sync - поставить окружение РОВНО по uv.lock
  • uv run <cmd> - выполнить команду в окружении проекта (uv run python …, uv run ruff …)

Версия Python фиксируется файлом .python-version (у нас 3.13) и полем requires-python в pyproject.toml
uv.lock коммитим - это и есть воспроизводимость (CI ставит uv sync --frozen)

ruff - линтер и форматтер​

ruff - один инструмент вместо связки flake8 + pylint + isort + black
Делает две разные вещи:

  • линт (ruff check) - находит ошибки и нарушения стиля, многое чинит сам (--fix)
  • формат (ruff format) - приводит код к единому виду (аналог black)
uv add --dev "ruff>=0.14,<0.15"
uv run ruff check . # линт
uv run ruff check --fix . # + авто-фиксы
uv run ruff format . # форматирование
uv run ruff check --watch . # следить за файлами и перепроверять на лету
Пин версии ruff

Держим ruff>=0.14,<0.15: в 0.15.x баг форматтера режет скобки в except-кортеже → получается невалидный Python
Снимем пин, когда апстрим починит

mypy - статическая типизация​

Аннотации типов (a: int, def f(x: str) -> bool:) не влияют на выполнение - Python их в рантайме игнорирует
Их проверяет mypy: находит несоответствия типов ДО запуска

uv add --dev mypy
uv run mypy .

Типизация в Python​

Аннотация - это «контракт» функции для других разработчиков и для IDE (автодополнение, подсветка ошибок)

  • Встроенные типы
    • int
    • float
    • bool
    • str
    • list
    • dict
    • set
    • tuple
    • None
  • Примеры
    • a: int = 5
    • names: list[str] = []
    • def add(a: int, b: int) -> int: ...
  • Модуль typing для сложных случаев
    • Optional[X] (= X | None)
    • Union (= X | Y)
from typing import Callable, Literal, Any

def apply(fn: Callable[[int, int], int], a: int, b: int) -> int:
return fn(a, b) # fn - функция с сигнатурой (int, int) -> int

def request(method: Literal['GET', 'POST']) -> None: ... # только эти строки, не любой str

def log(value: Any) -> None: # 'любой тип' - mypy не проверяет это место
print(value)

Generic - для своих обобщённых классов (свой list[T]); понадобится не на старте, отдельно разбирать здесь не будем

Docstrings​

Docstring - строка сразу под def/class или в начале модуля, описывающая, что делает объект
Google-style:

def transform(path: str, angle: int) -> bytes:
"""Повернуть изображение на заданный угол.

Args:
path: путь к файлу изображения.
angle: угол поворота в градусах.

Returns:
Байты повёрнутого изображения.
"""

Расширение autoDocstring генерирует каркас по сигнатуре
В нашем ruff-конфиге правило D (pydocstyle) не включено - docstrings это хороший тон, но не жёсткое требование грейдера

pyproject.toml - единый конфиг​

Весь тулинг настраивается в одном файле
Конфиг курса:

[tool.ruff]
line-length = 88
indent-width = 4
target-version = "py313"

[tool.ruff.lint]
fixable = ["ALL"]
select = ["E", "F", "FAST", "PL", "I", "ARG", "RET", "ASYNC", "B", "A", "ICN", "SLF", "SIM", "C90", "N", "W"]

[tool.ruff.lint.mccabe]
max-complexity = 10

[tool.ruff.format]
quote-style = "single"
indent-style = "space"

[tool.mypy]
plugins = ["pydantic.mypy"]

select - включённые семейства правил: E/W - PEP 8, F - ошибки, I - сортировка импортов, N - имена, B - багбир (типичные баги), PL - pylint, SIM - упрощения, C90 - цикломатическая сложность (см. ниже) и т.д.
quote-style = "single" - строковые литералы одинарными кавычками (наш стиль; docstrings ruff оставляет тройными двойными """)

Цикломатическая сложность (C90)​

Грубо говоря - число независимых путей выполнения через функцию: считается по числу ветвлений (if/elif/for/while/except/and/or) плюс один за саму функцию
Чем больше ветвлений в одной функции - тем больше сценариев нужно держать в голове и покрыть тестами
Поэтому на неё стоит порог (max-complexity) - ruff выдаёт C901 ... is too complex, если функция его превысила, и это сигнал разбить её на более мелкие, а не бороться с предупреждением

pre-commit - проверки до коммита​

pre-commit вешает проверки на git commit: код автоматически линтится и форматируется ПЕРЕД попаданием в историю - не забудешь прогнать ruff вручную

.pre-commit-config.yaml:

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14 # серия 0.14.x - совместимо с пином ruff<0.15
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
uvx pre-commit install # поставить хук в репозиторий (один раз)

IDE (VS Code)​

Расширения: Python, Pylance (автодополнение/подсветка), Ruff, Mypy Type Checker, autoDocstring
Тема - на вкус (напр. Dark+)
Тогда ruff/mypy работают прямо в редакторе: подсветка проблем и формат при сохранении / из контекстного меню

Примеры​

Поднять проект с нуля и настроить тулинг:

uv init myproject && cd myproject
uv add --dev "ruff>=0.14,<0.15" mypy
# вставить секции ruff/mypy в pyproject.toml (см. выше)
uv run ruff format .
uv run ruff check --fix .
uv run mypy .

Типизированная функция с docstring и строковым литералом:

def greet(name: str) -> str:
"""Собрать приветствие для пользователя."""
return f'Привет, {name}!' # строки - одинарные кавычки (quote-style = 'single')

Проверь себя​

1. Что такое PEP 8 и кто на практике следит за его соблюдением в коде?

PEP 8 - официальный style guide Python (отступы, именование, длина строки и т.д.)
На практике правила проверяет ruff - держать весь стандарт в памяти не нужно

2. Чем ruff check отличается от ruff format?

check - линтер (находит ошибки/нарушения, часть чинит с --fix); format - форматтер (единый вид кода)
Разные задачи, ruff умеет обе

3. Зачем коммитить uv.lock?

В нём зафиксированы точные версии всех зависимостей → у всех и в CI ставится ровно одно и то же окружение (воспроизводимость)
CI ставит uv sync --frozen строго по локу

4. Проверяет ли Python аннотации типов при запуске?

Нет
В рантайме аннотации игнорируются; их проверяет отдельный инструмент - mypy - ДО запуска

5. Что делает pre-commit?

Вешает проверки (линт/формат) на git commit - код проверяется автоматически перед попаданием в историю

6. Где настраивается ruff и mypy?

В pyproject.toml: секции [tool.ruff], [tool.ruff.lint], [tool.ruff.format], [tool.mypy]

7. Какой командой добавить dev-зависимость через uv?

uv add --dev <пакет> (напр. uv add --dev ruff mypy)

8. Что измеряет цикломатическая сложность (C90) и что делать при C901 ... is too complex?

Грубо - число независимых путей выполнения через функцию (ветвления if/for/while/except и т.д.)
Предупреждение значит, что функция взяла на себя слишком много веток - обычно её стоит разбить на более мелкие, а не подавлять предупреждение

Итог / чек-лист​

  • PEP 8 - стандарт стиля Python; правила проверяет ruff, а не память
  • Окружение и зависимости - только через uv (uv add / uv sync / uv run); Python пинится .python-version (3.13); uv.lock коммитим
  • ruff = линт + формат; mypy = типы; конфиг - в pyproject.toml (единый на курс)
  • Цикломатическая сложность (C90/C901, mccabe) - метрика ветвистости функции; предупреждение - сигнал разбить функцию, а не игнорировать
  • pre-commit гоняет ruff перед коммитом; тот же тулчейн - в CI-грейдере
  • Аннотации типов + docstrings - читаемость и контракт функций

Дополнительно​