О разделе

Python — второй по популярности язык backend-мира после JavaScript/PHP. Для PHP-разработчика это дешёвая инвестиция: половина концепций (ООП, ассоциативные массивы, интерполяция строк, декораторы, замыкания) уже знакомы, различия в основном в синтаксисе. Дальнейшие 20% — асинхронность, type hints и pythonic-стиль — усваиваются за пару недель практики.

Цель раздела: дать рабочее знание Python для backend-задач — не язык для алгоритмических задач, а практический инструмент: развернул FastAPI, подключился к БД, положил в Docker, задеплоил. Плюс ориентирование в экосистеме: pip vs poetry, Django vs FastAPI, requests vs httpx.
Карта раздела
БлокЧто внутри
ОсновыУстановка, синтаксис, типы, строки, условия, циклы, функции, comprehensions.
ПродвинутоООП + dunder, модули, venv/poetry, исключения, файлы, декораторы, type hints, async, генераторы.
Веб-фреймворкиDjango (батарейки в комплекте), FastAPI (современный async), Flask (микро), сравнение.
Данные и HTTPpsycopg2 / SQLAlchemy, requests / httpx, Pandas базово.
Инструментыpytest, logging, ruff/black/mypy.
Для PHP-разработчикаМосты Python ↔ PHP по типовым задачам, частые вопросы.
Где Python сегодня реально используется

ML / AI / Data Science

Абсолютный монополист. NumPy, Pandas, scikit-learn, PyTorch, TensorFlow, LangChain, Anthropic SDK. Если ты пишешь что-то с AI — почти наверняка это Python (или JavaScript-обёртка вокруг Python-модели на сервере).

Backend web / API

Django (Instagram, Pinterest, Reddit — начинали с него), FastAPI (Netflix, Uber — для новых сервисов), Flask (маленькие сервисы, прототипы). Для микросервисов и AI-бэков — FastAPI сейчас де-факто стандарт.

DevOps + автоматизация

Ansible, SaltStack, скрипты деплоя, обёртки над AWS/GCP CLI, парсинг логов, генерация конфигов. В любой команде DevOps — Python как «второй bash».

Data engineering + ETL

Airflow, dbt, Prefect. Загрузка данных из десятков источников в хранилище — почти всегда Python-пайплайны.

Скрипты, парсеры, крон-задачи

Beautiful Soup, Scrapy, requests. Небольшие «одноразовые» задачи — почти всегда Python, потому что быстрее написать, чем на любом другом языке.

Одной фразой: Python — это PHP без скобок и с отступами вместо {}, плюс лучшая экосистема для AI/Data и async-первый веб через FastAPI. Синтаксис читается как псевдокод. Порог входа для PHP-разработчика — 2 недели чтобы стать продуктивным.
Установка Python + версии + pip
Что ставить в 2026

Актуальная стабильная — Python 3.13 (вышла в 2024). Минимум для нового кода — 3.11 (значительный прирост скорости + улучшенные ошибки). Всё что 3.10 и старше — legacy. Python 2 мёртв с 2020 — не трогай, если только не поддерживаешь очень старый код.

macOS
# Через Homebrew
brew install python@3.13
python3 --version           # Python 3.13.x

# Или через pyenv — если нужно держать несколько версий
brew install pyenv
pyenv install 3.13.1
pyenv install 3.11.9
pyenv global 3.13.1         # default для системы
pyenv local 3.11.9          # для конкретного проекта (создаст .python-version)
Системный Python в macOS (/usr/bin/python3) не трогай — это OS-owned интерпретатор для скриптов Apple. Всегда ставь свой через brew / pyenv.
Linux (Ubuntu/Debian)
# Ubuntu 24.04 — уже есть Python 3.12 из коробки
python3 --version

# Если нужна другая версия — PPA от deadsnakes
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt update
sudo apt install python3.13 python3.13-venv python3.13-dev -y
pip — менеджер пакетов

pip — Python-аналог composer в PHP или npm в JS. Ставит библиотеки из PyPI (Python Package Index).

# Установить пакет
pip install requests

# Конкретная версия
pip install django==5.0.1

# Список установленных
pip list

# Заморозить зависимости → requirements.txt
pip freeze > requirements.txt

# Восстановить из requirements.txt
pip install -r requirements.txt

# Обновить сам pip
python -m pip install --upgrade pip
Не ставь пакеты в глобальный Python. Разные проекты — разные версии зависимостей. Ставь через venv (виртуальные окружения) — это как vendor/ в PHP, только на уровне интерпретатора. В Ubuntu 24.04 pip install в глобал вообще запрещён по умолчанию (externally-managed-environment).
Мгновенная альтернатива: uv

uv — новый (2024) менеджер от Astral (авторы ruff). Написан на Rust, в 10-100 раз быстрее pip. Заменяет pip + venv + pip-tools одной утилитой. Стандарт де-факто в новых проектах.

# Установка
curl -LsSf https://astral.sh/uv/install.sh | sh

# Создать проект
uv init my-app && cd my-app

# Добавить зависимость (сам создаст venv, обновит pyproject.toml)
uv add fastapi uvicorn

# Запустить
uv run python main.py
uv run uvicorn main:app --reload
Итог по установке: ставь Python через brew/pyenv (macOS) или apt/deadsnakes (Linux). Для проектов используй uv в новых, venv+pip в старых. Никогда не ставь пакеты в глобальный интерпретатор.
Синтаксис Python + отступы вместо скобок
Главное отличие от PHP: отступы вместо { }

В Python отступы — часть синтаксиса. Блок кода определяется одинаковым уровнем отступа. Классические { } отсутствуют. Стандарт: 4 пробела (не таб, никогда не смешивай).

# Python
if user.age >= 18:
    print("adult")
    send_email(user)
else:
    print("minor")

# Аналог в PHP
# if ($user->age >= 18) {
#     echo "adult";
#     send_email($user);
# } else {
#     echo "minor";
# }
Смешал tab и spaces — получишь TabError: inconsistent use of tabs and spaces. Настрой IDE, чтобы Tab превращался в 4 пробела. В PyCharm/VS Code с плагином Python — по умолчанию так.
Переменные — без объявления
# Просто присваиваем — тип выводится автоматически
name = "Alice"       # str
age = 30              # int
salary = 1500.50      # float
is_admin = True       # bool (с большой буквы!)
tags = ["php", "python"]      # list
user = {"name": "Alice", "age": 30}   # dict

# Constants — просто UPPER_CASE конвенция, языком не защищено
MAX_RETRIES = 3

# Type hints (опционально, для читаемости и mypy)
name: str = "Alice"
age: int = 30

Ключевые отличия от PHP:

  • Нет $ перед именем — просто name, не $name.
  • True/False/None — с большой буквы. В PHP было true/false/null.
  • Нет объявления типа обязательно — можно опционально через : (type hints).
  • Констант в языке нет — только конвенция UPPER_CASE. Реальную неизменяемость даёт только Final[int] из typing (проверяется mypy, но не runtime).
print() — вывод в консоль
print("Hello")                          # Hello
print("Hello", name)                    # Hello Alice (через пробел)
print("a", "b", "c", sep="-")          # a-b-c
print("no newline", end="")          # без переноса строки
print(f"Name: {name}, age: {age}")      # f-string — интерполяция
input() — чтение по одной строке

Один вызов input() читает ровно одну строку — до перевода строки — и возвращает её всегда как str, даже если там одни цифры. Поэтому для чисел результат сразу оборачивают в int(). Если на вход подано несколько строк, вызовы разбирают их по очереди: первый — первую, второй — вторую.

# на вход подано:
# 3
# 6200
# 2150

count  = int(input())   # 3
price1 = int(input())   # 6200
price2 = int(input())   # 2150

print(count, price1, price2)   # 3 6200 2150

В интерактивном запуске программа на каждом input() останавливается и ждёт ввода с клавиатуры. Когда ввод подаётся из файла или через конвейер, ожидания нет — строки просто разбираются подряд. Для кода разницы никакой: читается следующая непрочитанная строка.

Перевод строки input() убирает, пробелы оставляет. Строка " 42 " вернётся как ' 42 ' целиком. Для int() это неважно — он сам обрезает пробелы по краям, — а вот при сравнении строк пробелы выйдут боком, поэтому текстовый ввод обычно сразу пропускают через .strip().
Когда чисел несколько
# 1. количество задано первой строкой, дальше по числу в строке
n = int(input())
prices = [int(input()) for _ in range(n)]
print(sum(prices))

# 2. несколько чисел в одной строке через пробел: "6200 2150 11800"
a, b, c = map(int, input().split())
prices = list(map(int, input().split()))   # когда их неизвестно сколько

# 3. весь ввод разом, без оглядки на то, как он разбит на строки
import sys
nums = list(map(int, sys.stdin.read().split()))
Про split() без аргументов. Он режет по любому количеству пробелов и переводов строк сразу и выбрасывает пустые куски. Поэтому третий способ разбирает ввод одинаково, как бы он ни был разложен по строкам: 3 / 6200 2150 / 11800 даст тот же список, что и четыре отдельные строки.
Две ошибки, на которых падает такой код
Что произошлоИсключение
В строке не число: пустая строка, буквы, запятая вместо точкиValueError: invalid literal for int() with base 10: 'abc'
Вызовов input() больше, чем строк во вводеEOFError: EOF when reading a line
EOFError почти всегда означает рассогласование с форматом ввода. Лишний input(), неверно понятое количество строк, или первая строка прочитана дважды. Отлаживать это удобнее третьим способом: прочитать всё сразу в список и работать с индексами — тогда видно, сколько значений пришло на самом деле.
Дробные числа — через float(), не int(). int("3.5") завершится той же ValueError: функция не округляет текст, а требует целое. Нужно либо float(input()), либо int(float(input())), если дробную часть действительно надо отбросить.
Комментарии
# Однострочный (как // в PHP)

"""
Многострочный (docstring).
Стандарт для документации функций и классов.
"""

def divide(a, b):
    """Делит a на b. Бросает ZeroDivisionError если b == 0."""
    return a / b

Тройные кавычки — не «многострочный комментарий» (такого понятия нет), а обычная строка, которую можно не присваивать. Стандартное использование — как первая строка функции/класса — docstring. IDE и help(func) его показывают.

Операторы — что по-другому
PHPPythonЗаметка
. (конкатенация)+"a" + "b". Только для одинаковых типов — "a" + 1 упадёт.
===, !====, !=В Python нет loose comparison — обычные операторы уже строгие
&&, ||, !and, or, notБуквенные ключевые слова
/ (int div)//7 // 2 == 3. Обычный / всегда возвращает float: 7 / 2 == 3.5
** (power)**2 ** 10 == 1024
% (mod)%Так же
—in"a" in ["a", "b"] → True. Работает для list/dict/str/set
—isСравнение по идентичности (тот же объект). a is None — правильная проверка на None
Truthy / falsy

В Python «пустое» = False:

if not users:                # True если users — пустой list, dict, str, 0, None, False
    print("nothing to do")

# Явно проверить на None (правильный питонический способ)
if user is not None:
    ...
if x == None — работает, но не питонично. Правильно: if x is None. Причина: is сравнивает идентичность (быстрее и точнее для singletons), None — синглтон.
Первая программа
# hello.py
def main():
    name = input("Твоё имя: ")
    print(f"Привет, {name}!")

if __name__ == "__main__":
    main()
$ python hello.py
Твоё имя: Санжар
Привет, Санжар!

Конструкция if __name__ == "__main__": — питонический шаблон. Она выполняется, только когда файл запущен напрямую как скрипт, но не при import. Подробнее — в секции Модули + импорты.

Типы данных Python
7 базовых типов + их PHP-аналоги
PythonПримерPHP-аналог
int42int
float3.14float
str"hello"string
boolTrue / Falsebool
NoneNonenull
list[1, 2, 3]Индексированный array
tuple(1, 2, 3)Неизменяемый array (нет прямого аналога)
dict{"a": 1, "b": 2}Ассоциативный array
set{1, 2, 3}Нет прямого — используют array_unique
list — упорядоченный, изменяемый
users = ["alice", "bob", "charlie"]

# Доступ по индексу
users[0]                          # "alice"
users[-1]                         # "charlie" (с конца)

# Slicing — срезы
users[1:3]                        # ["bob", "charlie"]
users[:2]                         # ["alice", "bob"]
users[::2]                        # каждый второй → ["alice", "charlie"]
users[::-1]                       # reverse

# Модификация
users.append("dave")              # в конец
users.insert(0, "zoe")           # в начало
users.remove("bob")              # удалить по значению
del users[0]                     # удалить по индексу
users.pop()                        # удалить и вернуть последний

# Проверка
"alice" in users                 # True
len(users)                       # 3

# Сортировка
users.sort()                       # in-place, вернёт None
sorted(users)                    # возвращает НОВЫЙ отсортированный, не меняет
sorted(users, reverse=True)
tuple — упорядоченный, неизменяемый
coords = (55.75, 37.61)
x, y = coords                     # unpacking — очень частый паттерн

# Неизменяемый:
coords[0] = 99                    # TypeError

# Функции часто возвращают tuple — сразу распаковываем:
name, age = get_user()

# Именованные — namedtuple (легковесная замена классу)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(p.x, p.y)                   # 1 2

Зачем tuple, если есть list: tuple немного быстрее, может быть ключом словаря (list — нельзя), и семантически говорит «эти данные вместе, порядок фиксирован» (координаты, RGB-цвет, дата разбитая на y/m/d).

dict — ассоциативный массив (hash map)
user = {
    "name": "Alice",
    "age": 30,
    "roles": ["admin", "editor"],
}

# Доступ
user["name"]                      # "Alice"
user["phone"]                     # KeyError!
user.get("phone")                # None — безопасно
user.get("phone", "unknown")     # дефолт если нет

# Модификация
user["email"] = "a@b.c"          # добавили
del user["age"]                 # удалили

# Обход
for key in user:                  # обход ключей
    print(key, user[key])
for key, value in user.items():  # pythonic — пара сразу
    print(key, value)
for value in user.values():
    print(value)

# Слияние — Python 3.9+
merged = defaults | overrides     # справа побеждает

# Проверка ключа
"name" in user                  # True — работает по ключам
set — множество без дубликатов
tags = {"php", "python", "js", "php"}
print(tags)                       # {"php", "python", "js"} — дубликат ушёл

# Операции над множествами (математические!)
a = {1, 2, 3}
b = {2, 3, 4}
a | b                             # объединение → {1, 2, 3, 4}
a & b                             # пересечение → {2, 3}
a - b                             # разность → {1}
a ^ b                             # симметрическая разность → {1, 4}

# Убрать дубликаты из списка
unique_users = list(set(users))
Изменяемость (mutable vs immutable)
ТипИзменяемый?
int, float, str, bool, None, tuple, frozensetImmutable
list, dict, setMutable

Immutable-объекты нельзя менять «на месте» — методы возвращают новую копию. У str: s.upper() вернёт новую строку, оригинал не тронется. Это как в PHP: str — value type.

Классический баг: mutable default argument.
def add_item(item, items=[]):    # ← []  создаётся ОДИН раз при определении функции!
    items.append(item)
    return items

add_item("a")   # ["a"]
add_item("b")   # ["a", "b"] !!! копится между вызовами
Правильно: items=None и внутри if items is None: items = [].
Python vs PHP — мосты для перехода
Основные соответствия
PHPPythonКомментарий
<?phpНичегоPython-файл — сразу код, без открывающего тега
$namenameБез $
echo, printprint()Функция, всегда со скобками
{ ... }Отступы (4 пробела)Отступ — часть синтаксиса
; в конце—Не нужен
true/false/nullTrue/False/NoneС большой буквы!
&&, ||, !and, or, notБуквенные
. (concat)+Только для одинаковых типов
foreach ($arr as $v)for v in arr:Прямой обход значений
foreach ($arr as $k => $v)for k, v in arr.items():Для dict — метод .items()
functiondef
use, namespaceimport, from ... import
class Foo extends Barclass Foo(Bar):Скобки вместо extends
$this->nameself.nameself — первый аргумент метода явно
__construct__init__
const FOO = 1FOO = 1 (конвенция)Реальных констант нет
?string $namename: str | NoneType hints
array_maplist comprehension / map()[x*2 for x in arr]
array_filterlist comprehension[x for x in arr if x > 0]
compact('user'){"user": user}Аналога нет — dict вручную
json_encode, json_decodejson.dumps, json.loadsИз stdlib import json
composerpip / uv / poetry
vendor/.venv/ или site-packages/
composer.jsonrequirements.txt / pyproject.toml
artisanmanage.py (Django) / скрипты
LaravelDjango (batteries included) / FastAPI (микро+async)
Что серьёзно отличается

1. Всё — объект, всё передаётся по ссылке на объект

В PHP массив передаётся по значению (копируется). В Python list и dict — mutable объекты, передаются как ссылки. Функция может изменить исходный список. Похоже на объекты в PHP, но касается вообще всех collections.

2. Асинхронность — первого класса

В PHP async/await — экзотика (Swoole, ReactPHP). В Python — стандарт стандартной библиотеки: asyncio, aiohttp, FastAPI полностью на нём построен. Изучить сразу, если планируешь микросервисы.

3. Type hints — опциональные, но важные

В PHP 7.4+ типы обязательны для сериозного кода. В Python — опциональны, работают через отдельный tool (mypy), рантайм их игнорирует. Но в новом коде почти всегда пишут — читаемость и IDE-подсказки в разы лучше.

4. Duck typing и «easier to ask forgiveness than permission»

В PHP чаще проверяют «есть ли метод» через method_exists. В Python — идиома «просто вызови, и обработай исключение». try: obj.method(); except AttributeError: .... Это питонично.

5. Один способ сделать вещь

Zen of Python: «There should be one — and preferably only one — obvious way to do it». В PHP часто 3-4 способа (array_map / foreach / array_walk). В Python — почти всегда один канонический (list comprehension), остальные — «можно, но обычно не нужно».

6. Многопоточность и GIL

У Python есть Global Interpreter Lock (GIL) — только один поток выполняет Python-код в один момент. Для CPU-bound нагрузки → multiprocessing (отдельные процессы). Для I/O-bound → asyncio. С Python 3.13+ GIL постепенно ослабляется, но пока правило работает.

Стратегия перехода PHP→Python: первую неделю — синтаксис, list/dict/comprehensions. Вторую — ООП, модули, venv. Третью — сделать что-то полезное на FastAPI + PostgreSQL. Дальше уже глубина: async, type hints, тестирование. За месяц становишься продуктивным.
Строки + f-strings + regex
Литералы строк
a = "hello"              # двойные
b = 'hello'              # одинарные — эквивалентны
c = """multi
line string"""          # тройные — многострочная
d = r"C:\path\file"     # raw — не экранирует \n \t \
e = b"bytes"             # bytes (не str)
f = f"Hi {name}"         # f-string — интерполяция
f-strings — стандарт (Python 3.6+)
name = "Alice"
age = 30
price = 1234.5678

f"Hi, {name}!"                       # "Hi, Alice!"
f"{name} is {age} years old"          # подстановка нескольких
f"{name.upper()}"                    # выражения — не только имена
f"total: {a + b}"                    # арифметика внутри

# Форматирование
f"{price:.2f}"                       # "1234.57" — округление до 2
f"{price:,.2f}"                      # "1,234.57" — с разделителем
f"{price:10.2f}"                     # "   1234.57" — ширина 10, padding
f"{price:>10}"                       # выравнивание вправо
f"{price:<10}"                       # влево
f"{price:^10}"                       # по центру
f"{age:03d}"                         # "030" — паддинг нулями
f"{price:.0%}"                       # проценты
f"{now:%Y-%m-%d %H:%M}"              # дата (datetime)

# Debug (Python 3.8+) — печатает имя + значение
f"{name=}"                            # "name='Alice'"
f"{price=:.2f}"                      # "price=1234.57"

# repr vs str внутри f-string
f"{name!r}"                          # "'Alice'" — через repr()
f"{name!s}"                          # "Alice"
Правило: в новом коде — только f-strings. .format() и %-форматирование — устаревшее, встречается только в legacy. f-strings быстрее (в 2-3 раза), читаемее, поддерживают все выражения.
Четыре способа подставить значение — и чем они отличаются

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

text = 'привет'

# 1. f-строка — основной способ
print(f"Он зашёл в чат и написал: {text}!")
# Он зашёл в чат и написал: привет!

# 2. метод .format()
print("Скажи мне {} ещё раз.".format(text))
# Скажи мне привет ещё раз.

# 3. сложение через +
print("Она ответила: " + text)
# Она ответила: привет

# 4. перечисление через запятую в print()
print("Новое сообщение:", text, "всем")
# Новое сообщение: привет всем
СпособЧто получаетсяКогда уместен
f"..."strВсегда, если текст известен в момент написания кода
.format()strКогда шаблон приходит извне: из настроек, из файла переводов, из базы
+strДве-три части, и все уже строки. Для длинной сборки нечитаемо
print(a, b)NoneТолько для вывода на экран. Строки на выходе нет
Четвёртый способ стоит особняком. Это не сборка строки, а несколько аргументов функции: print сам печатает их подряд, вставляя пробел между соседними. Результата в переменную не положить — print возвращает None. Разделитель меняется параметром: print("a", "b", sep=" | ") напечатает a | b, а end="" уберёт перевод строки в конце.
Сложение требует, чтобы обе части были строками. Выражение "сообщений: " + 5 завершится ошибкой TypeError: can only concatenate str (not "int") to str — неявного приведения, как в PHP, в Python нет. Пришлось бы писать str(5), и это ещё одна причина предпочитать f-строку: внутри {} преобразование выполняется само.
Почему .format() не исчез совсем. f-строка вычисляется там, где написана, и шаблоном быть не может: её нельзя сохранить в настройках и подставить значения позже. А "Привет, {name}!" — обычная строка, её можно хранить где угодно и применить к данным в нужный момент: template.format(name=user.name). Для переводимых сообщений берут именно этот способ.
В логах — ни то, ни другое. Там пишут logger.info("user %s вошёл", name) с подстановкой через %s и отдельными аргументами: сообщение собирается, только если уровень логирования включён. f-строка собралась бы всегда, даже когда запись будет отброшена. Подробнее — в разделе Логирование.
Когда частей много, ни один из четырёх способов не подходит — берут "".join(parts) или ", ".join(names): склейка списка одним вызовом вместо цепочки плюсов.
Основные методы str
МетодЧто делаетПример
.upper(), .lower(), .title(), .capitalize()Регистр"hello".upper() → "HELLO"
.strip(), .lstrip(), .rstrip()Убрать whitespace (или указанные символы)" hi ".strip() → "hi"
.split(sep)Разбить на list по разделителю"a,b,c".split(",") → ["a","b","c"]
sep.join(iterable)Собрать iterable в строку",".join(["a","b"]) → "a,b"
.replace(old, new)Заменить подстроку"a-b".replace("-", "_")
.startswith(x), .endswith(x)Проверка префикса/суффикса"file.jpg".endswith(".jpg")
.find(x), .index(x)Позиция первого вхождения (find → -1, index → ValueError)"hello".find("l") → 2
x in stringПроверка вхождения"lo" in "hello" → True
.count(x)Число вхождений"aaa".count("a") → 3
.isdigit(), .isalpha(), .isalnum()Проверка типа"123".isdigit() → True
.zfill(n), .center(n), .ljust(n), .rjust(n)Паддинг"5".zfill(3) → "005"
.splitlines()Разбить по \n / \r\nКросс-платформенно
.encode(enc)str → bytes"hi".encode("utf-8")

Все методы возвращают НОВУЮ строку — str immutable. s.upper() не меняет s, а возвращает новую.

Slicing (срезы)
s = "hello world"

s[0]                    # "h"
s[-1]                   # "d"
s[0:5]                  # "hello"
s[:5]                   # "hello" — с начала
s[6:]                   # "world" — до конца
s[::-1]                 # "dlrow olleh" — reverse
s[::2]                  # "hlowrd" — каждый второй
len(s)                 # 11
Regex — модуль re
import re

# Основные функции
re.search(pattern, string)     # найти первое → Match | None
re.match(pattern, string)      # только с НАЧАЛА строки
re.fullmatch(pattern, string)  # вся строка целиком
re.findall(pattern, string)    # все совпадения → list
re.finditer(pattern, string)   # все → iterator Match'ей (для больших текстов)
re.sub(pattern, repl, string)  # замена
re.split(pattern, string)     # split по regex

# Пример: проверить email
if re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.-]+", email):
    print("looks like email")

# Захват групп
m = re.search(r"(\d+)-(\d+)", "order 42-100")
if m:
    print(m.group(0))         # "42-100" — вся сматченная строка
    print(m.group(1))         # "42"
    print(m.group(2))         # "100"
    print(m.groups())         # ("42", "100")

# Именованные группы
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-09")
m.group("year")              # "2026"

# Компиляция для повторного использования (быстрее)
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
EMAIL_RE.fullmatch(email)
Правило: паттерны — всегда raw strings (r"..."), иначе \d, \s, \n будут интерпретироваться Python-ом до regex-движка. Флаг re.IGNORECASE для case-insensitive, re.MULTILINE — ^/$ на каждой строке.
str vs bytes — кодировки

В Python 3 str — это Unicode (последовательность code point'ов). bytes — сырые байты. Между ними — явная конвертация через кодировку.

s = "Привет"                    # str (unicode)
b = s.encode("utf-8")             # bytes: b'\xd0\x9f\xd1\x80\xd0\xb8...'
s2 = b.decode("utf-8")            # обратно в str

# Файлы
with open("file.txt", encoding="utf-8") as f:  # явно всегда utf-8
    text = f.read()

# Бинарные — bytes
with open("image.png", "rb") as f:
    data = f.read()             # bytes
Никогда не полагайся на дефолтный encoding. open("f.txt") без явного encoding="utf-8" — на Windows возьмёт cp1251, на Linux — utf-8. Один и тот же код будет ломаться на разных ОС.
Особые случаи
1. Конкатенация + в цикле — медленно. Каждый + создаёт новую строку. Для N-строк — "".join(list_of_strings), работает за O(n) вместо O(n²).
2. .replace() не regex. Убрать все пробелы — s.replace(" ", ""); но убрать любые whitespace — re.sub(r"\s+", "", s).
3. Split пустой строки. "".split(",") → [""], не []. Проверяй if s перед split.
4. str.format() с dict — точки не работают. "{d.name}".format(d=obj) — обращение по атрибуту, не по ключу dict. Используй f-string.
5. Regex без raw string. "\d+" в Python 3.12+ выдаёт DeprecationWarning. Всегда r"\d+".
Условия + циклы + match/case
if / elif / else + тернарник
if age >= 18:
    status = "adult"
elif age >= 13:
    status = "teen"
else:
    status = "child"

# Тернарник: value_if_true if condition else value_if_false
status = "adult" if age >= 18 else "minor"

# Не путать с C-style — синтаксис Python другой:
# status = age >= 18 ? "adult" : "minor"   — SyntaxError

# Chained comparisons — красивая питоническая фича
if 18 <= age < 65:                # эквивалент age >= 18 and age < 65
    ...

# Walrus operator := (Python 3.8+) — присвоить внутри выражения
if (n := len(items)) > 10:
    print(f"too many: {n}")
Циклы for и while
# for — обход iterable (list/tuple/dict/str/set/generator)
for user in users:
    print(user)

# range(start, stop, step)
for i in range(10):           # 0..9
    ...
for i in range(1, 11):        # 1..10
    ...
for i in range(0, 100, 10):    # 0, 10, 20, ..., 90
    ...

# while — пока условие true
while queue:
    task = queue.pop()
    process(task)

# break / continue — как везде
for item in items:
    if item.is_bad:
        continue
    if item.is_stop:
        break
    process(item)
Питонические паттерны обхода
# enumerate — индекс + значение
for i, user in enumerate(users):
    print(f"{i}: {user}")

for i, user in enumerate(users, start=1):    # с 1
    ...

# zip — параллельный обход двух iterables
names = ["Alice", "Bob"]
ages  = [30, 25]
for name, age in zip(names, ages):
    print(name, age)

# dict — .items(), .keys(), .values()
for key, value in user.items():
    ...

# reversed — обход в обратном порядке
for item in reversed(items):
    ...

# sorted — обход отсортированного
for user in sorted(users, key=lambda u: u.age):
    ...
Не используй for i in range(len(items)) — это анти-паттерн. Питонично:
  • нужно только значение → for item in items
  • нужен индекс + значение → for i, item in enumerate(items)
  • обход двух списков — for a, b in zip(list1, list2)
else в цикле — редкая, но живая конструкция
# else срабатывает если цикл завершился БЕЗ break
for item in items:
    if item.is_target:
        print("found!")
        break
else:
    print("not found")   # сработает только если target не встретился

Кто это придумал — вопрос философский. Практика — заменяют flag-переменной для читаемости.

match / case — pattern matching (Python 3.10+)

Мощная замена switch — не просто сравнение по значению, а деструктуризация:

# Простой — как switch
match status:
    case "active":
        handle_active()
    case "paused" | "stopped":      # несколько значений
        handle_off()
    case _:                          # default
        handle_unknown()

# Деструктуризация — вот где magic
match event:
    case {"type": "order.created", "data": {"id": order_id}}:
        process_order(order_id)
    case {"type": "payment.received", "data": {"amount": amount, "currency": cur}}:
        record_payment(amount, cur)
    case {"type": "error", "message": msg}:
        logger.error(msg)

# Классы + деструктуризация
match shape:
    case Circle(radius=r):
        return 3.14 * r ** 2
    case Rectangle(width=w, height=h):
        return w * h

# Guard — доп условие
match point:
    case (x, y) if x == y:
        print("on diagonal")
Особые случаи
1. Модификация коллекции во время обхода. for x in list: list.remove(x) — пропустит элементы. Обходи копию (list[:]) или собери в новый.
2. range() — не список. В Python 3 это ленивый iterator. Хочешь список — list(range(10)). Для миллионных диапазонов важно.
3. zip() обрезает до самого короткого. Для строгой проверки — zip(a, b, strict=True) (Python 3.10+), кинет ValueError.
4. match ≠ switch. case <VAR>: без точки — это захват (bind), а не сравнение. case value: просто присвоит что угодно в value. Для сравнения с константой класса — case Color.RED:.
Функции + lambda + замыкания
Определение — def
def greet(name):
    """Docstring — первая строка, доступна через help(greet)."""
    return f"Hello, {name}!"

greet("Alice")              # "Hello, Alice!"

# С type hints (рекомендуется в новом коде)
def greet(name: str) -> str:
    return f"Hello, {name}!"

# Без явного return — функция вернёт None
def log_it(msg):
    print(msg)         # возвращает None автоматически
Аргументы — позиционные vs keyword
def create_user(name, age, role="user", active=True):
    return {"name": name, "age": age, "role": role, "active": active}

# Позиционные — по порядку
create_user("Alice", 30)

# Keyword — по имени, порядок не важен
create_user(age=30, name="Alice", role="admin")

# Смешанно — сначала позиционные, потом keyword
create_user("Alice", 30, role="admin")

# Keyword-only — принудительно через *
def create_order(items, *, currency="USD", express=False):
    ...
create_order([...], currency="EUR")   #
create_order([...], "EUR")             # TypeError

# Positional-only — до /
def divmod_(a, b, /):     # a и b нельзя передать как keyword
    return a // b, a % b
*args и **kwargs — «собирают» и «раскрывают»
Ключевая идея: * и ** — одна операция в две стороны:
  • В def — СОБИРАЮТ много аргументов в один (много → одно)
  • При вызове — РАСКРЫВАЮТ одну коллекцию во много аргументов (одно → много)
Один * — работает с позиционными. Два ** — с именованными.

Сторона 1: в def — собирают

# *args — собрать все позиционные в кортеж
def sum_all(*args):
    return sum(args)

sum_all(1, 2, 3, 4)
# sum_all(1, 2, 3, 4)
#         └──┬───────┘
#       *args собирает всё это
#            ↓
#        args = (1, 2, 3, 4)   ← кортеж

# **kwargs — собрать все именованные в словарь
def make_dict(**kwargs):
    return kwargs

make_dict(name="Alice", age=30)
# kwargs = {"name": "Alice", "age": 30}   ← словарь

# Комбинация — обычная / позиционные / именованные
def log(level, *messages, **extras):
    print(level, messages, extras)

log("INFO", "user", "created", user_id=42, ip="1.1.1.1")
#     ↓         └────┬─────┘   └──────┬───────────┘
#   level      *messages           **extras
#              ("user","created")  {"user_id":42,"ip":"1.1.1.1"}

Сторона 2: при вызове — раскрывают (обратная операция)

# * при вызове — раскрыть список в позиционные
def add(a, b, c):
    return a + b + c

nums = [1, 2, 3]
add(nums)                # TypeError — передался ОДИН список
add(*nums)               # = add(1, 2, 3) → 6

# ** при вызове — раскрыть словарь в именованные
def create_user(name, age): ...

kwargs = {"name": "Alice", "age": 30}
create_user(**kwargs)     # = create_user(name="Alice", age=30)

Одна таблица — обе стороны:

МестоСимволЧто делаетНаправление
в def*argsСОБИРАЕТ позиционные → кортежмного → один
в def**kwargsСОБИРАЕТ именованные → словарьмного → один
при вызовеf(*list)РАСКРЫВАЕТ список → позиционныеодин → много
при вызовеf(**dict)РАСКРЫВАЕТ словарь → именованныеодин → много

Мнемоника: в def — «звёздочка собирает» (много в одно). При вызове — «звёздочка раскрывает» (одно в много). Это обратные операции одного механизма.

Полный цикл — собери и раскрой
def f(*args, **kwargs):
    print("args   =", args, type(args).__name__)
    print("kwargs =", kwargs, type(kwargs).__name__)

f(1, 2, x=10, y=20)
# args   = (1, 2) tuple                     ← позиционные → КОРТЕЖ
# kwargs = {'x': 10, 'y': 20} dict          ← именованные → СЛОВАРЬ

# Раскрыть их и передать в другую функцию
def g(a, b, x, y): return a + b + x + y

args = [1, 2]
kwargs = {"x": 10, "y": 20}
g(*args, **kwargs)                        # = g(1, 2, x=10, y=20) → 33
Разбор строки print("args =", args, type(args).__name__)

Три аргумента через запятую — print напечатает их через пробел:

  1. "args =" — обычная строка-подпись
  2. args — переменная (кортеж, собранный *args)
  3. type(args).__name__ — цепочка:
    • type(args) → <class 'tuple'> (объект-класс)
    • .__name__ → 'tuple' (имя класса как строка, без обёртки <class ...>)

Итого — «напечатай подпись, значение и красивое имя типа». Часто используется в отладке.

type(42).__name__       # 'int'
type("hi").__name__     # 'str'
type([1, 2]).__name__   # 'list'
type({"a": 1}).__name__ # 'dict'
type(None).__name__     # 'NoneType'
type(True).__name__     # 'bool'

В f-string короче и читаемее:

print(f"args = {args} ({type(args).__name__})")
# args = (1, 2) (tuple)
Главное применение — декораторы

Декоратор не знает, какие аргументы принимает оборачиваемая функция. Ловит «всё» через *args, **kwargs и прокидывает дальше:

def logged(func):
    def wrapper(*args, **kwargs):     # СОБИРАЕТ что угодно
        print("вызов:", func.__name__)
        return func(*args, **kwargs)     # РАСКРЫВАЕТ и передаёт
    return wrapper

@logged
def add(a, b): return a + b

add(1, 2)
# вызов: add
# 3
Ловушки
1. Порядок в def строго фиксирован: def f(обычные, *args, keyword_only=X, **kwargs). Обратный порядок — SyntaxError.
2. Имена args/kwargs — просто конвенция. Работает и def f(*nums, **opts). Важны звёздочки, не имена.
3. * для словаря раскроет КЛЮЧИ:
d = {"a": 1, "b": 2}
f(*d)      # = f("a", "b") — раскрылись ключи, не значения!
f(**d)     # = f(a=1, b=2) — вот так правильно
4. args — кортеж (неизменяемый). args[0] = 99 → TypeError. Хочешь менять — list(args).
5. Не путать * в def и * при вызове. Один и тот же символ, но противоположные операции. Смотри по позиции: def f(*args) — собирает; f(*[1,2,3]) — раскрывает.
Mutable default argument — классический баг.
def add(item, items=[]):    # [] создаётся ОДИН раз при определении!
    items.append(item)
    return items

add("a")     # ["a"]
add("b")     # ["a", "b"] — не пустой!

# Правильно:
def add(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
Возврат нескольких значений
def divmod_(a, b):
    return a // b, a % b       # на самом деле — tuple (a//b, a%b)

quotient, remainder = divmod_(10, 3)   # unpacking

# Часто возвращают dict / dataclass для читаемости
def get_user_stats(user_id):
    return {"orders": 42, "total": 1500.0, "last_order_at": ...}
lambda — анонимные функции

Однострочные функции без имени. Полезны для sorted, map, filter. Не для сложной логики — там обычная def.

# Сортировка по полю
users = [{"name": "Bob", "age": 25}, {"name": "Alice", "age": 30}]
sorted(users, key=lambda u: u["age"])

# Максимум по критерию
max(users, key=lambda u: u["age"])

# Фильтр
list(filter(lambda u: u["age"] >= 18, users))
# Питоничнее — comprehension:
[u for u in users if u["age"] >= 18]
Правило: lambda хорош для одноразовой функции-выражения. Если логика в 2+ строки — делай def. Читаемость важнее краткости.
Замыкания (closures)

Внутренняя функция запоминает переменные внешней. Классический паттерн — фабрика функций:

def make_multiplier(factor):
    def multiply(x):
        return x * factor       # factor — из внешней области
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
double(5)                        # 10
triple(5)                        # 15

# Изменение внешней переменной — через nonlocal
def counter():
    count = 0
    def inc():
        nonlocal count       # без этого count = local, будет UnboundLocalError
        count += 1
        return count
    return inc

next_id = counter()
next_id(), next_id(), next_id()   # 1, 2, 3
Late binding в закрытии по циклу — классическая ловушка.
funcs = [lambda: i for i in range(3)]
[f() for f in funcs]           # [2, 2, 2] — не [0, 1, 2]!
# Все lambda ссылаются на ОДНУ и ту же i, к моменту вызова = 2

# Фикс через default:
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs]           # [0, 1, 2]
Особые случаи
1. Функция первого класса. Функцию можно присвоить переменной, положить в list/dict, передать как аргумент, вернуть из другой функции. Это фундамент декораторов и функционального стиля.
2. Scope — LEGB. Local → Enclosing (closure) → Global → Built-in. Имя ищется в этом порядке. global x и nonlocal x — модификаторы «пиши в верхний scope».
3. Docstring — стандарт индустрии. Первая строка функции в тройных кавычках. IDE, help(), документация (Sphinx) используют её. Пиши всегда для публичных функций.
4. Return типов — рекомендуется явно. -> None для void, -> list[str] для типизированной коллекции. mypy проверит.
5. Не мутируй входные аргументы без предупреждения. def append_id(items, id): items.append(id) — сюрприз для вызывающего. Возвращай новый список либо явно назови *_inplace.
Итог: def — стандарт, lambda — для одноразовых key-функций. *args/**kwargs для гибких API. Никогда mutable default arg — только None + проверка. Closures работают, но осторожно с late binding.
Comprehensions — list / dict / set / generator
List comprehension — питоничная замена map/filter
# [expression for item in iterable if condition]

# Простое преобразование
squared = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# С фильтром
positives = [x for x in nums if x > 0]

# Оба сразу
even_squares = [x ** 2 for x in range(20) if x % 2 == 0]

# Извлечение из объектов
emails = [user.email for user in users if user.is_active]

# if/else в expression — тернарник (тогда if НЕ фильтр, а часть выражения)
labels = ["adult" if u.age >= 18 else "minor" for u in users]
Правило: comprehension короче, быстрее и питоничнее чем map()/filter(). В новом коде — только comprehensions. map/filter остались в лямбда-мире функционального программирования.
Dict comprehension
# {key_expr: value_expr for item in iterable if condition}

# Из пары списков
names = ["Alice", "Bob", "Charlie"]
ages  = [30, 25, 35]
user_ages = {n: a for n, a in zip(names, ages)}
# {"Alice": 30, "Bob": 25, "Charlie": 35}

# Инверсия dict
inverted = {v: k for k, v in d.items()}

# Трансформация значений
upper_d = {k: v.upper() for k, v in d.items()}

# Индекс по id
users_by_id = {u.id: u for u in users}

# С фильтром
active = {k: v for k, v in d.items() if v.is_active}
Set comprehension
unique_tags = {tag for post in posts for tag in post.tags}

# Уникальные домены email-ов
domains = {email.split("@")[1] for email in emails}
Generator expression — ленивая версия

Просто скобки (...) вместо [...] — но получается генератор, а не список. Не занимает память под все элементы сразу, вычисляет по одному.

# List — сразу все 10^9 чисел в памяти. Убьёт RAM.
sum([x ** 2 for x in range(10 ** 9)])

# Generator — по одному, память O(1)
sum(x ** 2 for x in range(10 ** 9))

# При передаче в функцию скобки comprehension'а можно опустить
any(u.is_admin for u in users)
max(o.total for o in orders)
Правило: результат нужен как список (обход несколько раз, индексация, len) → list comprehension. Однократный обход + большая последовательность → generator expression.
Как читать comprehension с двумя for
Главное правило: читай comprehension слева направо, как обычные вложенные циклы. for идут в том же порядке, что и в обычном коде: первый for — внешний, второй — внутренний. Никакой магии — просто одна строка вместо трёх.

Универсальная формула перевода:

# Comprehension
[expr for A in X for B in Y if cond]

# Эквивалент через обычный код
result = []
for A in X:              # ← первый for (внешний)
    for B in Y:          # ← второй for (внутренний)
        if cond:
            result.append(expr)
Разбор: уплощение матрицы
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Разворот в обычный цикл:
flat = []
for row in matrix:        # row = [1,2,3], потом [4,5,6], потом [7,8,9]
    for x in row:         # x = 1, 2, 3   потом 4, 5, 6   потом 7, 8, 9
        flat.append(x)

«Прочитай вслух» — [x for row in matrix for x in row] → «возьми x — для каждой row в matrix — для каждого x в row». Порядок for = порядок вложенности.

Разбор: уникальные теги из постов
unique_tags = {tag for post in posts for tag in post.tags}

# Развёрнуто:
unique_tags = set()
for post in posts:            # каждый пост
    for tag in post.tags:     # каждый его тег
        unique_tags.add(tag)   # set сам убирает дубли

Второй for использует post, объявленный в первом → порядок нельзя менять. post.tags — коллекция внутри объекта.

Разбор: домены email-ов (один for)
domains = {email.split("@")[1] for email in emails}

# Развёрнуто:
domains = set()
for email in emails:
    domains.add(email.split("@")[1])

Один for — потому что вход плоский (список строк). В прошлых примерах был двумерный (список списков / список объектов с вложенными коллекциями) — оттуда два for.

Комбинации — независимые циклы (декартово произведение)
pairs = [(a, b) for a in range(3) for b in range(3) if a != b]

# Развёрнуто:
pairs = []
for a in range(3):        # a = 0, 1, 2
    for b in range(3):    # b = 0, 1, 2 (для КАЖДОГО a)
        if a != b:
            pairs.append((a, b))

# Результат:
# a=0: (0,1) (0,2)
# a=1: (1,0) (1,2)
# a=2: (2,0) (2,1)
# → 6 пар

Разница с уплощением:

ТипПримерВторой for
Уплощение (зависимые)[x for row in matrix for x in row]зависит от первого (row оттуда)
Комбинации (независимые)[(a,b) for a in range(3) for b in range(3)]независим — свой итератор
Визуально: где какой цикл
[x for row in matrix for x in row] ↑ ↑ ↑ что ОТКУДА берём ОТКУДА берём собираем первый уровень второй уровень (зависит от row)
Как быстро разобрать любой comprehension
  1. Найди первый for — это внешний цикл.
  2. Найди второй for — внутренний (вложен в первый).
  3. Всё, что стоит в начале (до первого for) — что собираем.
  4. if в конце — фильтр (можно несколько).
  5. if после for применяется к последнему циклу.
[ ВЫРАЖЕНИЕ for A in X for B in Y if УСЛОВИЕ ]
#     ↑         ↑          ↑          ↑
#  собираем  внешний   внутренний  фильтр
Частые ошибки
1. Путать порядок for:
[x for row in matrix for x in row]     #
[x for x in row for row in matrix]     # NameError: row не объявлен
Первый for должен объявить переменную, которую использует второй.
2. Путать if в начале и в конце:
[x if x > 0 else 0 for x in nums]     # ← тернарник (преобразование каждого)
[x for x in nums if x > 0]            # ← фильтр (отбор)
if после for — фильтр. if...else перед for — часть выражения.
3. Три уровня вложенности — уже нечитабельно. [x for a in A for b in a for c in b for d in c] — разверни в циклы или вспомогательные функции.
Практические примеры
# Все слова из списка предложений (плоский)
[word for sentence in text for word in sentence.split()]

# Все числа из вложенного списка
[n for group in groups for n in group]

# Все теги из постов (уникальные)
{tag for post in posts for tag in post.tags}

# Все комбинации цвет × размер
[(c, s) for c in colors for s in sizes]

# Пары без повторов (i < j)
[(a, b) for a in range(5) for b in range(5) if a < b]

# Все ключи из списка словарей
[k for d in dicts for k in d.keys()]

# Транспонировать матрицу
[[row[i] for row in matrix] for i in range(len(matrix[0]))]
Итог: два for = вложенные циклы, записанные в одну строку. Порядок for — как в обычном коде (первый внешний, второй внутренний). Второй for может зависеть от первого (уплощение) или быть независимым (комбинации). Если больше двух уровней — пиши обычными циклами, не мучай читателя.
PHP-аналоги
PHPPython
array_map(fn($x) => $x * 2, $nums)[x * 2 for x in nums]
array_filter($nums, fn($x) => $x > 0)[x for x in nums if x > 0]
array_combine($keys, $values){k: v for k, v in zip(keys, values)}
array_column($users, 'email')[u["email"] for u in users]
array_unique($items)list({x for x in items})
Практика: операции над списком словарей (must-know cheatsheet)

Самый частый паттерн в реальном коде — обработка данных из БД, API-ответов, JSON. Держи как справочник:

users = [{"name": "Bob", "age": 25}, {"name": "Alice", "age": 30}]

# ─── Сортировка по полю ──────────────────────────────────────
sorted(users, key=lambda u: u["age"])
sorted(users, key=lambda u: u["age"], reverse=True)

# По нескольким полям (сначала age, потом name)
sorted(users, key=lambda u: (u["age"], u["name"]))

# Через itemgetter — быстрее lambda
from operator import itemgetter
sorted(users, key=itemgetter("age"))

# ─── Максимум / минимум ──────────────────────────────────────
max(users, key=lambda u: u["age"])          # {"name": "Alice", "age": 30}
min(users, key=lambda u: u["age"])

# ─── Фильтр (питонично) ──────────────────────────────────────
[u for u in users if u["age"] >= 18]

# ─── Извлечь одно поле (аналог array_column PHP) ─────────────
[u["name"] for u in users]              # ["Bob", "Alice"]

# ─── Сумма / среднее по полю ─────────────────────────────────
sum(u["age"] for u in users)               # 55
sum(u["age"] for u in users) / len(users)   # 27.5

# ─── Логика по коллекции ─────────────────────────────────────
any(u["age"] > 28 for u in users)           # True — есть хоть один
all(u["age"] >= 18 for u in users)          # True — все

# ─── Найти первого по условию (безопасно) ────────────────────
next((u for u in users if u["name"] == "Bob"), None)
# default None — если не нашли (иначе StopIteration)

# ─── Индекс по ключу (dict из списка) ────────────────────────
users_by_name = {u["name"]: u for u in users}
users_by_name["Alice"]                          # O(1) доступ

# ─── Группировка по полю ─────────────────────────────────────
from collections import defaultdict
by_age = defaultdict(list)
for u in users:
    by_age[u["age"]].append(u["name"])
# {25: ["Bob"], 30: ["Alice"]}

# itertools.groupby — работает ТОЛЬКО с отсортированными по ключу
from itertools import groupby
sorted_users = sorted(users, key=itemgetter("age"))
for age, group in groupby(sorted_users, key=itemgetter("age")):
    print(age, list(group))

# ─── Добавить / изменить поле у каждого ──────────────────────
for u in users:
    u["is_adult"] = u["age"] >= 18          # мутация in-place

# Или через comprehension — новый список из новых dict'ов
enriched = [{**u, "is_adult": u["age"] >= 18} for u in users]

# ─── Удалить дубликаты по полю (сохранить первое) ────────────
seen = set()
unique = []
for u in users:
    if u["name"] not in seen:
        seen.add(u["name"])
        unique.append(u)

# ─── Разбить на два по условию (partition) ───────────────────
adults = [u for u in users if u["age"] >= 18]
minors = [u for u in users if u["age"] < 18]

# ─── Топ-N по полю ───────────────────────────────────────────
import heapq
top_3 = heapq.nlargest(3, users, key=itemgetter("age"))
bottom_3 = heapq.nsmallest(3, users, key=itemgetter("age"))
# быстрее sorted(...)[:3] для больших списков

# ─── Есть ли dict с таким полем ──────────────────────────────
"Bob" in {u["name"] for u in users}       # True

# ─── Слить два списка dict'ов по ключу ───────────────────────
orders = [{"user": "Bob", "total": 100}, {"user": "Alice", "total": 200}]
merged = [{**u, **next((o for o in orders if o["user"] == u["name"]), {})}
          for u in users]
# Для больших данных — переходи на pandas.merge
Правило: для мелких списков (до ~10k) — обычные comprehensions хватает. Для больших датасетов и множества операций — pandas (векторизация быстрее). Для реальной БД — SQL через GROUP BY/ORDER BY/JOIN.
Опечатка в ключе — молчит. u["nmae"] кинет KeyError при доступе. Безопаснее u.get("name") — вернёт None. Ещё безопаснее — @dataclass или Pydantic: IDE и mypy подсветят опечатку.
Мутация во время итерации. for u in users: users.remove(u) — пропустит элементы. Собирай новый список или итерируй по копии users[:].
Встроенные функции Python — справочник
Что это и зачем

Python предоставляет ~70 функций без импорта — они всегда доступны из любого модуля. Живут в модуле builtins, автоматически загружаемом. Полный список: dir(__builtins__) (там ~150 имён, включая исключения типа ValueError).

Правило: не зубри всё. Реально в 90% кода нужны 15: print, input, len, range, enumerate, zip, sorted, min, max, sum, int, str, list, isinstance, open. Остальные — по мере необходимости.
Ввод / вывод
ФункцияЧто делаетПример
print(*args, sep=' ', end='\n')Вывод в консольprint("a", "b", sep="-") → a-b
input(prompt="")Читает строку от пользователя (всегда str)name = input("Имя: ")
open(path, mode, encoding)Открыть файл (использовать через with)with open("f.txt", "r", encoding="utf-8") as f:
Длина, тип, проверка
ФункцияЧто делаетПример
len(x)Длина коллекции / строкиlen("hello") → 5
type(x)Класс объектаtype(42) → <class 'int'>
isinstance(x, T)Экземпляр класса T (учитывает наследников) — предпочтительнее type(x) == Tisinstance(42, (int, str)) → True (можно кортеж типов)
callable(x)Можно ли вызвать x()callable(print) → True; callable(42) → False
issubclass(A, B)A — подкласс Bissubclass(bool, int) → True
Правило: isinstance(x, T) — всегда вместо type(x) == T. Первое учитывает наследование (isinstance(True, int) → True, потому что bool — наследник int). Второе — строгое сравнение классов.
Приведение типов (конструкторы)
ФункцияЧто делаетПример
int(x)В целое (обрезает дробь, НЕ округляет)int("42") → 42; int(3.9) → 3
int(x, base)Парсинг с системой счисленияint("ff", 16) → 255
float(x)В floatfloat("3.14") → 3.14
str(x)В строку (через __str__)str([1, 2]) → "[1, 2]"
bool(x)В True/False (falsy: 0, "", [], {}, None)bool([]) → False; bool("hi") → True
list(x)Список из iterablelist("abc") → ['a', 'b', 'c']
tuple(x)Кортежtuple([1, 2]) → (1, 2)
set(x)Множество (уникальные)set([1, 1, 2]) → {1, 2}
dict(x)Словарьdict(a=1, b=2) → {'a': 1, 'b': 2}
bytes(x), bytearray(x)Бинарные типыbytes("hi", "utf-8")
frozenset(x)Неизменяемое множество (hashable — можно как ключ dict)frozenset([1, 2])
int(3.9) = 3, не 4 — обрезает к нулю. Для правильного округления — round(3.9).
Итерация
ФункцияЧто делаетПример
range(stop), range(start, stop, step)Ленивая последовательность чиселlist(range(0, 10, 2)) → [0, 2, 4, 6, 8]
enumerate(x, start=0)Пары (index, value)list(enumerate("ab")) → [(0, 'a'), (1, 'b')]
zip(a, b, ..., strict=False)Параллельный обход. strict=True (3.10+) кинет ошибку при разной длинеlist(zip([1, 2], ["a", "b"])) → [(1, 'a'), (2, 'b')]
reversed(x)Итератор в обратном порядке (без изменения оригинала)list(reversed([1, 2, 3])) → [3, 2, 1]
iter(x)Получить iteratorit = iter([1, 2, 3]); next(it) → 1
next(it, default)Следующий элемент или default вместо StopIterationnext(it, "!") → "!"
map(f, x)Применить f к каждому (ленивый)list(map(str, [1, 2, 3])) → ['1', '2', '3']
filter(f, x)Оставить элементы, где f(x) истинноlist(filter(lambda n: n > 0, [1, -2, 3])) → [1, 3]
Питоничнее: map/filter в новом коде обычно заменяют comprehension — короче и читаемее:
list(map(str, nums)) → [str(n) for n in nums]
list(filter(lambda n: n > 0, nums)) → [n for n in nums if n > 0]
Сортировка и границы
ФункцияЧто делаетПример
sorted(x, key=None, reverse=False)Отсортированная копия (в отличие от list.sort())sorted(users, key=lambda u: u.age)
min(x, key=None, default=...)Минимумmin(users, key=lambda u: u.age)
max(x, key=None, default=...)Максимумmax([1, 2, 3]) → 3
Математика
ФункцияЧто делаетПример
sum(x, start=0)Суммаsum([1, 2, 3]) → 6; sum([1, 2], 10) → 13
abs(x)Модульabs(-5) → 5
round(x, ndigits=None)Округление (bankers' rounding — половина к чётному)round(3.14159, 2) → 3.14; round(2.5) → 2 (!!!)
pow(base, exp, mod=None)Степень. С mod — эффективно для больших чисел (крипта)pow(2, 10, 100) → 24
divmod(a, b)Частное + остаток одним вызовомdivmod(7, 2) → (3, 1)
bin(x), oct(x), hex(x)В строковое представление системы счисленияhex(255) → '0xff'
ord(char)Char → int (Unicode code point)ord("A") → 65
chr(n)Int → char (Unicode)chr(65) → 'A'
round(2.5) → 2, не 3. Python использует bankers' rounding (half to even) — половина округляется к чётному. Это стандарт IEEE 754 для избежания статистического смещения. Не баг — фича.
Логика
ФункцияЧто делаетПример
all(x)Все элементы truthy? Пустой → Trueall([True, True]) → True; all([]) → True
any(x)Хоть один truthy? Пустой → Falseany([False, True]) → True; any([]) → False

Реальный use-case:

# Все users имеют email?
if all(u.email for u in users):
    send_bulk(users)

# Есть хоть один админ?
if any(u.is_admin for u in users):
    ...

# Ленивое короткое замыкание — как в SQL EXISTS
# При первом True в any / False в all — обход прекращается
Отладка и интроспекция
ФункцияЧто делаетПример
help(x)Показать docstring / документациюhelp(str.split)
dir(x)Список атрибутов и методов объектаdir("hi") → [..., 'split', 'upper', ...]
repr(x)«Отладочное» представление (видны escape-символы, кавычки)repr("hi\n") → "'hi\\n'"
id(x)Адрес объекта в памяти (для is)id(42)
hash(x)Хеш immutable-объекта. Для list — ошибка (unhashable)hash((1, 2)) — да; hash([1, 2]) — нет
getattr(obj, name, default)Атрибут по имени (динамически), с дефолтомgetattr(user, "email", "none")
setattr(obj, name, value)Установить атрибут по имениsetattr(user, "role", "admin")
hasattr(obj, name)Есть ли атрибутhasattr(user, "email")
delattr(obj, name)Удалить атрибут—
Область видимости
ФункцияЧто делаетПример
vars(x)__dict__ объекта (атрибуты как словарь)vars(user) → {'name': 'Alice', ...}
vars() (без аргумента)То же что locals()—
globals()Словарь глобальных переменных модуляОтладка, метапрограммирование
locals()Словарь локальных переменных (для чтения; изменения не сохранятся)Отладка

Используются редко — в основном для отладки и метапрограммирования (декораторы, ORM, тестовые фреймворки).

Опасное: eval / exec
eval("2 + 3")              # 5 — выполнить ВЫРАЖЕНИЕ, вернуть значение
eval("len('hi')")          # 2

exec("x = 5\nprint(x)")     # выполнить ИНСТРУКЦИИ, вернуть None
КРИТИЧНО: eval/exec на пользовательских данных = RCE. eval(user_input) с вводом "__import__('os').system('rm -rf /')" — уничтожит сервер. Использовать только для доверенных строк (свои шаблоны, тесты), никогда для внешнего ввода.

Безопасные альтернативы:

  • Парсинг чисел из строки → int(s) / float(s), а не eval(s)
  • Парсинг литералов (числа, строки, списки, dict) → ast.literal_eval(s) — только литералы, никаких вызовов функций
  • Парсинг математических выражений → библиотека sympy или свой AST-парсер
  • Динамическое выполнение кода из БД → крайне пересматривай архитектуру, обычно есть безопасное решение
Полная картина по категориям
КатегорияФункции
Ввод/выводprint, input, open
Длина / типlen, type, isinstance, issubclass, callable
Приведениеint, float, str, bool, list, tuple, set, dict, bytes, frozenset
Итерацияrange, enumerate, zip, reversed, iter, next, map, filter
Сортировкаsorted, min, max
Математикаsum, abs, round, pow, divmod, bin, hex, oct, ord, chr
Логикаall, any
Интроспекцияhelp, dir, repr, id, hash, getattr/setattr/hasattr, vars, globals, locals
Опасноеeval, exec, compile
Что запомнить:
  • 90% кода использует ~15 функций: print, input, len, range, enumerate, zip, sorted, min/max/sum, int/str/list, isinstance, open
  • iter/next/map/filter — низкоуровневые, чаще заменяются comprehensions
  • eval/exec — почти никогда не нужны и опасны; для литералов — ast.literal_eval
  • vars/globals/locals/getattr/setattr — метапрограммирование и отладка
  • Полный список — dir(__builtins__), ~150 имён с исключениями (ValueError, TypeError...)
ООП — классы, наследование, dunder, dataclass
Базовый класс
class User:
    # Class-атрибут — общий для всех экземпляров
    role = "guest"

    # Конструктор — __init__
    def __init__(self, name: str, age: int):
        self.name = name           # instance-атрибуты
        self.age = age

    # Обычный метод — первый аргумент всегда self
    def greet(self) -> str:
        return f"Hi, I'm {self.name}"

user = User("Alice", 30)
user.greet()                    # "Hi, I'm Alice"
user.name                       # "Alice"
user.role                       # "guest" (из class-атрибута)
Отличия от PHP: self — явный первый аргумент каждого метода (в PHP было неявное $this). Никаких public/private/protected в языке — только конвенция: _name = «внутреннее» (soft), __name = name-mangling (hard, редко). Всё по умолчанию публично.
Наследование + super()
class Admin(User):                    # extends User
    role = "admin"                       # переопределили class-атрибут

    def __init__(self, name, age, permissions):
        super().__init__(name, age)      # вызов конструктора родителя
        self.permissions = permissions

    def greet(self) -> str:
        base = super().greet()             # вызов метода родителя
        return f"{base} (admin)"

admin = Admin("Bob", 40, ["read", "write"])
admin.greet()                       # "Hi, I'm Bob (admin)"
isinstance(admin, User)            # True — Admin наследник User

Множественное наследование возможно (в отличие от PHP). Порядок разрешения — MRO (Method Resolution Order) по алгоритму C3:

class A: def hi(self): print("A")
class B(A): def hi(self): print("B")
class C(A): def hi(self): print("C")
class D(B, C): pass

D().hi()               # "B" — MRO: D → B → C → A → object
D.__mro__              # покажет полный порядок
Множественное наследование — редко. В 95% кода одиночное наследование или композиция. Multiple inheritance используется в основном для mixin-паттерна (класс с одним методом, добавляется как «примесь»).
@property — getter/setter без синтаксиса скобок
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self) -> float:
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("radius must be positive")
        self._radius = value

    @property
    def area(self) -> float:            # вычисляемое поле
        return 3.14159 * self._radius ** 2

c = Circle(5)
c.radius                        # 5 (вызовет getter, но выглядит как атрибут)
c.radius = 10                   # вызовет setter
c.radius = -1                   # ValueError
c.area                          # 314.159 — без скобок!

Питоническое решение: начинай с обычного публичного атрибута; когда потребуется валидация/вычисление — превращай в @property, интерфейс наружу не меняется.

@classmethod и @staticmethod
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_dict(cls, data: dict) -> "User":
        # cls = сам класс. Работает и в наследниках.
        return cls(data["name"], data["age"])

    @staticmethod
    def is_valid_email(email: str) -> bool:
        # Ни self, ни cls — обычная функция в namespace класса
        return "@" in email

user = User.from_dict({"name": "Alice", "age": 30})
User.is_valid_email("a@b.c")   # True

Различие: @classmethod получает класс — используется для альтернативных конструкторов (from_dict, from_json). @staticmethod — просто функция, помещённая в namespace класса для семантики.

Dunder-методы (magic methods)

Специальные методы с двойным подчёркиванием — «крючки» в поведение языка. Питон вызывает их сам при определённых операциях.

DunderКогда вызываетсяПример
__init__Создание объектаКонструктор
__str__str(obj), print(obj), f"{obj}"Человекочитаемое представление
__repr__repr(obj), в REPL, в debugОднозначное представление для разработчика
__eq__, __ne__a == b, a != bСравнение на равенство
__lt__, __le__, __gt__, __ge__<, <=, >, >=Для сортировки
__hash__hash(obj), использование в set/dict-ключахОбязателен если __eq__
__len__len(obj)Для коллекций
__iter__, __next__for x in obj:Iterator protocol
__getitem__, __setitem__obj[key], obj[key] = vИндексация
__call__obj(...)Объект как функция
__enter__, __exit__with obj as x:Context manager
class Money:
    def __init__(self, amount, currency):
        self.amount = amount
        self.currency = currency

    def __repr__(self) -> str:
        return f"Money({self.amount}, {self.currency!r})"

    def __str__(self) -> str:
        return f"{self.amount} {self.currency}"

    def __eq__(self, other) -> bool:
        return self.amount == other.amount and self.currency == other.currency

    def __hash__(self) -> int:
        return hash((self.amount, self.currency))

    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError("cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)

Money(100, "USD") + Money(50, "USD")   # Money(150, 'USD')
Dunder: атрибут vs метод — принципиальная разница

В Python есть два вида dunder-имён (double underscore, «двойное подчёркивание с двух сторон»). Их часто путают:

Dunder-атрибут (данные)Dunder-метод (функция)
Что этоПросто значениеФункция, которую можно вызвать
ОбращениеБез скобок: obj.__name__Со скобками: obj.__str__()
Примеры__name__, __doc__, __dict__, __class__, __module__, __file____init__, __str__, __len__, __eq__, __add__, __call__
Кто устанавливаетPython — сам, читаешь готовоеТы — определяешь в своём классе, Python вызывает
Проверкаcallable(x.__name__) → Falsecallable(x.__len__) → True
Ключевая мысль: «Dunder» — это соглашение об именах в Python: __что-то__ зарезервировано для языка. Некоторые из них — атрибуты-данные (Python их устанавливает автоматически, ты просто читаешь). Другие — методы-хуки: ты пишешь их в своём классе, а Python вызывает сам, когда пользователь делает обычные операции (+, len(), print, for, with).
Dunder-атрибуты — «данные, которые даёт Python»

Не методы, а готовая информация об объекте. Читаешь без скобок.

АтрибутЧто содержитПример
__name__Имя объекта как строкаgreet.__name__ → 'greet'
__doc__Docstring (первая строка в """) функции/класса/модуляstr.__doc__
__dict__Словарь атрибутов объектаuser.__dict__ → {'name': 'Alice'}
__class__Класс объекта (= type(obj))user.__class__.__name__ → 'User'
__module__Модуль, где определёнUser.__module__ → 'app.users'
__file__Путь к файлу модуляmy_module.__file__
__all__Список публичных имён при from x import *Задаётся вручную в модуле
__slots__Разрешённые атрибуты (экономит память)Задаётся вручную в классе
__annotations__Type hints как dictfunc.__annotations__ → {'x': int, 'return': str}

Особый случай — __name__ у разных объектов:

У чегоЧто содержит
Модуль (файл .py)Имя модуля ('mymodule'), либо '__main__' при прямом запуске
ФункцияИмя функции: greet.__name__ → 'greet'
КлассИмя класса: User.__name__ → 'User'
Тип (через type(...))Имя типа: type(42).__name__ → 'int'

Отсюда идиома:

# mymodule.py
def main(): ...

if __name__ == "__main__":
    main()      # сработает только при `python mymodule.py`
                    # НЕ при `import mymodule`
Dunder-методы — «хуки, которые вызывает Python»

Ты определяешь в своём классе, Python сам вызывает когда пользователь делает обычную вещь. В этом и есть «магия».

1. Жизненный цикл

МетодКогда вызывается
__init__(self, ...)При создании: User("Alice")
__new__(cls, ...)Ещё раньше — создаёт сам объект. Редко нужен (immutable-типы, метаклассы)
__del__(self)При удалении объекта GC. Ненадёжно, лучше использовать with

2. Строковое представление

МетодДля чего
__str__(self)str(x), print(x), f-строка — «для людей»
__repr__(self)repr(x), REPL, debug — «для программиста», желательно чтобы можно было eval(repr(x))
__format__(self, spec)f"{x:spec}", format(x, spec)

3. Сравнение

МетодОператор
__eq__ / __ne__== / !=
__lt__ / __le__< / <=
__gt__ / __ge__> / >=
__hash__(self)hash(x) — обязателен если __eq__, иначе объект unhashable

Хочешь все 6 операторов сравнения сразу — @functools.total_ordering сгенерирует их из __eq__ + __lt__.

4. Арифметика

МетодОператор
__add__+
__sub__-
__mul__*
__truediv__/
__floordiv__//
__mod__%
__pow__**
__matmul__@ (матрицы, NumPy/PyTorch)
__neg__, __pos__, __abs__-x, +x, abs(x)
__iadd__, __isub__, ...+=, -=, ... (in-place)

5. Контейнеры (list/dict-подобное поведение)

МетодОператор
__len__(self)len(x)
__getitem__(self, key)x[key]
__setitem__(self, key, val)x[key] = val
__delitem__(self, key)del x[key]
__contains__(self, item)item in x
__iter__(self)for i in x:
__next__(self)next(x) — для iterator'ов
__reversed__(self)reversed(x)

6. Context managers (with)

МетодКогда
__enter__(self)Вход в with. Возвращаемое значение = as var
__exit__(self, exc_type, exc_val, tb)Выход, даже при exception

7. Другие важные хуки

МетодОператор
__call__(self, ...)obj(...) — объект вызывается как функция
__bool__(self)bool(x), if x:
__getattr__(self, name)Атрибут не найден — fallback
__setattr__(self, name, val)При любом присваивании атрибута (осторожно с рекурсией!)
__getattribute__(self, name)При любом доступе (даже к существующему) — очень опасно
__slots__ = ("x", "y")Ограничение разрешённых атрибутов, экономия памяти
Полный пример — свой класс со всеми хуками
class Basket:
    def __init__(self, items=None):        # создание
        self._items = items or []

    def __repr__(self):                    # debug
        return f"Basket({self._items!r})"

    def __str__(self):                     # print
        return f"Корзина: {len(self._items)} товаров"

    def __len__(self):                     # len()
        return len(self._items)

    def __getitem__(self, i):                # basket[i]
        return self._items[i]

    def __contains__(self, item):            # item in basket
        return item in self._items

    def __iter__(self):                    # for x in basket
        return iter(self._items)

    def __bool__(self):                    # if basket:
        return len(self._items) > 0

    def __add__(self, other):                # basket1 + basket2
        return Basket(self._items + other._items)

    def __eq__(self, other):                 # basket1 == basket2
        return self._items == other._items

# Использование — Python сам дёргает нужные хуки
b = Basket(["хлеб", "молоко"])
print(b)                # __str__      → "Корзина: 2 товаров"
len(b)                  # __len__      → 2
b[0]                    # __getitem__  → "хлеб"
"молоко" in b            # __contains__ → True
bool(b)                 # __bool__     → True
for item in b:            # __iter__
    print(item)

# Что даёт Python сам как атрибуты — без определения:
b.__class__.__name__     # 'Basket'
b.__dict__               # {'_items': ['хлеб', 'молоко']}
Как отличить атрибут от метода на практике
x = "hello"

callable(x.__str__)         # True — метод, можно вызвать
callable(x.__class__)       # True — класс сам callable (создаёт объект)
callable(x.__doc__)         # False — атрибут (строка)

# Ещё способ — type
type(x.__str__)             # <class 'method-wrapper'>
type(x.__doc__)             # <class 'str'>
Мнемоника: Ты определяешь методы (хуки), Python даёт тебе атрибуты (готовые данные).
Хуки — «Python дёрнет когда пользователь напишет +/len()/print()».
Атрибуты — «Python сохранил информацию, ты её читаешь».
@dataclass — DTO без boilerplate (must-have Python 3.7+)
from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    age: int
    role: str = "user"
    tags: list[str] = field(default_factory=list)   # для mutable default

user = User("Alice", 30)
print(user)                    # User(name='Alice', age=30, role='user', tags=[])
user == User("Alice", 30)      # True — __eq__ автоматом

# Опции:
@dataclass(frozen=True)          # иммутабельный
@dataclass(slots=True)           # меньше памяти, быстрее (3.10+)
@dataclass(kw_only=True)         # только keyword-arguments

Автоматически генерирует __init__, __repr__, __eq__. Аналог PHP readonly class или Java records. Используй по умолчанию для всех классов-DTO. Для валидации данных из внешнего мира (API/форм) — Pydantic.

Особые случаи
1. self — не ключевое слово. Можно назвать хоть this, но никогда так не делай — это конвенция сообщества. IDE и линтеры на неё завязаны.
2. Class-атрибуты shared между экземплярами. Если class-атрибут — mutable (list, dict), изменение через instance.attr без переприсваивания повлияет на всех.
3. Нет реального private. _x — «руками не трогать». __x — name-mangling (внутри класса Foo превращается в _Foo__x), только для избежания коллизий в наследовании. Не защита.
4. __eq__ без __hash__ — объект становится unhashable. После переопределения __eq__ обязательно определи __hash__ (или @dataclass(frozen=True) сделает автоматом).
5. Не наследуйся от dict/list. Хочешь свою коллекцию — наследуйся от collections.UserDict / UserList, иначе внутренние методы могут не звать твои переопределения.
6. Abstract Base Class — через abc. from abc import ABC, abstractmethod; class Repo(ABC): @abstractmethod def save(self): .... Экземпляр создать нельзя, пока не реализуешь метод.
Итог: для DTO — @dataclass. Для сервисов — обычный класс с DI через __init__. Для валидации внешних данных — Pydantic. Наследование — сдержанно, композиция чаще лучше. Dunder-методы — при необходимости (__eq__, __hash__, __repr__ — почти всегда стоит). Множественное наследование — только для mixin.
Модули и импорты
Модуль = файл

Любой .py-файл — модуль. Импорт делает его переменные/функции/классы доступными в другом файле.

# math_utils.py
PI = 3.14159
def area(radius): return PI * radius ** 2

# app.py
import math_utils                      # модуль целиком
math_utils.area(5)
math_utils.PI

from math_utils import area, PI       # только нужное
area(5)

from math_utils import area as circle_area   # переименовать
circle_area(5)

from math_utils import *                    # анти-паттерн, засоряет namespace
Пакет = директория с __init__.py
myapp/
├── __init__.py            # делает директорию пакетом
├── main.py
├── users/
│   ├── __init__.py
│   ├── models.py          # class User
│   └── service.py         # def register(...)
└── orders/
    ├── __init__.py
    └── models.py

# В main.py — абсолютные импорты (рекомендуется)
from myapp.users.models import User
from myapp.users.service import register

# Внутри users/service.py — относительные (только между модулями пакета)
from .models import User         # тот же пакет
from ..orders.models import Order  # родительский пакет

__init__.py может быть пустым (просто маркер пакета) или содержать re-exports:

# myapp/users/__init__.py
from .models import User
from .service import register

# Теперь можно короче:
from myapp.users import User, register
if __name__ == "__main__":

Каждый модуль имеет специальную переменную __name__:

  • Если файл запущен напрямую (python app.py) → __name__ == "__main__"
  • Если файл импортирован (import app) → __name__ == "app"
# cli.py
def main():
    print("Running CLI")

if __name__ == "__main__":
    main()      # сработает только при `python cli.py`,
                    # не при `import cli`

Позволяет один файл использовать и как скрипт, и как импортируемый модуль без побочных эффектов при импорте.

Особые случаи
1. Circular imports. a.py импортирует b, b.py импортирует a → ImportError. Решения: разбить общий код в третий модуль c.py; отложенный импорт внутри функции; type hints через TYPE_CHECKING.
2. Модуль импортируется один раз. Python кеширует в sys.modules. Повторный import — no-op. Перезагрузка — importlib.reload(m) (редко нужно, только для интерактивной разработки).
3. Код на верхнем уровне модуля исполняется при первом импорте. Не пиши там тяжёлые операции (запросы к БД, HTTP), только определения. Инициализация — в if __name__ == "__main__":.
4. from X import * — не используй. Ломает читаемость (непонятно откуда символ), может перекрыть built-ins. Исключение — __all__ в __init__.py пакета, если сам это контролируешь.
5. Абсолютные vs относительные. Абсолютные (from myapp.users.models import User) — предпочтительнее. Относительные (from ..models import User) — только внутри пакета, экономят на длине.
venv, pip, poetry, uv — управление зависимостями
Зачем venv

Проект A требует Django 4.2, проект B — Django 5.1. Оба ставить глобально нельзя. Virtual environment — изолированная копия интерпретатора со своим site-packages/. Аналог vendor/ в PHP, но на уровне интерпретатора.

Ubuntu 24.04 запрещает pip install глобально по умолчанию (PEP 668, externally-managed-environment). Только через venv или --break-system-packages (не делай так). macOS через brew — та же ситуация.
Классика: venv + pip
# Создать окружение в текущей директории
python -m venv .venv

# Активировать
source .venv/bin/activate         # macOS/Linux
.venv\Scripts\activate            # Windows PowerShell

# Теперь python/pip = из .venv/
which python                     # /path/to/project/.venv/bin/python
pip install fastapi uvicorn sqlalchemy

# Заморозить
pip freeze > requirements.txt

# Восстановить в новом окружении
pip install -r requirements.txt

# Деактивировать
deactivate

Добавь .venv/ в .gitignore — окружение локально, восстанавливается из requirements.txt. По той же логике, что vendor/ в PHP.

Современно (2026): uv

uv (Astral, 2024) — Rust-based замена pip + venv + pip-tools. В 10-100 раз быстрее. Управляет всем через pyproject.toml + uv.lock — воспроизводимые сборки как composer.lock.

# Установка (один раз)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Создать проект
uv init my-api && cd my-api

# Установить конкретную версию Python (если её нет — скачает)
uv python install 3.13

# Добавить зависимости — сам создаст .venv, обновит pyproject.toml + uv.lock
uv add fastapi uvicorn sqlalchemy
uv add --dev pytest ruff mypy

# Запустить — не нужно `source .venv/bin/activate`
uv run python app.py
uv run uvicorn main:app --reload

# Установить из lock-файла (для CI/деплоя)
uv sync                           # reproducible install
Poetry — старый стандарт (уходит)

Poetry (2018) — до uv был стандартом. Тот же принцип (pyproject.toml + lock), но написан на Python и в 100 раз медленнее.

poetry init
poetry add fastapi
poetry install
poetry run pytest

Для нового проекта в 2026 — uv. Poetry — только если проект уже на нём.

Сравнение инструментов
ИнструментСкоростьLock-файлУправляет Python-версиейКогда выбирать
pip + venvМедленноrequirements.txt (плоский)НетLegacy, простые скрипты
pip-toolsМедленноrequirements.lockНетЕсть pip, нужен lock
PoetryМедленноДаНет (нужен pyenv)Существующие проекты
pipenvОчень медленноДаНетНе выбирать
uvМгновенноДаДаНовые проекты (2026)
pyproject.toml — современный конфиг
# pyproject.toml — как composer.json в PHP
[project]
name = "my-api"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.110",
    "sqlalchemy>=2.0",
    "asyncpg",
]

[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]

[tool.ruff]
line-length = 100
target-version = "py313"

[tool.mypy]
strict = true
Итог: для нового проекта в 2026 — uv. Для legacy/скриптов — venv + pip. Всегда .venv/ в .gitignore. Файл pyproject.toml с pin-версиями + lock-файл в git — воспроизводимая сборка на CI и в проде.
Исключения — try / except / raise / with
Базовый синтаксис
try:
    data = json.loads(payload)
    result = data["user"]["id"]
except json.JSONDecodeError as e:
    logger.error(f"Invalid JSON: {e}")
    result = None
except KeyError as e:
    logger.warning(f"Missing key: {e}")
    result = None
except Exception:
    logger.exception("Unexpected")   # exception() = error() + traceback
    raise                              # перебросить дальше
else:
    # выполнится только если НЕ было исключения в try
    logger.info(f"Got user_id={result}")
finally:
    # выполнится ВСЕГДА — успех, исключение или return/break
    connection.close()
Правило: лови только те исключения, которые знаешь как обработать. except Exception без raise — почти всегда баг: скрывает реальные ошибки.
Иерархия — во что кастовать
BaseException ← не ловить (SystemExit, KeyboardInterrupt) └── Exception ← ловить ВСЁ остальное ├── ValueError ← неверное ЗНАЧЕНИЕ (int('abc')) ├── TypeError ← неверный ТИП (len(5)) ├── KeyError ← нет ключа в dict ├── IndexError ← индекс за пределами list ├── AttributeError ← нет атрибута у объекта ├── FileNotFoundError ← файл не найден (subclass OSError) ├── ZeroDivisionError ├── StopIteration ← конец итератора └── RuntimeError ← generic "что-то пошло не так"

Не ловить BaseException напрямую — там KeyboardInterrupt (Ctrl+C) и SystemExit. Пользователь не сможет прервать программу.

raise — бросить исключение
def divide(a, b):
    if b == 0:
        raise ValueError("cannot divide by zero")
    return a / b

# Перебросить с сохранением контекста
try:
    do_something()
except Exception as e:
    raise RuntimeError("failed") from e
    # traceback покажет ОБА — оригинал не потерян

# Голый raise — только внутри except, перебрасывает текущее
try:
    ...
except Exception:
    log("got exception")
    raise
Кастомные исключения
class OrderError(Exception):
    """Base для всех доменных ошибок заказов."""

class OrderNotFound(OrderError):
    pass

class InsufficientStock(OrderError):
    def __init__(self, sku, requested, available):
        super().__init__(f"SKU {sku}: need {requested}, have {available}")
        self.sku = sku
        self.requested = requested
        self.available = available

# Ловим одним except по базовому классу
try:
    place_order(...)
except OrderError as e:
    logger.error(f"Order failed: {e}")

Свой класс — когда обработчик выше по стеку должен различать типы ошибок. Внутри одного модуля обычные ValueError/RuntimeError сойдут.

with — context managers

Гарантированное освобождение ресурсов даже при исключении. Аналог try/finally, только короче:

# Без with — легко забыть закрыть
f = open("data.txt")
data = f.read()
f.close()                          # не выполнится если read() кинет

# С with — close автоматически
with open("data.txt") as f:
    data = f.read()

# Несколько ресурсов сразу
with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())

Свой context manager — через декоратор @contextmanager:

from contextlib import contextmanager

@contextmanager
def db_transaction(conn):
    conn.begin()
    try:
        yield conn
        conn.commit()
    except:
        conn.rollback()
        raise

with db_transaction(conn) as tx:
    tx.execute(...)
EAFP vs LBYL — питоничный подход

Два подхода к «а вдруг что-то пойдёт не так»:

LBYL (PHP-стиль)EAFP (питонично)
РасшифровкаLook Before You LeapEasier to Ask Forgiveness than Permission
ПодходПроверить условие ДО операцииПопробовать, поймать исключение
Пример
if "key" in d:
    v = d["key"]
try:
    v = d["key"]
except KeyError:
    v = None

Питоничнее — EAFP, но не догматично. d.get("key") ещё короче. Правило: EAFP когда «в обычном случае получится, ошибка — редкая»; LBYL когда «проверка дешёвая, ошибка ожидаема».

Особые случаи
1. except: pass — почти всегда баг. Проглатывает всё, включая KeyboardInterrupt. Минимум — except Exception: logger.exception(...).
2. Порядок except имеет значение. Специфичные — выше, общие — ниже. except Exception первым съест всё.
3. Исключения — не для управления потоком. for/break лучше, чем try/except StopIteration.
4. Не использовать исключения как return. Функция get_user_or_raise() ок; но не find_user() которая всегда кидает при отсутствии — верни None.
5. raise ... from None — скрывает оригинальный контекст. Используй только когда специально хочешь заменить traceback (redacting чувствительных данных).
Итог: лови конкретные исключения, не Exception. Используй raise ... from e для chaining. with для ресурсов — файлы, БД-соединения, блокировки. EAFP — питоничный дефолт.
Файлы + JSON + CSV + pathlib
open() и режимы
# Всегда через with — гарантированное закрытие
with open("data.txt", encoding="utf-8") as f:
    content = f.read()

# Режимы
open(path, "r")                # чтение (default) — если файла нет, FileNotFoundError
open(path, "w")                # запись — создаст / ПЕРЕЗАПИШЕТ
open(path, "a")                # append — создаст / допишет в конец
open(path, "x")                # exclusive create — упадёт если файл есть
open(path, "r+")               # чтение + запись
open(path, "rb")               # binary read (для картинок, PDF)
open(path, "wb")               # binary write

# Три способа чтения
with open("log.txt") as f:
    all_at_once = f.read()          # одна строка
    # или
    lines = f.readlines()           # список строк (все в память)
    # или (лучший для больших файлов) — построчно
    for line in f:
        process(line.rstrip())        # O(1) память

# Запись
with open("out.txt", "w", encoding="utf-8") as f:
    f.write("one line\n")
    f.writelines(["a\n", "b\n"])   # не добавит \n сам!
Всегда указывай encoding="utf-8". Без него берётся дефолтный OS — на Windows это cp1251, на Linux utf-8. Один код будет ломаться на разных ОС.
pathlib.Path — современная работа с путями

Забудь про os.path.join, os.path.exists, os.listdir. pathlib (Python 3.4+) — ООП-обёртка, чище и переносимо.

from pathlib import Path

p = Path("/var/log/app.log")
p = Path("data") / "users.json"       # склейка через /
p = Path.home() / ".config" / "app"     # $HOME/.config/app

# Свойства
p.name                          # "app.log"
p.stem                          # "app" (без расширения)
p.suffix                        # ".log"
p.parent                        # Path("/var/log")
p.parts                         # ("/", "var", "log", "app.log")
p.absolute()                    # абсолютный путь

# Проверки
p.exists()
p.is_file()
p.is_dir()

# Операции
p.mkdir(parents=True, exist_ok=True)     # создать директорию (рекурсивно, no-op если есть)
p.unlink(missing_ok=True)                # удалить файл
p.rename(Path("new_name.log"))
p.rmdir()                                # удалить пустую директорию

# Обход директории
for item in Path(".").iterdir():           # прямые потомки
    print(item)

for py in Path("src").rglob("*.py"):       // рекурсивно по glob
    print(py)

# Чтение / запись — методы прямо на Path
text = Path("config.txt").read_text(encoding="utf-8")
Path("out.txt").write_text("hello", encoding="utf-8")
data = Path("image.png").read_bytes()
JSON — модуль json
import json

# Из строки → Python-объект
data = json.loads('{"name": "Alice", "age": 30}')
# data = {"name": "Alice", "age": 30}

# Python-объект → строку
s = json.dumps(data)
s = json.dumps(data, indent=2, ensure_ascii=False)
# ensure_ascii=False — не эскейпить кириллицу в \uXXXX

# Из / в файл — load/dump без "s"
with open("data.json", encoding="utf-8") as f:
    data = json.load(f)

with open("out.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2, ensure_ascii=False)

# Соответствия типов
# JSON     → Python
# object   → dict
# array    → list
# string   → str
# number   → int / float
# true     → True
# null     → None

# Ошибки
try:
    data = json.loads(payload)
except json.JSONDecodeError as e:
    logger.error(f"bad JSON: {e}")
datetime не сериализуется по умолчанию. json.dumps({"created": datetime.now()}) → TypeError. Решения: конвертировать в строку заранее (.isoformat()), либо json.dumps(..., default=str), либо использовать Pydantic.
CSV — модуль csv
import csv

# Чтение — обычные списки-строки
with open("users.csv", encoding="utf-8") as f:
    reader = csv.reader(f)
    header = next(reader)          # первая строка — обычно шапка
    for row in reader:
        print(row)              # ['Alice', '30', 'a@b.c']

# Чтение как dict — DictReader (питоничнее)
with open("users.csv", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        print(row["name"], row["email"])

# Запись
with open("out.csv", "w", encoding="utf-8", newline="") as f:
    # newline="" — обязательно, иначе на Windows \r\r\n
    writer = csv.DictWriter(f, fieldnames=["name", "age"])
    writer.writeheader()
    writer.writerow({"name": "Alice", "age": 30})
    writer.writerows([{"name": "Bob", "age": 25}])

# Кастомный разделитель — TSV, semicolon
reader = csv.reader(f, delimiter=";")
Excel «съедает» leading zeros. Если открываешь CSV в Excel — "00123" станет 123. Не проблема Python, но пользователи ругаются. Для reliable выгрузки в Excel — .xlsx через openpyxl.
Особые случаи
1. Забыл with. f = open(path) без close() — файл висит открытым, на Windows его нельзя переименовать, на Linux — деcкриптор течёт.
2. Загрузка гигабайтного файла в память. f.read() прочтёт всё сразу. Для больших — for line in f или чтение чанками f.read(8192).
3. json.dumps без ensure_ascii=False. Кириллица превратится в При... — читабельно, но раздутый файл. Всегда ensure_ascii=False для внутреннего использования.
4. csv без newline="". На Windows будут двойные переносы строк.
5. Относительные пути. open("data.txt") откроет от текущей рабочей директории, а не от файла со скриптом. Лучше: Path(__file__).parent / "data.txt".
Декораторы @
Что такое декоратор

Декоратор — функция, принимающая другую функцию и возвращающая новую (обычно с дополнительным поведением до/после вызова). Синтаксис @decorator — просто сахар над func = decorator(func).

# Простой декоратор — измеряет время выполнения
import time
from functools import wraps

def timed(func):
    @wraps(func)                     # сохраняет __name__/__doc__ оригинала
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

@timed
def slow_calc(n):
    time.sleep(1)
    return n * 2

slow_calc(5)
# slow_calc took 1.001s
# 10

Эквивалент без синтаксиса @:

def slow_calc(n): ...
slow_calc = timed(slow_calc)   # ровно то же самое
Декоратор с аргументами
# Retry с настраиваемым числом попыток
def retry(times=3, delay=1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e
                    time.sleep(delay)
            raise last_error
        return wrapper
    return decorator

@retry(times=5, delay=0.5)
def fetch_url(url):
    return requests.get(url)

Три уровня функций: retry(times=3) возвращает decorator, тот принимает func, а wrapper уже делает работу. Синтаксический сахар разворачивается в fetch_url = retry(times=5, delay=0.5)(fetch_url).

Встроенные / stdlib декораторы
ДекораторЧто делает
@propertyGetter, вызываемый как атрибут
@staticmethodМетод без self/cls
@classmethodМетод, получающий cls вместо self
@dataclassАвтогенерирует __init__/__repr__/__eq__
@functools.cacheМемоизация — кеш результата по аргументам
@functools.lru_cache(maxsize=128)Мемоизация с ограничением размера
@functools.wraps(func)Сохраняет метаданные оригинала — используй в своих
@abstractmethodАбстрактный метод (в ABC-классе)
@contextmanagerПревращает генератор в context manager
from functools import cache

@cache
def fib(n):
    if n < 2: return n
    return fib(n-1) + fib(n-2)

fib(100)     # мгновенно — благодаря кешу
Декораторы во фреймворках
# Flask
@app.route("/users/<int:id>")
def get_user(id): ...

# FastAPI
@app.get("/users/{id}")
def get_user(id: int): ...

# pytest — параметризация тестов
@pytest.mark.parametrize("a, expected", [(1, 2), (2, 4)])
def test_double(a, expected):
    assert double(a) == expected

# Django — авторизация
@login_required
def profile(request): ...
Особые случаи
1. Забыл @wraps. wrapper.__name__ станет "wrapper", а не имя оригинальной функции. Ломает help(), debug, некоторые фреймворки (Flask ругается на конфликт имён при регистрации роутов).
2. Порядок декораторов важен. @a @b def f() = f = a(b(f)). Ближний к функции — применяется первым. Для маршрутов часто важно: @app.route @login_required — сначала auth-check.
3. Декоратор с/без аргументов — разные функции. @retry и @retry() — не одно и то же. Первый передаст retry(func), второй — retry()(func).
4. Type hints оригинала теряются если декоратор не типизирован через ParamSpec. Для нового кода — ParamSpec из typing (Python 3.10+).
5. Не переусердствуй. Стек из 4-5 декораторов на одной функции — сложно отлаживать. Логики много — сделай явный класс.
Итог: декоратор = функция, оборачивающая функцию. @wraps — обязательно. Встроенные (@property, @dataclass, @cache) — используй сразу. Свои — для сквозных обязанностей: логирование, retry, кеш, auth-check в фреймворке.
Type hints + mypy
Базовый синтаксис
# Переменные
name: str = "Alice"
age: int = 30
is_active: bool = True
tags: list[str] = ["php", "python"]
user: dict[str, int] = {"age": 30}

# Функции
def greet(name: str, times: int = 1) -> str:
    return (f"Hi {name}! ") * times

# Возврат None (void в других языках)
def log_it(msg: str) -> None:
    print(msg)

# Классы
class User:
    name: str
    age: int

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age
Ключевое: type hints — подсказки, не enforcement. Python не проверяет их в рантайме. Проверяет отдельный tool — mypy. IDE использует их для автокомплита и подсветки ошибок.
Опциональные и объединения — |
# X | None — может быть X или None (Python 3.10+)
def find_user(id: int) -> User | None:
    ...

# Legacy до 3.10 (ещё встречается)
from typing import Optional
def find_user(id: int) -> Optional[User]:
    ...

# Union — X или Y или Z
def parse(value: str | int | float) -> float:
    ...

# Коллекции с несколькими типами внутри
users: list[User | Admin] = [...]
Коллекции — новый vs старый синтаксис
Legacy (до Python 3.9)Modern (3.9+)
List[int]list[int]
Dict[str, int]dict[str, int]
Tuple[int, str]tuple[int, str]
Set[int]set[int]
Optional[X]X | None (3.10+)
Union[X, Y]X | Y (3.10+)

В новом коде — только list/dict/|. Импорты из typing для этих типов больше не нужны.

Специальные типы
from typing import Any, Callable, Literal, Final, TypedDict, Protocol

# Any — «выключить» type-check (использовать в крайних случаях)
def parse_json(s: str) -> Any:
    return json.loads(s)

# Callable — функция как аргумент
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

# Literal — конкретные значения
def set_status(status: Literal["active", "paused", "stopped"]) -> None:
    ...
set_status("active")     #
set_status("deleted")    # mypy error

# Final — константа, mypy запретит перезаписать
MAX_RETRIES: Final[int] = 3

# TypedDict — структура словаря
class UserDict(TypedDict):
    name: str
    age: int
    email: str | None

def save_user(u: UserDict) -> None: ...

# Protocol — duck typing со статической проверкой
class HasName(Protocol):
    name: str

def greet(obj: HasName) -> str:
    return f"Hi {obj.name}"
# Работает с любым классом у которого есть поле name — независимо от наследования
mypy — статическая проверка
# Установка
uv add --dev mypy

# Запуск
mypy app.py
mypy src/

# Конфиг — pyproject.toml
[tool.mypy]
python_version = "3.13"
strict = true                    # все проверки на максимум
warn_unused_ignores = true
disallow_untyped_defs = true

# Игнорировать одну строку
result = something_weird()  # type: ignore[assignment]

# Игнорировать модуль без типов
[[tool.mypy.overrides]]
module = "legacy_lib.*"
ignore_missing_imports = true
Особые случаи
1. Type hints — не enforcement. x: int = "hello" запустится без ошибки в рантайме. Только mypy заметит. Для проверки в рантайме — Pydantic (см. FastAPI).
2. Forward reference — в кавычках. Если типизируешь классом, который ещё не определён (self-reference или взаимные ссылки) — используй строку: def foo(self) -> "User" или from __future__ import annotations в начале файла.
3. Any — «выключение» типизации. Использовать как крайнюю меру. mypy пропускает через Any любые операции.
4. Legacy импорты List/Dict. В новом коде — list/dict. Не смешивай.
5. mypy strict сразу — больно. На существующем проекте включай постепенно: сначала для новых модулей, потом расширяй.
6. Runtime access к аннотациям. func.__annotations__ — dict имя→тип. Используется Pydantic, FastAPI, dataclasses для генерации кода.
Итог: в новом Python-коде всегда пиши type hints — читаемость + IDE + mypy окупают дополнительные символы. Модерн: list[int], X | None, TypedDict, Protocol. Настрой mypy strict в pyproject.toml и запусти в CI.
async / await + asyncio
Зачем async в backend

HTTP-запрос к БД, вызов внешнего API, чтение файла — во всех случаях процесс ждёт I/O. Синхронный код в этот момент блокирует поток. Async позволяет одному потоку обслуживать сотни одновременных операций I/O — пока один запрос ждёт БД, поток обрабатывает другие.

Аналогия: синхронный официант обслуживает один столик — приняв заказ, стоит рядом и ждёт пока клиент выберет десерт. Async-официант — принял заказ, пошёл к следующему столику; вернётся, когда клиент помашет рукой.

Когда нужен async:

  • HTTP API с тысячами RPS (FastAPI async — стандарт)
  • WebSocket-серверы, real-time
  • Web-scrapers — тысячи параллельных запросов
  • Микросервисы с множеством внешних вызовов

Когда НЕ нужен: CPU-heavy вычисления (тут async не помогает — нужен multiprocessing), простые CRUD с 5 RPS, скрипты. Не тащить async туда, где он не нужен — иначе получишь сложный код без выигрыша.

Базовый синтаксис
import asyncio

# async def создаёт корутину, не исполняет функцию сразу
async def fetch_user(id: int) -> dict:
    await asyncio.sleep(1)          # эмуляция I/O
    return {"id": id, "name": "Alice"}

# Обычный вызов вернёт корутину, не результат:
fetch_user(1)         # <coroutine object fetch_user at 0x...>

# Правильный запуск — только через event loop:
result = asyncio.run(fetch_user(1))

# await — только внутри async def
async def main():
    user = await fetch_user(1)      # дождались, получили dict
    print(user)

asyncio.run(main())
Параллельное выполнение — gather
async def fetch_user(id):
    await asyncio.sleep(1)
    return {"id": id}

# Последовательно — 3 секунды
async def bad():
    a = await fetch_user(1)
    b = await fetch_user(2)
    c = await fetch_user(3)

# Параллельно — 1 секунда
async def good():
    a, b, c = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3),
    )

# Обработка ошибок в gather
results = await asyncio.gather(*tasks, return_exceptions=True)
# исключения будут В списке, не бросят весь gather
Таймауты и отмена
# Timeout — Python 3.11+
async def get_with_timeout():
    async with asyncio.timeout(5.0):
        return await slow_operation()

# Legacy до 3.11 — wait_for
result = await asyncio.wait_for(slow_operation(), timeout=5.0)

# Создать задачу в фоне — не блокировать текущую функцию
task = asyncio.create_task(background_job())
print("main continues...")
await task                       # дождаться когда потребуется
task.cancel()                    # можно отменить
Async в реальном коде — HTTP, БД
# HTTP через httpx (async-first, поддерживает и sync)
import httpx

async def fetch_all(urls):
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [r.json() for r in responses]

# БД через asyncpg (PostgreSQL)
import asyncpg

async def get_users():
    conn = await asyncpg.connect("postgresql://...")
    rows = await conn.fetch("SELECT id, name FROM users")
    await conn.close()
    return rows

# SQLAlchemy 2.0 async
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession

engine = create_async_engine("postgresql+asyncpg://...")
async with AsyncSession(engine) as session:
    result = await session.execute(select(User))
    users = result.scalars().all()
asyncio vs threading vs multiprocessing
asynciothreadingmultiprocessing
Для чегоI/O-bound (HTTP, БД)I/O-bound + legacy codeCPU-bound
GIL мешает?Нет (один поток)Да, но I/O освобождаетНет (разные процессы)
OverheadМинимальныйСреднийВысокий
Общая памятьДа (тот же процесс)Да, нужны locksНет (IPC)
Тысячи задачЛегкоКончится памятьOverhead убьёт
Особые случаи
1. Забыл await. result = fetch_user(1) — в result корутина, а не dict. IDE подсветит; при запуске упадёт с RuntimeWarning: coroutine was never awaited.
2. Sync-вызов в async функции блокирует event loop. time.sleep(5) внутри async def — заблокирует ВЕСЬ сервер на 5 секунд. Используй await asyncio.sleep(5). Синхронный requests.get() — тоже блокирует; вместо него httpx.AsyncClient.
3. CPU-bound в async — бессмысленно. Тяжёлый цикл в корутине блокирует loop. Выноси в asyncio.to_thread(func) или в отдельный процесс.
4. Смешение sync + async кода. Из sync-функции нельзя await. Приходится asyncio.run(async_func()), но нельзя вложенно (если уже в event loop). Правило: если проект async — весь стек async.
5. GIL не касается asyncio. Async работает в одном потоке — GIL не влияет. Но всё что не I/O — блокирует.
6. Отладка сложнее. Traceback без имён async-функций (частично лечится в Python 3.12+). Используй logger.exception внутри corountine.
Итог: async def + await = concurrent I/O в одном потоке. Для тысяч одновременных операций — идеально. Для CPU — не поможет (нужен multiprocessing). Правило проекта: либо весь стек async, либо весь sync — смешивание болит. FastAPI + httpx + asyncpg/SQLAlchemy async — стандартная async-связка backend 2026.
Генераторы + iterators + itertools
Iterator protocol

Iterable — объект, у которого есть __iter__(). Возвращает iterator. Iterator — объект с __next__(), возвращает следующий элемент, кидает StopIteration когда всё кончилось. for x in obj: — сахар над этим протоколом.

# Внутри for крутится примерно вот это:
it = iter(users)          # получить iterator из iterable
while True:
    try:
        user = next(it)     # next() зовёт __next__()
    except StopIteration:
        break
    process(user)

# Свой iterator — редко, обычно используем генератор (см. ниже)
class Counter:
    def __init__(self, limit): self.n, self.limit = 0, limit
    def __iter__(self): return self
    def __next__(self):
        if self.n >= self.limit: raise StopIteration
        self.n += 1
        return self.n

for i in Counter(3): print(i)     # 1, 2, 3
Генератор через yield

Функция с yield — генератор. При вызове не выполняется сразу, возвращает generator-объект. Каждый next() исполняет до следующего yield, потом заморозит состояние.

def count_up_to(limit):
    n = 1
    while n <= limit:
        yield n
        n += 1

gen = count_up_to(3)   # НИЧЕГО не выполнилось; gen — генератор-объект
next(gen)               # 1
next(gen)               # 2
next(gen)               # 3
next(gen)               # StopIteration

# Обычно через for
for n in count_up_to(10):
    print(n)

# Или превратить в list (загрузит всё в память)
list(count_up_to(100))
Зачем это надо — ленивость

Главный use-case: обработка больших потоков данных без загрузки всего в память.

# Загрузим весь 10GB лог в память — OOM
def read_all(path):
    with open(path) as f:
        return f.readlines()      # list всех строк

# Читаем по строке, память O(1)
def read_lazy(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip()

# Пайплайн — можно накладывать фильтры/преобразования лениво
def errors_only(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def parse_json(lines):
    for line in lines:
        yield json.loads(line)

# Читаем 10GB, фильтруем, парсим — всё лениво, память O(1)
for event in parse_json(errors_only(read_lazy("app.log"))):
    save_to_db(event)
yield from — делегирование
def flatten(matrix):
    for row in matrix:
        yield from row              # yield каждого элемента row

# Эквивалент:
# for row in matrix:
#     for x in row:
#         yield x

list(flatten([[1, 2], [3, 4]]))   # [1, 2, 3, 4]
itertools — стандартная библиотека
ФункцияЧто делает
chain(a, b, c)Соединяет несколько iterables в один
islice(it, start, stop, step)Срез iterator'а (нельзя срезать generator обычным [:])
count(start, step)Бесконечный счётчик start, start+step, ...
cycle(iterable)Бесконечно повторяет
repeat(x, n)Повторяет x N раз (или бесконечно)
groupby(iterable, key)Группировка подряд идущих одинаковых (не как SQL GROUP BY)
combinations(it, r)Все сочетания по r элементов
permutations(it, r)Все перестановки
product(a, b)Декартово произведение
batched(it, n) (Python 3.12+)Разбить на батчи размером n
takewhile(pred, it), dropwhileПока условие true / после того как false
from itertools import chain, islice, batched

# Соединить несколько источников
for item in chain(users_db, users_api, users_cache):
    ...

# Взять первые 100 из бесконечного generator
for line in islice(read_lazy("huge.log"), 100):
    ...

# Батчи по 100 — для bulk-INSERT в БД
for chunk in batched(records, 100):
    User.bulk_create(chunk)
Особые случаи
1. Генератор — одноразовый. После полного обхода — пустой. list(gen) дважды — второй раз получишь []. Хочешь повторный обход — заверни в list.
2. Нельзя len(generator). Длина неизвестна. sum(1 for _ in gen) посчитает (но выпьет генератор).
3. Нельзя индексировать gen[5]. Только последовательный доступ. Для 5-го элемента — next(islice(gen, 5, 6)).
4. Забыл, что generator ленивый. gen = (fn(x) for x in items) — fn ещё не вызвана. Ошибки внутри fn вылезут только при обходе, не при создании.
5. yield внутри with-блока и досрочный exit. Если генератор бросили не дочитав — __exit__ вызовется через GC (не сразу). Для файлов/соединений лучше contextmanager обёртка.
Итог: генератор через yield — стандартный питонический способ обработки потоков без загрузки в память. Read → filter → transform → save — пайплайн из генераторов. Для обычных задач хватает yield в def + for; itertools — когда нужны chain/batched/groupby.
Django — «Python Laravel»
Что это

Django (с 2005) — full-stack framework: ORM, миграции, admin-панель, auth, templates, sessions, forms, i18n. По философии — batteries included: почти всё что нужно для сайта, уже внутри.

Аналогия для PHP-разработчика: Django ≈ Laravel в мире Python. ORM ≈ Eloquent, миграции ≈ Laravel migrations, admin ≈ Nova/Filament (только из коробки), templates ≈ Blade, forms ≈ FormRequest. Философия «convention over configuration» тоже похожа.

На чём построены: Instagram, Pinterest, Disqus, Bitbucket, Mozilla, Washington Post. К 2026 — по-прежнему стандарт для «сайта с админкой», но для чистых API проиграл FastAPI.

Быстрый старт
uv add django
uv run django-admin startproject myapp .
uv run python manage.py startapp users
uv run python manage.py migrate
uv run python manage.py runserver
# http://localhost:8000

Структура:

myapp/
├── manage.py                # CLI как artisan
├── myapp/
│   ├── settings.py          # конфиг всего
│   ├── urls.py              # роуты
│   ├── wsgi.py / asgi.py    # entry points для сервера
├── users/                   # app
│   ├── models.py            # модели БД
│   ├── views.py             # контроллеры
│   ├── urls.py
│   ├── admin.py             # регистрация в admin-панели
│   ├── migrations/
│   └── templates/
MVT-паттерн (Model — View — Template)

Django-версия MVC. «View» здесь — не то же, что вью в Laravel: это контроллер. «Template» — HTML-шаблон (Blade-аналог).

# users/models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.name

# users/views.py
from django.shortcuts import render, get_object_or_404
from .models import User

def user_list(request):
    users = User.objects.all()
    return render(request, "users/list.html", {"users": users})

def user_detail(request, pk):
    user = get_object_or_404(User, pk=pk)
    return render(request, "users/detail.html", {"user": user})

# users/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.user_list, name="user_list"),
    path("<int:pk>/", views.user_detail, name="user_detail"),
]

Template (аналог Blade):

<!-- users/templates/users/list.html -->
{% extends "base.html" %}

{% block content %}
<ul>
    {% for user in users %}
        <li><a href="{% url 'user_detail' user.pk %}">{{ user.name }}</a></li>
    {% empty %}
        <li>Пользователей нет</li>
    {% endfor %}
</ul>
{% endblock %}
Django ORM — сила фреймворка
# CRUD
u = User.objects.create(name="Alice", email="a@b.c")
User.objects.get(pk=1)                             # найти один или DoesNotExist
User.objects.filter(name__startswith="A")             # QuerySet
User.objects.exclude(is_active=False)
User.objects.filter(age__gte=18, name__icontains="ali")

# Обновление
u.name = "Bob"
u.save()

# Bulk update
User.objects.filter(is_active=False).update(archived=True)

# Удаление
u.delete()

# Связи (аналог relations в Eloquent)
class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
    title = models.CharField(max_length=200)

# Обход:
user.posts.all()                    # все посты юзера
post.author.name                    # автор поста

# Оптимизация N+1
User.objects.prefetch_related("posts")          # посты одним запросом
Post.objects.select_related("author")             # join с author

# Агрегация
from django.db.models import Count, Sum, Avg
User.objects.annotate(posts_count=Count("posts"))
User.objects.aggregate(total_age=Sum("age"))
Миграции
# Сгенерировать по изменениям в models.py
python manage.py makemigrations
# Migrations for 'users':
#   users/migrations/0002_user_phone.py

# Применить
python manage.py migrate

# Откатить
python manage.py migrate users 0001

# SQL который выполнится
python manage.py sqlmigrate users 0002

В отличие от Laravel — ORM автогенерит миграции сам. Меняешь models.py, запускаешь makemigrations — Django поймёт диф и напишет миграцию.

Admin-панель — killer feature
# users/admin.py
from django.contrib import admin
from .models import User

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "created_at"]
    list_filter = ["is_active"]
    search_fields = ["name", "email"]
    ordering = ["-created_at"]

Одна регистрация — и в /admin/ получаешь готовую CRUD-панель с фильтрами, поиском, пермишенами. Это причина почему Django до сих пор берут для внутренних систем и админок.

Django REST Framework (DRF)

Для API поверх Django ставят DRF — отдельный пакет:

uv add djangorestframework

# users/serializers.py — аналог Pydantic-моделей
from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "name", "email", "created_at"]

# users/views.py
from rest_framework import viewsets
from .models import User
from .serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
# Всё — 5 CRUD endpoints готовы (GET list, POST create, GET/PUT/DELETE detail)
Django vs FastAPI — когда что
ЗадачаDjangoFastAPI
Сайт с админкойИдеальноНе подходит
Внутренняя система с CRUD-формамиОгромная экономия времениБольше кода писать
Публичный REST APIDRF — работает, но многословноСтандарт 2026
Микросервис / AI-backendТяжеловатБыстро и легко
WebSocket / real-timeDjango Channels — через костылиAsync из коробки
Стартап-прототип с сайтомМгновенно рабочий MVPПридётся собирать всё
Особые случаи
1. Django async частичный. В 4.1+ async views есть, но большинство ORM и middleware — sync. Для полноценного async — FastAPI.
2. Fat models vs fat views. В Django принято класть бизнес-логику в модель (User.register(...)), а не в view. Обратно от Laravel «тонкий контроллер + Service».
3. Signals — глобальный event bus. Мощно, но приводит к «магии» — трудно отладить кто на что подписан. Использовать умеренно.
4. N+1 — легко получить. for post in posts: print(post.author.name) — запрос на каждый автор. Спасение — select_related/prefetch_related.
5. QuerySet ленивый. qs = User.objects.filter(...) — SQL ещё не выполнен. Вызовется при for, list(), len(). Кешируется после первого обхода.
FastAPI — must-know backend-фреймворк 2026
Что это и зачем

FastAPI — современный async-first веб-фреймворк на type hints (Python 3.7+, актив с 2018, к 2026 — #1 выбор для нового Python-API). Основан на Starlette (ASGI) + Pydantic (валидация). Даёт из коробки: async endpoints, автоматическая валидация вход/выхода по type hints, автогенерация OpenAPI и Swagger UI, DI через Depends().

Аналогия для PHP-разработчика: FastAPI ≈ Laravel-контроллер + FormRequest + JsonResource + автогенерация Swagger — в одной функции. Type hint параметра = валидация, Pydantic-модель = FormRequest, return type = JsonResource.
Минимальный старт
uv init my-api && cd my-api
uv add fastapi uvicorn[standard]

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"status": "ok"}

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id}
uv run uvicorn main:app --reload
# http://localhost:8000
# http://localhost:8000/docs — Swagger UI автоматом
# http://localhost:8000/redoc — ReDoc
Магия: user_id: int — не просто аннотация. FastAPI сам конвертирует path-параметр в int, валидирует, при ошибке возвращает 422 с понятным JSON. Никакого (int)$request->input() и валидатора руками.
Pydantic — валидация входа
from pydantic import BaseModel, Field, EmailStr

class CreateUserRequest(BaseModel):
    name: str = Field(min_length=2, max_length=50)
    email: EmailStr
    age: int = Field(ge=18, le=120)
    role: Literal["user", "admin"] = "user"

class UserResponse(BaseModel):
    id: int
    name: str
    email: str

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(payload: CreateUserRequest) -> UserResponse:
    # payload — уже провалидирован, все типы правильные
    user = await user_service.create(payload)
    return user      # автоматом сериализуется по UserResponse (hide-hostile поля)

Что делает Pydantic:

  • Приходит {"age": "30"} (string) → сконвертит в int автоматически
  • Приходит {"age": "abc"} → 422 с "value is not a valid integer"
  • Приходит {"email": "not-email"} → 422 с "value is not a valid email"
  • Приходит лишние поля — игнорируются (или бросается ошибка если model_config = {"extra": "forbid"})
  • response_model обрезает вывод — password не утечёт даже если в объекте есть

Laravel-аналог: Pydantic-модель ≈ FormRequest (валидация) + Eloquent (типы) + JsonResource (сериализация) в одном классе.

Query params, path params, body
from fastapi import Query, Path

@app.get("/users/{user_id}/orders")
async def list_orders(
    user_id: int = Path(gt=0),                # из URL
    limit: int = Query(10, ge=1, le=100),      # ?limit=10
    status: str | None = None,                # ?status=paid (опц.)
    tag: list[str] = Query(default_factory=list),  # ?tag=a&tag=b
):
    ...

@app.post("/orders")
async def create_order(payload: OrderRequest):     # Pydantic — из body
    ...

Правило: FastAPI сам различает по типу. Path-параметр {user_id} в URL — берёт оттуда. Простые типы (int, str, bool) — из query. Pydantic-модель — из body JSON.

Depends — Dependency Injection
from fastapi import Depends, HTTPException, status

# Общий provider — сессия БД
async def get_db() -> AsyncSession:
    async with AsyncSession(engine) as session:
        yield session

# Auth-проверка
async def current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    user = await verify_jwt(token, db)
    if not user:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED)
    return user

# Использование — DI по типу параметра
@app.get("/me")
async def read_me(user: User = Depends(current_user)) -> User:
    return user

# Depends можно повесить на роут — auth ко всем эндпоинтам роутера
router = APIRouter(dependencies=[Depends(current_user)])

Как это работает: FastAPI смотрит на аннотации параметров и рекурсивно резолвит зависимости. get_db возвращает через yield → это context manager, сессия закроется после ответа. Кешируется в рамках запроса (одна БД-сессия на весь request).

Структура реального проекта
myapi/
├── pyproject.toml
├── uv.lock
├── .env                       # DATABASE_URL, SECRET_KEY, ...
├── main.py                    # FastAPI() + include_router
├── src/
│   ├── config.py              # pydantic-settings — типизированный env
│   ├── database.py            # engine, get_db()
│   ├── auth/
│   │   ├── router.py          # /login, /register
│   │   ├── schemas.py         # Pydantic-модели входа/выхода
│   │   ├── service.py         # бизнес-логика
│   │   ├── models.py          # SQLAlchemy-модели
│   │   └── deps.py            # current_user, etc
│   ├── users/
│   │   └── ...
│   └── orders/
│       └── ...
└── tests/
    └── ...
# main.py
from fastapi import FastAPI
from src.auth.router import router as auth_router
from src.users.router import router as users_router

app = FastAPI(title="My API", version="1.0")
app.include_router(auth_router, prefix="/auth", tags=["auth"])
app.include_router(users_router, prefix="/users", tags=["users"])
Обработка ошибок
from fastapi import HTTPException

@app.get("/users/{id}")
async def get_user(id: int):
    user = await find(id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

# Глобальный обработчик кастомного исключения
from fastapi.responses import JSONResponse

@app.exception_handler(OrderError)
async def order_error_handler(request, exc: OrderError):
    return JSONResponse(
        status_code=400,
        content={"error": str(exc), "type": exc.__class__.__name__},
    )
FastAPI vs Django vs Flask — выбор
FastAPIDjangoFlask
ТипMicro/API-first, asyncBatteries-includedMicro
AsyncПервого классаЧастично (3.1+)Нет (только через расширения)
ORMНет — свой выбор (SQLAlchemy)Свой мощный ORMНет — Flask-SQLAlchemy
Admin panelНет из коробкиМощнаяНет
ВалидацияPydantic — на type hintsDjango Forms / DRF SerializersОтдельные libs
OpenAPI/SwaggerАвтоматическиЧерез DRF + пакетыНужны пакеты
Кривая обученияБыстро — type hints и всёДолго — много концепцийБыстро — но всё собирать
Когда выбиратьHTTP-API, микросервис, AI-backendСайт с админкой, монолитСкрипт, прототип, legacy
Особые случаи
1. sync-запросы в async endpoint блокируют loop. requests.get(), time.sleep(), синхронный SQLAlchemy — все убивают производительность. Используй httpx.AsyncClient, await asyncio.sleep, SQLAlchemy async.
2. Тяжёлые CPU-операции в endpoint. Хеш паролей (bcrypt), генерация PDF, resizing картинок — всё блокирует event loop. Вынести в await asyncio.to_thread(func) или в отдельный воркер (Celery/RQ).
3. Возврат SQLAlchemy-модели напрямую. Может утечь лишнее, ленивые relations кинут MissingGreenlet. Используй response_model с Pydantic-схемой или model.model_dump().
4. @app.on_event устарел. В новых версиях FastAPI — lifespan context manager: app = FastAPI(lifespan=my_lifespan).
5. Depends — не singleton по умолчанию. Кешируется в рамках одного запроса. Хочешь real singleton — оборачивай в @lru_cache.
6. CORS. Из коробки не разрешён. Для SPA-фронта — app.add_middleware(CORSMiddleware, allow_origins=[...]).
Итог: FastAPI — стандарт для нового Python-API-бекенда. Type hint = валидация + документация. Pydantic — сердце. Depends() — DI. Deploy — uvicorn в Docker + reverse-proxy Nginx. Для сайта с админкой — Django; для микросервиса/AI-бэка/API — FastAPI.
Flask — микрофреймворк
Что это

Flask (с 2010) — микрофреймворк: даёт роутинг + шаблоны + request/response. Всё остальное (ORM, форма, auth) — плагины по выбору. Долгое время был «PHP для Python» — быстро написать что-то маленькое.

К 2026: уступает FastAPI. Для нового кода выбирают FastAPI (async + type hints + автодоки). Flask остался в: legacy-системах, обучении (простой для входа), небольших скриптах-веб-инструментах.

Минимальный старт
uv add flask

# app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello!"

@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
    return jsonify({"id": user_id, "name": "Alice"})

@app.route("/users", methods=["POST"])
def create_user():
    data = request.get_json()          # ручной парсинг
    if not data or "name" not in data:
        return jsonify({"error": "name required"}), 400
    return jsonify({"created": data}), 201

if __name__ == "__main__":
    app.run(debug=True)
python app.py
# http://localhost:5000

Обрати внимание — валидация вручную (сравни с Pydantic в FastAPI, где просто аннотация типа). Это основной минус.

Экосистема плагинов
ЗадачаПлагин
ORMFlask-SQLAlchemy
МиграцииFlask-Migrate (Alembic)
Формы + CSRFFlask-WTF
Auth (сессии)Flask-Login
JWTFlask-JWT-Extended
RESTFlask-RESTful, Flask-Smorest
Admin-панельFlask-Admin

В сумме получается тот же Django, только собираешь его сам из кубиков.

Когда сегодня стоит выбрать Flask
  • Мини-скрипт с 3-5 endpoint'ами (webhook receiver, health-check server)
  • Проект на legacy Python 2 / 3.6 (FastAPI требует 3.7+)
  • Обучение — синтаксис проще чем у FastAPI, меньше «магии»
  • Работаешь в существующем Flask-проекте

Для нового REST-API в 2026 — берут FastAPI, не Flask. Скорость разработки выше за счёт Pydantic и автодоки.

Django vs FastAPI vs Flask — итоговое сравнение
Полная таблица
АспектDjangoFastAPIFlask
ТипFull-stackMicro / API-firstMicro
Год200520182010
ФилософияBatteries includedType-driven, async-firstDo-it-yourself
AsyncЧастично (4.1+)Первого класса(только Quart-fork)
ORM в комплектеDjango ORM(SQLAlchemy отдельно)(Flask-SQLAlchemy отдельно)
Admin-панельИз коробкиFlask-Admin (отдельно)
Auth-системаПолнаяСобирать (FastAPI Users / OAuth2)Flask-Login
ВалидацияDjango Forms / DRF SerializersPydantic на type hintsВручную / Flask-WTF
Автодоки OpenAPIЧерез DRF + drf-spectacularАвтоматическиЧерез плагин Flask-Smorest
TemplatesDjango templatesJinja2 через StarletteJinja2
МиграцииАвтогенерацияAlembic отдельноFlask-Migrate
Скорость (RPS)СредняяВысокая (async)Средняя
Кривая обученияКрутая — много концепцийПологая, но нужны type hintsОчень пологая
Комьюнити 2026Огромное, зрелоеВзрывной рост, активноеСтабильное, но растёт медленно
Кто используетInstagram, Pinterest, RedditNetflix, Uber, MicrosoftНебольшие сервисы, скрипты
Правило выбора

Сайт с админкой, внутренняя система, e-commerce

→ Django. Batteries included окупятся за 1-2 недели: admin, auth, ORM, миграции — всё уже настроено. Не изобретай стек.

Публичный REST API, микросервис, AI-backend

→ FastAPI. Async из коробки, Pydantic для валидации, автогенерация OpenAPI. Стандарт нового Python-backend в 2026.

Мини-скрипт, webhook receiver, prototype

→ Flask — если проект действительно минимальный (3-5 endpoints, никаких перспектив роста). Иначе всё равно FastAPI.

Работа с существующим кодом

→ что уже стоит. Переписывать legacy Flask на FastAPI ради «модно» — редко того стоит. Мигрируй только новые модули.

Гибридный подход в реальности

Крупные компании часто смешивают:

  • Django — «основной сайт» с админкой + бизнес-логика
  • FastAPI — новые публичные API-эндпоинты, микросервисы, ML-inference
  • Общая база данных через одну ORM (обычно SQLAlchemy)

Так на разные задачи используется правильный инструмент, без религиозных войн.

Итог одной фразой: Django для сайтов, FastAPI для API, Flask для мини-задач. В 2026 доля FastAPI растёт быстрее всех остальных Python-фреймворков вместе взятых.
БД в Python — psycopg / SQLAlchemy / Alembic
Три уровня работы с БД
УровеньИнструментыPHP-аналог
1. Сырые SQLpsycopg (PG), mysql-connector, sqlite3 (stdlib)PDO
2. Query Builder + Core ORMSQLAlchemy CoreLaravel Query Builder
3. Full ORMSQLAlchemy ORM, Django ORMEloquent, Doctrine

Для нового backend-проекта на FastAPI — стандарт SQLAlchemy 2.0 async. Для Django-проекта — Django ORM (входит).

Сырые SQL — psycopg

psycopg (v3, 2021) — актуальный драйвер PostgreSQL. Старая версия psycopg2 — legacy, но ещё популярна. Аналог PDO — прямой доступ к SQL.

uv add "psycopg[binary]"

import psycopg

with psycopg.connect("postgresql://user:pass@localhost/mydb") as conn:
    with conn.cursor() as cur:
        # SELECT с параметрами (защита от SQL-injection)
        cur.execute("SELECT id, name FROM users WHERE age > %s", (18,))
        rows = cur.fetchall()          # list of tuples
        for id, name in rows:
            print(id, name)

        # INSERT + RETURNING
        cur.execute(
            "INSERT INTO users (name, email) VALUES (%s, %s) RETURNING id",
            ("Alice", "a@b.c"),
        )
        new_id = cur.fetchone()[0]

    conn.commit()

# Row factory — получить dict вместо tuple
from psycopg.rows import dict_row
with psycopg.connect(DSN, row_factory=dict_row) as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT id, name FROM users")
        for row in cur:
            print(row["name"])
Только параметризация через %s, никогда f-string / concat. cur.execute(f"SELECT ... WHERE id = {user_input}") — SQL-инъекция. Всегда cur.execute("... WHERE id = %s", (user_input,)) — драйвер эскейпит.
SQLAlchemy 2.0 — стандарт для FastAPI

SQLAlchemy — de-facto ORM в Python-экосистеме (кроме Django-проектов). Версия 2.0 (2023) кардинально переработана: новый API, полноценный async, отличные type hints.

uv add "sqlalchemy[asyncio]" "asyncpg"

# models.py — декларация моделей
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, ForeignKey
from datetime import datetime

class Base(DeclarativeBase): pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(100))
    email: Mapped[str] = mapped_column(String(200), unique=True)
    created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# database.py — async engine + session
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

async def get_db():                     # для FastAPI Depends()
    async with AsyncSessionLocal() as session:
        yield session
# Использование в FastAPI
from sqlalchemy import select, insert, update, delete

@app.get("/users/{id}")
async def get_user(id: int, db: AsyncSession = Depends(get_db)):
    # SELECT one
    user = await db.get(User, id)
    if not user:
        raise HTTPException(404)
    return user

@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
    # SELECT many
    stmt = select(User).where(User.name.ilike("a%")).limit(100)
    result = await db.execute(stmt)
    return result.scalars().all()

@app.post("/users")
async def create_user(payload: CreateUserRequest, db: AsyncSession = Depends(get_db)):
    user = User(name=payload.name, email=payload.email)
    db.add(user)
    await db.commit()
    await db.refresh(user)                # подтянуть id, created_at
    return user
Связи и оптимизация N+1
# Связь один-ко-многим
class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(primary_key=True)
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    title: Mapped[str]
    author: Mapped["User"] = relationship(back_populates="posts")

class User(Base):
    ...
    posts: Mapped[list["Post"]] = relationship(back_populates="author")

# Оптимизация — selectinload = prefetch, joinedload = JOIN
from sqlalchemy.orm import selectinload

stmt = select(User).options(selectinload(User.posts))
# Один запрос за юзерами + один за всеми постами (через IN)

# Без — N+1: SELECT users, потом на каждого — SELECT posts
Alembic — миграции

Стандарт для миграций SQLAlchemy. Работает как artisan migrate в Laravel — генерация SQL по diff моделей.

uv add alembic

alembic init migrations              # создать конфиг

# Настроить alembic/env.py — указать target_metadata = Base.metadata

# Автогенерация по diff моделей
alembic revision --autogenerate -m "add users table"

# Применить все миграции
alembic upgrade head

# Откатить одну
alembic downgrade -1

# Показать текущую версию
alembic current

# История
alembic history
Особые случаи
1. Смешивание sync и async в SQLAlchemy. Sync-код (session.query(...)) не работает в async-сессии. Один стек — либо весь sync, либо весь async.
2. Забыл await db.commit(). Изменения не сохранятся. В отличие от Django/Laravel, где транзакция автозакрывается — SQLAlchemy требует явного commit.
3. Ленивая загрузка в async. user.posts без предварительной selectinload в async — MissingGreenlet exception. Async не умеет ленивую подгрузку — грузи заранее.
4. Возврат SQLAlchemy-модели напрямую из FastAPI. Утечка полей + ленивые relations. Используй Pydantic-схему через response_model.
5. Одна сессия на весь запрос. Не создавай AsyncSession() в каждой функции — используй Depends() и один shared через request. Иначе connection pool не окупится.
6. Alembic autogenerate — не серебряная пуля. Не увидит переименование колонки (сгенерит DROP + CREATE — потеряет данные). Проверяй сгенерённые миграции перед применением.
Итог: для FastAPI-стека — SQLAlchemy 2.0 async + Alembic + asyncpg. Для Django — Django ORM. Для мини-скрипта — psycopg напрямую. Всегда параметризованные запросы. В async — selectinload для relations, чтобы избежать MissingGreenlet.
HTTP-клиенты — requests / httpx
requests vs httpx — что выбрать
requestshttpx
Год20112019
APIПростой, стандартныйТакой же — можно мигрировать с sed
AsyncНетSync + Async в одном пакете
HTTP/2НетДа
Timeout по умолчаниюНет (висит навсегда)5 секунд
Проверка типовСлабаяПолные type hints

Для нового кода — httpx. requests — для legacy или совсем маленьких скриптов.

Базовый GET / POST
uv add httpx

import httpx

# GET
r = httpx.get("https://api.example.com/users/1")
r.status_code                       # 200
r.text                              # raw body как строка
r.json()                            # JSON → dict
r.headers["content-type"]

# Query params
r = httpx.get("https://api.ex.com/search", params={"q": "python", "limit": 10})
# автосклейка → ?q=python&limit=10

# POST JSON
r = httpx.post("https://api.ex.com/users", json={"name": "Alice"})
# Content-Type: application/json автоматически

# POST form-data
r = httpx.post(url, data={"name": "Alice"})
# Content-Type: application/x-www-form-urlencoded

# POST file (multipart)
with open("img.png", "rb") as f:
    r = httpx.post(url, files={"image": f})

# Headers
r = httpx.get(url, headers={"Authorization": f"Bearer {token}"})

# Timeout
r = httpx.get(url, timeout=10.0)     # обязательно ставить!

# Обработка ошибок HTTP-статусов
r.raise_for_status()                # бросит HTTPStatusError если 4xx/5xx
Client — переиспользование соединений

Прямой вызов httpx.get() — каждый раз новое TCP-соединение. Для 10+ запросов к одному хосту используй Client:

# Sync client
with httpx.Client(
    base_url="https://api.example.com",
    headers={"Authorization": f"Bearer {token}"},
    timeout=10.0,
) as client:
    r1 = client.get("/users/1")
    r2 = client.get("/users/2")
    r3 = client.post("/users", json={...})
# Соединение переиспользуется, ~10× быстрее
Async — AsyncClient
import asyncio, httpx

async def fetch_all(urls):
    async with httpx.AsyncClient(timeout=10.0) as client:
        tasks = [client.get(u) for u in urls]
        responses = await asyncio.gather(*tasks)
        return [r.json() for r in responses]

data = asyncio.run(fetch_all(["url1", "url2", "url3"]))
# Три запроса параллельно
Retry с tenacity

В httpx/requests нет retry из коробки. Стандарт — библиотека tenacity:

uv add tenacity

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
)
def fetch(url):
    r = httpx.get(url, timeout=5.0)
    r.raise_for_status()
    return r.json()
Mocking в тестах — respx
uv add --dev respx pytest

import respx, httpx, pytest

@respx.mock
def test_fetch_user():
    route = respx.get("https://api.ex.com/users/1").mock(
        return_value=httpx.Response(200, json={"id": 1, "name": "Alice"})
    )
    result = fetch_user(1)
    assert result["name"] == "Alice"
    assert route.called
Особые случаи
1. Забыл timeout. В requests нет дефолта — запрос висит навсегда. В httpx дефолт 5 сек. Всегда указывай явно.
2. Ошибки HTTP-статусов не бросаются автоматически. r = httpx.get(...) с 500 не выкинет exception, только r.status_code == 500. Явно — r.raise_for_status().
3. r.text может не быть JSON. Сначала проверь r.headers.get("content-type"), потом r.json(). Или try/except JSONDecodeError.
4. Sync-клиент в async endpoint (FastAPI). httpx.get() внутри async def блокирует event loop. Используй httpx.AsyncClient.
5. Большие ответы в память. r.content прочитает весь response. Для 10GB-файла — httpx.stream("GET", url) as r: for chunk in r.iter_bytes().
6. Verify SSL. httpx.get(url, verify=False) — отключает проверку сертификата. НИКОГДА в проде, только для тестового localhost.
NumPy — фундамент ML/Data-стека
Что это и зачем

NumPy — библиотека для работы с многомерными массивами (ndarray) и векторизованных вычислений. На нём построены Pandas, scikit-learn, PyTorch, TensorFlow, SciPy. Для ML — обязательный фундамент.

Аналогия: Python-список — «универсальная тележка» (в неё можно положить любую вещь, но проверять тип и обрабатывать каждый элемент нужно вручную). NumPy-массив — «промышленный конвейер»: все элементы одного типа, операции применяются ко всем сразу в C-коде, минуя интерпретатор. Векторизованный a * 2 в 50-100 раз быстрее [x*2 for x in list].
Почему это критично: в ML данные — это матрицы (features × samples), тензоры (batch × height × width × channels). Обучение = миллионы умножений/сложений на массивах. Без векторизации вычисления медленнее в 100+ раз. PyTorch/TensorFlow — та же идея, только с автодифференцированием и GPU.
Установка
uv add numpy

import numpy as np      # общепринятый alias, всегда np
Создание массивов (ndarray)
# Из Python-списка
a = np.array([1, 2, 3, 4])                # 1D — вектор
b = np.array([[1, 2, 3], [4, 5, 6]])     # 2D — матрица
c = np.array([[[1], [2]], [[3], [4]]]) # 3D — тензор

# Из «шаблонов»
np.zeros((3, 4))                       # матрица 3×4 из нулей
np.ones((2, 3))                        # из единиц
np.full((2, 2), 7)                     # заполнить константой
np.eye(3)                              # единичная 3×3
np.arange(0, 10, 2)                    # [0, 2, 4, 6, 8] — как range
np.linspace(0, 1, 11)                  # 11 точек от 0 до 1

# Случайные
np.random.rand(3, 4)                    # uniform [0, 1)
np.random.randn(3, 4)                   # normal (0, 1)
np.random.randint(0, 10, size=(2, 3))    # целые

# Свойства
a.shape                             # (4,) — размеры
b.shape                             # (2, 3)
a.ndim                              # 1 — размерность
b.ndim                              # 2
a.dtype                             # int64 — тип элементов
a.size                              # 4 — общее число элементов
Массивы одного типа. В отличие от list, ndarray хранит элементы одного dtype. np.array([1, "hello"]) сконвертит всё в строки. Задавать явно: np.array([1, 2], dtype=np.float32).
Векторизация — главная идея
a = np.array([1, 2, 3, 4])

# Арифметика — поэлементно, БЕЗ циклов
a * 2              # [2, 4, 6, 8]
a + 10             # [11, 12, 13, 14]
a ** 2             # [1, 4, 9, 16]
a > 2              # [False, False, True, True]  — булева маска

# Между массивами — тоже поэлементно
b = np.array([10, 20, 30, 40])
a + b               # [11, 22, 33, 44]
a * b               # [10, 40, 90, 160]  — НЕ матричное умножение
a @ b               # 300 — скалярное произведение (dot product)

# Универсальные функции (ufuncs) — векторизованные
np.sqrt(a)          # [1., 1.41, 1.73, 2.]
np.exp(a)           # e^x
np.log(a)           # ln
np.sin(a)
np.abs(a)
Скорость: цикл Python по массиву из 1млн — ~200ms. Векторизованный a * 2 — ~2ms. Разница 100×. Не пиши for i in range(len(a)): a[i] += 1 — пиши a += 1.
Broadcasting — арифметика массивов разной формы

NumPy автоматически «растягивает» меньший массив, если операция это допускает. Одно из самых мощных свойств.

a = np.array([[1, 2, 3],       # shape (2, 3)
              [4, 5, 6]])
row = np.array([10, 20, 30])       # shape (3,)

a + row
# [[11, 22, 33],
#  [14, 25, 36]]
# row «растянут» на 2 строки, потом поэлементно

# Классический use-case: нормализация features
# X shape (n_samples, n_features), mean shape (n_features,)
X_normalized = (X - X.mean(axis=0)) / X.std(axis=0)

Правила broadcasting (упрощённо):

  1. Сравнивают формы с конца.
  2. Размерности совпадают → ок.
  3. Одна из размерностей = 1 → «растягивается» до другой.
  4. Иначе — ошибка.
# Совместимые
(3, 4) + (4,)       # (3,4) + (1,4) → (3,4)
(3, 4) + (3, 1)     → (3,4)
(2, 1, 4) + (3, 4)  → (2,3,4)

# Несовместимые — ValueError
(3, 4) + (3,)       операнды не совпадают
Индексация и срезы
a = np.arange(10).reshape(2, 5)
# [[0, 1, 2, 3, 4],
#  [5, 6, 7, 8, 9]]

# Обычный slicing
a[0]                # [0, 1, 2, 3, 4] — вся строка 0
a[0, 2]             # 2 — элемент (row 0, col 2)
a[:, 1]             # [1, 6] — колонка 1 целиком
a[:, 1:3]           # колонки 1..2
a[0, ::2]           # [0, 2, 4] — каждый второй в строке 0

# Fancy indexing — по массиву индексов
a[[0, 1], [2, 3]]     # [2, 8] — a[0,2], a[1,3]

# Булева маска — самый частый паттерн
mask = a > 3            # маска той же формы, что и a
a[mask]                # все элементы > 3 — [4, 5, 6, 7, 8, 9]
a[a % 2 == 0]         # все чётные

# Модификация через маску
a[a < 5] = 0           # все < 5 → 0
Срез — это VIEW, не копия. b = a[:, 1:3]; b[0, 0] = 99 изменит оригинал a. Хочешь копию — явно a[:, 1:3].copy(). В Pandas та же ловушка (SettingWithCopyWarning).
Оси (axis) и агрегации

Ось (axis) — направление, вдоль которого агрегируем. Для 2D-матрицы: axis=0 — по столбцам (сжимает строки), axis=1 — по строкам (сжимает столбцы). Классическая путаница — запоминай: axis = «размерность, которая исчезает».

a = np.array([[1, 2, 3],
              [4, 5, 6]])            # shape (2, 3)

a.sum()                              # 21 — сумма всего
a.sum(axis=0)                        # [5, 7, 9] — по столбцам (shape (3,))
a.sum(axis=1)                        # [6, 15] — по строкам (shape (2,))

# Все агрегации имеют axis
a.mean(axis=0)                       # среднее по столбцам
a.max(axis=1)                        # max в каждой строке
a.std(axis=0)                        # стандартное отклонение
a.argmax(axis=1)                     # индекс максимума в строке
a.cumsum(axis=0)                     # накопительная сумма
Reshape и manipulation
a = np.arange(12)                    # [0, 1, ..., 11]

a.reshape(3, 4)                    # (3, 4)
a.reshape(2, 2, 3)                 # (2, 2, 3)
a.reshape(-1, 3)                   # -1 = «посчитай сам» → (4, 3)

# Транспонирование
b = a.reshape(3, 4)
b.T                                # shape (4, 3)

# Уплощение
b.flatten()                          # копия, 1D
b.ravel()                            # view, 1D (быстрее)

# Добавить размерность
a[np.newaxis, :]                   # (12,) → (1, 12)
a[:, np.newaxis]                   # (12,) → (12, 1)
a[None, :]                          # то же — None = np.newaxis

# Склейка
np.concatenate([a, a], axis=0)       # по оси 0
np.vstack([a, a])                    # вертикально (по строкам)
np.hstack([a, a])                    # горизонтально (по столбцам)
np.stack([a, a])                     # добавляет новую ось

# Split — обратная операция
np.split(a, 3)                      # на 3 равные части
Линейная алгебра
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

A @ B                              # матричное умножение (Python 3.5+)
np.dot(A, B)                        # то же, для legacy
A * B                              # поэлементно (Hadamard), НЕ матричное

np.linalg.inv(A)                    # обратная матрица
np.linalg.det(A)                    # определитель
np.linalg.solve(A, b)                # решить систему A·x = b
np.linalg.eig(A)                    # собственные значения/векторы
np.linalg.norm(a)                   # норма (по умолчанию L2)
Практический пример: нормализация данных
import numpy as np

# 100 samples × 4 features
X = np.random.rand(100, 4)

# Z-score normalization: (x - mean) / std
X_norm = (X - X.mean(axis=0)) / X.std(axis=0)
# mean/std shape (4,) → broadcasting до (100, 4)

# Min-max normalization: (x - min) / (max - min)
X_scaled = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

# Проверка
X_norm.mean(axis=0)                 # ≈ [0, 0, 0, 0]
X_norm.std(axis=0)                  # ≈ [1, 1, 1, 1]

# Разделение на train/test
np.random.seed(42)
indices = np.random.permutation(len(X))
train_idx = indices[:80]
test_idx = indices[80:]

X_train = X[train_idx]
X_test = X[test_idx]
Особые случаи
1. Срез — view, не копия. Модификация среза меняет оригинал. Явно .copy() если нужен независимый массив.
2. a * b vs a @ b. Первое — поэлементное умножение (Hadamard product), второе — матричное. Классическая ошибка при переходе с MATLAB.
3. Целочисленное переполнение. np.int8 вмещает -128..127. np.array([120], dtype=np.int8) + 100 → -36. В ML обычно float32/float64.
4. axis=0 vs axis=1. Путаница почти у всех новичков. Запомни: axis = «размерность, которая исчезает». axis=0 в матрице (rows, cols) — исчезают rows, остаются cols.
5. Python-цикл по ndarray = убийство производительности. Всегда ищи векторизованное решение. Если реально нужен цикл — используй numba с @jit.
6. NaN распространяется. np.array([1, np.nan, 3]).sum() → nan. Для игнорирования: np.nansum, np.nanmean, np.nanstd.
Итог: NumPy — обязательный первый шаг перед ML. Ключевые понятия: ndarray, shape/dtype/axis, векторизация (a * 2 вместо цикла), broadcasting (a + row), reshape/transpose. Pandas и PyTorch — надстройки над этим фундаментом.
Pandas — от базы до реальных задач
Зачем нужен Pandas

Pandas — DataFrame (таблица) + Series (колонка) на основе NumPy. Стандарт для анализа данных, ETL, подготовки датасетов под ML. Умеет читать всё (CSV/Excel/JSON/SQL/Parquet), фильтровать, группировать, объединять, обрабатывать пропуски. Обязательный инструмент data-стека.

  • Backend-разработчику: конвертация CSV↔JSON↔Excel, разовый ETL, отчёты, миграции
  • Для ML: подготовка признаков перед обучением — очистка, преобразование, agrgегация, объединение источников
  • Data-инженеру: пайплайны обработки, Airflow-задачи

Когда НЕ Pandas: production real-time API (тяжёлая инициализация), потоки >RAM (используй Polars / Dask / DuckDB / чанки).

Зачем backend-разработчику Pandas

Полноценный курс по Pandas — отдельная тема (это data-science стандарт). Для backend-разработчика полезно знать минимум — для разовых задач:

  • «Конвертнуть CSV в JSON / из Excel в БД / из БД в отчёт»
  • Быстрое исследование данных перед миграцией
  • Мини-ETL — раз в сутки достать данные, обработать, сохранить
  • Ad-hoc отчёты — «сгруппируй по клиенту, посчитай сумму, отсортируй»

Не для: production real-time API — pandas тяжёлый, для этого SQL/SQLAlchemy.

Установка + первый DataFrame
uv add pandas openpyxl                # openpyxl — для xlsx

import pandas as pd

# Из dict / list
df = pd.DataFrame({
    "name": ["Alice", "Bob", "Charlie"],
    "age": [30, 25, 35],
    "city": ["NY", "LA", "NY"],
})

# Из файлов
df = pd.read_csv("data.csv")
df = pd.read_excel("data.xlsx", sheet_name="Users")
df = pd.read_json("data.json")
df = pd.read_sql("SELECT * FROM users", connection)

# Выгрузка
df.to_csv("out.csv", index=False)
df.to_excel("out.xlsx", index=False)
df.to_json("out.json", orient="records")
df.to_sql("users", connection, if_exists="append")
Просмотр данных
df.head(10)                    # первые 10
df.tail(5)                     # последние 5
df.sample(3)                   # случайные 3
df.shape                        # (rows, cols) — (3, 3)
df.columns                      # Index(['name', 'age', 'city'])
df.dtypes                       # типы колонок
df.info()                     # сводка + типы + non-null count
df.describe()                 # статистика по числовым колонкам
len(df)                        # число строк
Фильтрация и выборка
# Выбрать колонку → Series
df["age"]

# Несколько колонок → DataFrame
df[["name", "age"]]

# Фильтр по условию — как WHERE в SQL
df[df["age"] > 30]
df[(df["age"] > 25) & (df["city"] == "NY")]        # & — AND
df[df["city"].isin(["NY", "LA"])]                    # IN
df[df["name"].str.startswith("A")]                # LIKE

# .query() — SQL-подобный синтаксис
df.query("age > 25 and city == 'NY'")

# Доступ по индексу
df.iloc[0]              # первая строка (по позиции)
df.loc[0, "name"]       # cell — строка 0, колонка name

# Сортировка
df.sort_values("age", ascending=False)
Логические операторы — &/|/~, не and/or/not. Из-за перегрузки операторов Pandas. И скобки вокруг каждого условия обязательны.
Группировка и агрегация — как SQL GROUP BY
# Сколько людей в каждом городе
df.groupby("city").size()
# city
# LA    1
# NY    2

# Средний возраст по городам
df.groupby("city")["age"].mean()

# Несколько агрегатов сразу
df.groupby("city").agg({
    "age": ["mean", "max", "count"],
    "name": "count",
})

# Двойная группировка
df.groupby(["city", "department"])["salary"].sum()
Merge — как JOIN
users = pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})
orders = pd.DataFrame({"user_id": [1, 1, 2], "total": [100, 200, 300]})

# INNER JOIN
pd.merge(users, orders, left_on="id", right_on="user_id")

# LEFT JOIN
pd.merge(users, orders, left_on="id", right_on="user_id", how="left")

# Все виды: 'inner' (default), 'left', 'right', 'outer', 'cross'
Работа с NaN и колонками
# NaN — как NULL в SQL
df.isna()                    # булевая маска — где NaN
df.isna().sum()              # сколько NaN по каждой колонке
df.dropna()                  # удалить строки с NaN
df.fillna(0)                 # заменить на 0
df["age"].fillna(df["age"].mean())    # заменить на среднее

# Новая колонка на основе других
df["is_adult"] = df["age"] >= 18
df["full"] = df["name"] + " (" + df["city"] + ")"

# apply — применить функцию
df["age_group"] = df["age"].apply(lambda x: "adult" if x >= 18 else "minor")

# Удалить колонку
df = df.drop(columns=["city"])
df = df.drop([0, 2])                        # удалить строки 0 и 2

# Переименовать
df = df.rename(columns={"age": "years"})
Особые случаи
1. SettingWithCopyWarning. df[df.age > 18]["name"] = "X" — модифицирует копию, не оригинал. Пиши через .loc: df.loc[df.age > 18, "name"] = "X".
2. Pandas в памяти держит всё сразу. Для 10GB CSV — не влезет. Читай кусками: pd.read_csv(..., chunksize=100_000) — генератор по чанкам.
3. df.iterrows() — медленно. Обход по строкам в 100 раз медленнее векторных операций. Используй apply или векторизацию.
4. Не используй Pandas в HTTP-endpoint. 200MB импорт + инициализация — не для request-response. Для API — SQL/SQLAlchemy.
5. Даты — pd.to_datetime(). После read_csv даты часто строки. Явно приведи: df["date"] = pd.to_datetime(df["date"]).
Итог: для backend — Pandas это «SQL для CSV/Excel/JSON». Знать read_csv/to_csv, фильтрацию, groupby, merge, fillna. Хватит на 95% разовых задач. Для ML — обязательно + concat, pivot, работа с датами, оптимизация типов. Для production real-time — оставь Pandas data-инженерам.
matplotlib и seaborn — визуализация
Что и зачем

В ML/DS 90% работы — понимать данные. Гистограмма распределения, scatter корреляций, heatmap важностей, learning curves — без графиков вслепую. Стандартные библиотеки:

  • matplotlib (с 2003) — фундамент, низкоуровневый API, полный контроль. Все другие библиотеки под капотом рисуют через matplotlib.
  • seaborn — обёртка над matplotlib, красивые дефолты, статистические графики (regplot, boxplot, heatmap) из коробки.
  • plotly — интерактивные графики (zoom, hover), для веба и дашбордов.
Установка
uv add matplotlib seaborn

import matplotlib.pyplot as plt      # стандартный alias
import seaborn as sns
import numpy as np
Базовые графики matplotlib
# Простой line-plot
x = np.linspace(0, 10, 100)
y = np.sin(x)

plt.plot(x, y)
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.title("Sine wave")
plt.grid(True)
plt.show()

# Несколько линий
plt.plot(x, np.sin(x), label="sin")
plt.plot(x, np.cos(x), label="cos")
plt.legend()

# Гистограмма — распределение
data = np.random.randn(1000)
plt.hist(data, bins=30, edgecolor="black")

# Scatter — корреляции
plt.scatter(x, y, c="red", alpha=0.5, s=30)

# Bar chart
plt.bar(["A", "B", "C"], [10, 20, 15])
Subplot — несколько графиков
fig, axes = plt.subplots(2, 2, figsize=(10, 8))

axes[0, 0].plot(x, np.sin(x))
axes[0, 0].set_title("sin")

axes[0, 1].plot(x, np.cos(x))
axes[0, 1].set_title("cos")

axes[1, 0].hist(data)
axes[1, 1].scatter(x, y)

plt.tight_layout()             # auto-spacing
plt.savefig("plots.png", dpi=150)
plt.show()
seaborn — статистика из коробки
import seaborn as sns
import pandas as pd

sns.set_theme(style="whitegrid")      # красивая сетка

# Данные
tips = sns.load_dataset("tips")          # встроенный dataset

# Scatter с регрессией одной строкой
sns.regplot(data=tips, x="total_bill", y="tip")

# Boxplot — распределение по категориям
sns.boxplot(data=tips, x="day", y="total_bill")

# Violin plot — boxplot + KDE
sns.violinplot(data=tips, x="day", y="tip", hue="sex")

# Heatmap — корреляция признаков (ML-must)
sns.heatmap(df.corr(), annot=True, cmap="coolwarm", fmt=".2f")

# Pairplot — все пары признаков сразу
sns.pairplot(tips, hue="sex")

# Гистограмма + KDE
sns.histplot(tips, x="total_bill", kde=True, bins=30)

# Countplot — счёт по категориям (как bar от Pandas)
sns.countplot(data=tips, x="day", hue="sex")
ML-must-know: 4 графика для EDA
ГрафикЗачем в ML
Histogram / KDEРаспределение признака. Гауссово? Skewed? Нужна ли нормализация / логарифмирование?
Heatmap correlations (df.corr())Найти мультиколлинеарность — если два признака сильно коррелируют, один можно выкинуть
Scatter target vs featureЕсть ли линейная зависимость? Выбросы?
Learning curves (train/val loss vs epoch)Overfit? Underfit? Пора остановить обучение?
Практический пример: EDA
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df = pd.read_csv("data.csv")

# 1. Обзор данных
df.info()
df.describe()

# 2. Распределения всех числовых
df.hist(figsize=(12, 8), bins=30)
plt.tight_layout()

# 3. Heatmap корреляций
plt.figure(figsize=(10, 8))
sns.heatmap(df.corr(numeric_only=True), annot=True, cmap="coolwarm", center=0)

# 4. Отношения target ↔ признаки
sns.pairplot(df, hue="target", diag_kind="kde")

# 5. Проверка баланса классов
sns.countplot(data=df, x="target")
Особые случаи
1. plt.show() в скрипте / %matplotlib inline в ноутбуке. В Jupyter графики рендерятся inline автоматически с 3+, показ plt.show() необязателен.
2. Не забывай plt.figure(figsize=...) — иначе всё будет квадратные микро-графики 6×4.
3. Легенды и подписи осей. Без них график непонятен спустя неделю. Дисциплина: всегда xlabel + ylabel + title.
4. Не смешивай plt и axes API в одной ячейке. Или plt.plot()/plt.title() (state-based), или fig, ax = plt.subplots(); ax.plot(); ax.set_title() (object-based). Второй чище для сложных фигур.
5. plt.close("all") в конце скриптов — иначе память с figures копится, особенно в цикле.
Итог: для EDA в ML — seaborn (быстрые красивые графики) поверх matplotlib. Must-know: hist, scatter, heatmap, pairplot. Для production-дашбордов — plotly. Инлайн-графики в Jupyter — стандартный workflow ML.
Jupyter Notebooks — рабочая среда ML
Что это и зачем

Jupyter Notebook — интерактивная среда, где код разбит на ячейки, каждая выполняется отдельно, состояние переменных сохраняется между ячейками. Файл .ipynb — JSON с ячейками (код + markdown + результаты выполнения). Для ML — стандартная среда, а не .py-скрипты.

Аналогия: обычный Python-скрипт — «спектакль», который прогоняешь от начала до конца. Jupyter — «ремонтная мастерская»: разложил детали на верстаке, вертишь каждую, смотришь, правишь. Загрузил датасет один раз, потом 50 ячеек экспериментов с ним.

Почему в ML именно так:

  • Загрузка данных занимает 10-60 секунд — глупо перезапускать при каждой правке
  • Обучение модели — минуты/часы, тоже не хочешь повторять при исследовании результата
  • Графики видны сразу под кодом, легко сравнивать эксперименты
  • Комбинация кода + markdown + графиков = живой отчёт для команды
Установка + запуск
uv add jupyter

# Классический Jupyter
uv run jupyter notebook

# JupyterLab (современный UI)
uv add jupyterlab
uv run jupyter lab

# Или просто открой .ipynb в VS Code — встроенная поддержка
# PyCharm Professional тоже умеет

Cloud-варианты (без установки):

  • Google Colab (colab.research.google.com) — бесплатно, GPU/TPU по подписке. Стандарт для learning.
  • Kaggle Notebooks — с встроенным доступом к датасетам
  • Deepnote, Databricks — enterprise-платформы
Основные горячие клавиши
КлавишаЧто делает
Shift + EnterВыполнить ячейку, перейти к следующей
Ctrl + EnterВыполнить, остаться на этой
Alt + EnterВыполнить + вставить новую ячейку снизу
EscВыйти из режима редактирования (command mode)
A / B (в command mode)Вставить ячейку выше / ниже
DDУдалить ячейку
M / YПереключить в markdown / в code
ZUndo удаления ячейки
Ctrl + /Комментировать/раскомментировать строку
TabАвтокомплит
Shift + TabПоказать docstring функции под курсором
Магические команды (magics)
# %% — cell magic, применяется к всей ячейке
# % — line magic, к одной строке

%time         expensive_function()      # замерить время одной операции
%%time                                    # замерить время ВСЕЙ ячейки
stuff()
more_stuff()

%timeit expensive()                      # усреднение по N запусков (для микро-бенчей)

%matplotlib inline                        # графики inline (default в новых Jupyter)
%matplotlib widget                        # интерактивные

%load_ext autoreload                      # авто-перезагрузка своих модулей
%autoreload 2

%who                                      # список переменных в namespace
%whos                                     # с типами и деталями
%reset                                    # очистить весь namespace

# Запуск shell-команд — префикс !
!ls -la
!pip list
!git status

# Присвоить результат shell в переменную
files = !ls *.csv
Типичный workflow ML-исследования
# Ячейка 1 — импорты (один раз)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Ячейка 2 — загрузка данных (тяжёлая, один раз)
df = pd.read_csv("train.csv")
df.shape

# Ячейка 3 — беглый обзор
df.head()

# Ячейка 4 — распределения
df.hist(figsize=(12, 8))
plt.tight_layout()

# Ячейка 5 — корреляции
sns.heatmap(df.corr(numeric_only=True), annot=True)

# Ячейка 6 — feature engineering
df["age_group"] = pd.cut(df["age"], bins=[0, 18, 65, 100])

# Ячейка 7 — обучение модели
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier()
model.fit(X_train, y_train)

# Ячейка 8 — оценка
score = model.score(X_test, y_test)
print(f"Accuracy: {score:.3f}")

Данные загружены один раз (ячейка 2). Экспериментируешь в ячейках 4-8 без повторной загрузки. Обучил модель раз — тестируешь на разных данных, не переобучая.

Особые случаи
1. Скрытое состояние. Ячейки можно выполнять в любом порядке — переменная x может быть от ячейки 5, а не от ячейки 3. Заканчивая работу: Kernel → Restart & Run All — проверить что notebook воспроизводим сверху вниз.
2. Утечка памяти при переменных. Тяжёлый DataFrame в переменной живёт до перезапуска kernel. del df; import gc; gc.collect() — если нужно освободить.
3. Git diff нечитаем. .ipynb — JSON с outputs. Diff в PR — ад. Решения: nbstripout (убирает outputs при commit), jupytext (парная .py-версия для diff), ReviewNB.
4. Notebook — не production. Для реального deploy — переноси код в .py-модули, notebook оставь для experiments. Не запускать jupyter nbconvert --execute prod.ipynb в cron — хрупко.
5. Долгие вычисления без прогресса. Для циклов используй from tqdm.auto import tqdm; for x in tqdm(items): — прогресс-бар.
6. Секреты в ноутбуке. API-ключ прямо в ячейке = утечёт в git. Читай из .env: from dotenv import load_dotenv; load_dotenv(); os.getenv("API_KEY").
Итог: для ML — Jupyter Notebook (или Colab) стандарт. Ячейки + Shift+Enter, magic-команды (%time, %matplotlib inline), tqdm для прогресса. Для production — переноси в модули. Git через nbstripout / jupytext.
Алгоритмические паттерны на массивах
Зачем это знать

В алгоритмических задачах (особенно ML/DS) и в тестах — обязательный навык. Задача: «найди пару чисел с суммой X», «максимальную подстроку без повторов», «скользящее окно из K чисел». Без знания паттернов пишешь O(n²) там, где нужен O(n).

Ключевая идея: вместо вложенных циклов держи дополнительное состояние (индекс, сумму, счётчик, словарь) и обрабатывай массив за один проход.
1. Проход с накоплением (running sum)

Считать что-то по мере прохода: сумму, произведение, максимум, счётчик.

# Задача: сумма всех положительных
def sum_positive(nums):
    total = 0
    for n in nums:
        if n > 0:
            total += n
    return total

# Задача: максимальная сумма подряд идущих (Kadane's algorithm)
def max_subarray_sum(nums):
    max_sum = current = nums[0]
    for n in nums[1:]:
        current = max(n, current + n)
        max_sum = max(max_sum, current)
    return max_sum

max_subarray_sum([-2, 1, -3, 4, -1, 2, 1, -5, 4])   # 6 (от [4,-1,2,1])
2. Два указателя (two pointers)

Два индекса, движущиеся навстречу или в одном направлении. Заменяет вложенный цикл O(n²) на O(n).

# Задача: есть ли в отсортированном массиве пара с суммой = target
def has_pair_sum(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        s = nums[left] + nums[right]
        if s == target:
            return True
        elif s < target:
            left += 1        # нужна большая сумма → сдвиг вправо
        else:
            right -= 1       # нужна меньшая → сдвиг влево
    return False

has_pair_sum([1, 2, 4, 7, 11, 15], 9)     # True (2 + 7)

# Задача: удалить дубликаты in-place
def remove_duplicates(nums):
    if not nums:
        return 0
    write = 1
    for read in range(1, len(nums)):
        if nums[read] != nums[read - 1]:
            nums[write] = nums[read]
            write += 1
    return write

# Задача: разворот массива in-place
def reverse(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        nums[left], nums[right] = nums[right], nums[left]
        left += 1
        right -= 1
3. Скользящее окно (sliding window)

Два указателя движутся в одном направлении, поддерживая «окно» — подмассив/подстроку. Классика для «максимум/минимум/сумма подмассива длины K», «подстрока с условием».

# Задача: максимальная сумма подмассива длины K
def max_sum_window(nums, k):
    window = sum(nums[:k])
    max_sum = window
    for i in range(k, len(nums)):
        window += nums[i] - nums[i - k]     # сдвинули окно на 1
        max_sum = max(max_sum, window)
    return max_sum

max_sum_window([1, 4, 2, 10, 23, 3, 1, 0, 20], 4)   # 39

# Задача: длина максимальной подстроки без повторов
def longest_unique(s):
    seen = {}                       # char → last index
    left = 0
    best = 0
    for right, ch in enumerate(s):
        if ch in seen and seen[ch] >= left:
            left = seen[ch] + 1     # сдвинуть окно после дубликата
        seen[ch] = right
        best = max(best, right - left + 1)
    return best

longest_unique("abcabcbb")         # 3 (abc)
longest_unique("pwwkew")           # 3 (wke)
4. Prefix sum — предвычисление префиксных сумм

Заранее считаем массив префиксных сумм. Потом сумма любого подмассива [i..j] — за O(1) вместо O(j-i).

nums = [1, 2, 3, 4, 5]
prefix = [0]                       # prefix[i] = sum(nums[:i])
for n in nums:
    prefix.append(prefix[-1] + n)
# prefix = [0, 1, 3, 6, 10, 15]

# Сумма nums[1..3] (индексы включительно) — за O(1)
prefix[4] - prefix[1]              # 10 - 1 = 9 (2+3+4)

# Полезно если запросов сумм МНОГО — предвычислили один раз
5. Hash-map + один проход

Классический паттерн «two sum» — за один проход через словарь. Меняет O(n²) на O(n) за счёт O(n) памяти.

# Задача: индексы двух чисел с суммой = target (LeetCode #1)
def two_sum(nums, target):
    seen = {}                       # value → index
    for i, n in enumerate(nums):
        need = target - n
        if need in seen:
            return [seen[need], i]
        seen[n] = i
    return []

two_sum([2, 7, 11, 15], 9)      # [0, 1]

# Задача: подсчёт частот
from collections import Counter
freq = Counter(["a", "b", "a", "c", "a"])
# Counter({'a': 3, 'b': 1, 'c': 1})
freq.most_common(2)              # [('a', 3), ('b', 1)]
6. Fast/slow pointers (заяц и черепаха)

Два указателя с разной скоростью. Найти середину списка, обнаружить цикл в связном списке, найти дубликат в массиве.

# Задача: найти дубликат в массиве где числа 1..n, размер n+1
def find_duplicate(nums):
    slow = fast = nums[0]
    while True:
        slow = nums[slow]
        fast = nums[nums[fast]]
        if slow == fast:
            break
    # Найти вход в цикл
    slow = nums[0]
    while slow != fast:
        slow = nums[slow]
        fast = nums[fast]
    return slow

find_duplicate([1, 3, 4, 2, 2])   # 2
7. Бинарный поиск
# Индекс в отсортированном массиве
def bsearch(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# Готовое из stdlib — bisect
import bisect
bisect.bisect_left([1, 3, 5, 7], 3)      # 1 — куда вставить 3
bisect.insort(a, x)                             # вставить с сохранением сортировки
Cheatsheet: какой паттерн под какую задачу
ЗадачаПаттернСложность
«Есть ли пара с суммой X» (сортированный)Два указателяO(n)
«Есть ли пара с суммой X» (произвольный)Hash-mapO(n) time, O(n) memory
«Максимум/минимум подмассива длины K»Sliding windowO(n)
«Подстрока/подмассив с условием»Sliding window + hashO(n)
«Сумма любого подмассива» (много запросов)Prefix sumO(n) prep + O(1) query
«Топ-K частых»Counter.most_commonO(n log k)
«Индекс в сортированном»Binary search / bisectO(log n)
«Цикл в связном списке / дубликат»Fast/slow pointersO(n) time, O(1) memory
«Максимальная сумма подряд»Kadane (running max)O(n)
Питоничные trick'и
# Обмен переменных без временной
a, b = b, a

# Развёртка сложных данных
first, *rest = [1, 2, 3, 4]     # first=1, rest=[2,3,4]
first, *middle, last = range(10)    # first=0, middle=[1..8], last=9

# Одна строка для условной вставки
result = [x if x > 0 else 0 for x in nums]

# zip для параллельного обхода
for a, b in zip(nums, nums[1:]):     # пары соседей
    ...

# Обход в обратном
for i in range(len(nums) - 1, -1, -1):
    ...
# или
for x in reversed(nums):
    ...

# Проверка на палиндром одной строкой
s == s[::-1]

# Уникальные + отсортированные + сохранить порядок первых вхождений
from collections import OrderedDict
list(OrderedDict.fromkeys([3, 1, 3, 2, 1]))   # [3, 1, 2]

# Инициализация словаря значениями по умолчанию
from collections import defaultdict
counts = defaultdict(int)              # без KeyError
groups = defaultdict(list)             # группировка
for item in items:
    groups[item.category].append(item)
Особые случаи
1. Слайс — копия. nums[:] создаёт новый список. Для больших массивов дорого. Итерируй через индексы или используй view (numpy).
2. Модификация массива при итерации. for x in nums: nums.remove(x) — пропустит элементы. Итерируй по копии либо строй новый список.
3. Целочисленное деление. mid = (l + r) // 2 — двойной слэш. Одинарный / в Python 3 всегда float.
4. Переполнение — нет. Python int безграничен. В отличие от C/Java (l + r) / 2 не переполнится. Всё равно пиши l + (r - l) // 2 для переноса кода на другие языки.
5. Пустой массив. Всегда проверяй edge case: if not nums: return. Ошибки типа max([]) = ValueError.
6. Комментировать сложность. В решении пиши # Time: O(n), Space: O(1). Показывает что понимаешь что написал.
Итог: запомни 6 паттернов и когда каждый применять — покрывает 80% алгоритмических задач на массивах / строках. Ключевая мысль: вместо вложенных циклов — одно дополнительное состояние (указатель, словарь, окно). Питонические trick'и (zip, enumerate, Counter, defaultdict, bisect) экономят строки и делают решение читаемее.
pytest + fixtures
Почему pytest, а не unittest

# unittest — многословно import unittest class TestMath(unittest.TestCase): def test_add(self): self.assertEqual(1 + 1, 2) # pytest — просто def test_add(): assert 1 + 1 == 2

Установка + структура
uv add --dev pytest pytest-cov

# Структура проекта
myapp/
├── src/
│   └── calc.py
└── tests/
    ├── test_calc.py           # файлы test_*.py или *_test.py
    └── conftest.py            # общие fixtures

# Запуск
uv run pytest                    # все тесты
uv run pytest tests/test_calc.py # конкретный файл
uv run pytest -k "test_add"       # по имени
uv run pytest -v                 # verbose
uv run pytest -x                 # остановиться на первой ошибке
uv run pytest --pdb              # дебаггер при падении
Тесты и assert
# tests/test_calc.py
from src.calc import add, divide

def test_add_positive():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-2, 3) == 1

# Ожидание исключения
import pytest

def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_divide_by_zero_message():
    with pytest.raises(ValueError, match="cannot divide"):
        divide(10, 0)

# Приблизительное сравнение float
def test_float():
    assert 0.1 + 0.2 == pytest.approx(0.3)

# Пропуск тестов
@pytest.mark.skip(reason="not implemented yet")
def test_todo(): ...

@pytest.mark.skipif(sys.version_info < (3, 11), reason="needs 3.11+")
def test_new_syntax(): ...
Fixtures — заменяют setUp/tearDown
# tests/conftest.py — общие fixtures
import pytest

@pytest.fixture
def sample_user():
    return {"name": "Alice", "age": 30}

@pytest.fixture
def db_session():
    # setup
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    session = Session(engine)

    yield session               # тут выполняется тест

    # teardown
    session.close()
    engine.dispose()

# Тест получает fixture через параметр — DI
def test_user_name(sample_user):
    assert sample_user["name"] == "Alice"

def test_db(db_session):
    user = User(name="Bob")
    db_session.add(user)
    db_session.commit()
    assert user.id is not None

# Scope — как долго живёт fixture
@pytest.fixture(scope="function")   # default — на каждый тест
@pytest.fixture(scope="module")     # один раз на модуль
@pytest.fixture(scope="session")    # один раз на всю сессию
Параметризация
@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (100, 200, 300),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

# Запустится 4 раза, каждый — отдельный тест в отчёте

# С id для читаемых имён
@pytest.mark.parametrize("email, valid", [
    ("a@b.c", True),
    ("nope", False),
    ("", False),
], ids=["valid", "no-at", "empty"])
def test_email(email, valid):
    assert is_valid_email(email) == valid
Mocking через unittest.mock
from unittest.mock import Mock, MagicMock, patch

# Простой mock
mock = Mock()
mock.some_method.return_value = "hello"
mock.some_method()                # "hello"
mock.some_method.assert_called_once_with()

# patch — подменить объект в модуле
@patch("src.services.email.send")
def test_register(mock_send):
    mock_send.return_value = True
    register_user("Alice")
    mock_send.assert_called_once()

# patch как context manager
def test_register_ctx():
    with patch("src.services.email.send") as mock_send:
        register_user("Alice")
        mock_send.assert_called_once()
Патчить надо там где ИСПОЛЬЗУЕТСЯ, не где ОБЪЯВЛЕНО. register_user внутри делает from src.email import send; send(...) — @patch("src.email.send") НЕ сработает. Патчи @patch("src.users.send") — там где импортировано.
Async-тесты и FastAPI
uv add --dev pytest-asyncio httpx

# tests/test_api.py
import pytest
from httpx import AsyncClient
from main import app

@pytest.mark.asyncio
async def test_create_user():
    async with AsyncClient(app=app, base_url="http://test") as client:
        r = await client.post("/users", json={"name": "Alice", "email": "a@b.c"})
        assert r.status_code == 201
        assert r.json()["name"] == "Alice"

В pyproject.toml:

[tool.pytest.ini_options]
asyncio_mode = "auto"              # тогда @mark.asyncio не нужен
Coverage
uv run pytest --cov=src --cov-report=term-missing
# покажет процент покрытия + строки без тестов

uv run pytest --cov=src --cov-report=html
# сгенерит htmlcov/ с интерактивным HTML-отчётом
Особые случаи
1. test_ префикс обязателен. pytest автоматически находит функции test_*. Опечатался в tests_* — тест не запустится.
2. Fixture без параметра — не вызовется. def test_x(): без параметра sample_user — fixture не будет использована.
3. Тесты должны быть независимы. Один тест не должен полагаться на состояние от другого. Fixtures с правильным scope сбрасывают состояние.
4. Не тестировать реальные внешние API. Медленно, flaky. Мокируй через respx для HTTP, @patch для остального.
5. tmp_path fixture — встроенная в pytest, даёт временную директорию, автоматически чистится. Не создавай файлы в /tmp руками.
Итог: pytest — простой синтаксис, мощные fixtures. Файлы test_*.py, функции test_*, обычный assert. @parametrize для табличных тестов. @patch/respx для моков. Плюс pytest-cov для покрытия.
Логирование — модуль logging
Забудь про print() в проде

print() ходит в stdout без метаданных (когда? что за модуль? какой уровень?). В проде — обязательно logging: уровни, timestamps, форматирование, разные handlers (файл + консоль + Sentry), config без правки кода.

Базовое использование
import logging

logger = logging.getLogger(__name__)      # имя = имя модуля

logger.debug("detailed info")                   # 10 — только для разработки
logger.info("user created")                    # 20 — обычные события
logger.warning("deprecated call")              # 30 — стоит обратить внимание
logger.error("failed to save")                 # 40 — операция провалилась
logger.critical("db down")                    # 50 — сервис умирает

# Форматирование через параметры (не f-string!)
logger.info("user %s created with id %d", name, user_id)

# Исключение — с traceback
try:
    something()
except Exception:
    logger.exception("unexpected")     # == error() + traceback
Не используй f-string в логах. logger.info(f"user {name}") сформирует строку ВСЕГДА, даже если этот уровень отключён. logger.info("user %s", name) — форматирование только когда handler решил залогировать. На горячем пути экономия времени.
Настройка через dictConfig
import logging.config

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        },
        "json": {
            "()": "pythonjsonlogger.jsonlogger.JsonFormatter",
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "standard",
            "level": "INFO",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": "/var/log/app.log",
            "maxBytes": 10 * 1024 * 1024,   # 10 MB
            "backupCount": 5,                    # хранить 5 файлов
            "formatter": "json",
            "level": "WARNING",
        },
    },
    "loggers": {
        "": {                                # root logger
            "handlers": ["console", "file"],
            "level": "INFO",
        },
        "myapp.db": {                        # специфичный логгер
            "level": "DEBUG",
        },
    },
}

logging.config.dictConfig(LOGGING)
Структурированные логи (JSON)

Для сборки в ELK / Loki / Datadog — логи в JSON. Каждая строка — валидный JSON, поля удобно фильтровать.

uv add python-json-logger

# Уже настроен в LOGGING выше (formatters.json)
# Дополнительные поля — через extra:
logger.info("user created", extra={"user_id": 42, "ip": "1.1.1.1"})
# {"asctime": "...", "levelname": "INFO", "name": "myapp", 
#  "message": "user created", "user_id": 42, "ip": "1.1.1.1"}
Красивый вывод для разработки — rich
uv add rich

from rich.logging import RichHandler

logging.basicConfig(
    level="INFO",
    format="%(message)s",
    handlers=[RichHandler()],
)
# В консоли — цветные уровни, красивые traceback, таблицы для extra полей
Особые случаи
1. logger = logging.getLogger(__name__) — во ВСЕХ модулях. Так каждый модуль пишет со своим именем, и можно фильтровать в конфиге myapp.users.service.
2. Не логгировать secrets. Пароли, токены, номера карт — никогда в логи. Middleware / фильтр должен вырезать.
3. Не крутить logger.debug() на горячем пути. Даже если DEBUG отключён — Python вычислит аргументы. Оборачивай в if logger.isEnabledFor(logging.DEBUG):.
4. RotatingFileHandler и Docker. В контейнерах пиши в stdout/stderr — Docker/K8s сами соберут. Файлы = нет reproducibility.
5. FastAPI/Django — свой logger. Uvicorn пишет свой access-log, Django свой. Настраивай их отдельно, не переопределяй root грубо.
ruff / black / mypy — инструменты качества
Стандартный tooling 2026
ИнструментРоль
ruffЛинтер + форматтер. На Rust, 10-100× быстрее flake8. К 2026 заменил flake8, isort, pylint, pyupgrade.
mypyСтатический тайп-чекер. Проверяет type hints, находит ошибки до runtime.
blackАвтоформатирование «no options». Стандарт много лет — но ruff format его вытесняет (совместимый).
pre-commitХуки для запуска ruff/mypy/black перед git commit.

Мнемоника: ruff — стилистика + мелкие баги; mypy — типы; black/ruff format — форматирование.

Ruff — линтер + форматтер
uv add --dev ruff

# Линт
uv run ruff check src/
uv run ruff check src/ --fix          # автофиксы

# Форматирование (замена black)
uv run ruff format src/

# Конфиг — pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py313"
extend-exclude = ["migrations"]

[tool.ruff.lint]
select = [
    "E",      # pycodestyle errors
    "F",      # pyflakes
    "I",      # isort — сортировка импортов
    "UP",     # pyupgrade — новый синтаксис
    "B",      # bugbear — типовые баги
    "SIM",    # simplify
    "N",      # PEP 8 naming
]
ignore = ["E501"]                # line too long — ruff format сам разрулит

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]           # asserts разрешены в тестах
mypy — статические типы
uv add --dev mypy

uv run mypy src/                       # проверить

# Конфиг — pyproject.toml
[tool.mypy]
python_version = "3.13"
strict = true                        # включает все проверки
warn_unused_ignores = true
warn_return_any = true

# Для сторонних библиотек без стабов
[[tool.mypy.overrides]]
module = ["legacy_lib.*", "untyped_pkg.*"]
ignore_missing_imports = true

На существующем проекте strict = true сразу — больно. Включай постепенно: сначала для новых модулей через per-module overrides, потом расширяй.

pre-commit — автоматизация
uv add --dev pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy

# Установить хуки
uv run pre-commit install

# Теперь при каждом `git commit` — прогонятся автоматом
В CI (GitHub Actions)
# .github/workflows/lint.yml
name: lint
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv sync --dev
      - run: uv run ruff check src/
      - run: uv run ruff format --check src/
      - run: uv run mypy src/
      - run: uv run pytest --cov
Особые случаи
1. Не конфликтуют ли ruff и black? Нет — ruff format написан как замена black с той же логикой. Держи одно (лучше ruff).
2. mypy strict на legacy проекте — фиаско. Могут выпасть тысячи ошибок за час. Включай постепенно, файл за файлом.
3. Игнорировать надо явно. # noqa: E501 для ruff, # type: ignore[assignment] для mypy. Всегда указывай КОД ошибки, не голое noqa — иначе спрячешь новые баги.
4. ruff --fix — автозамены. Просмотри diff перед commit — иногда меняет семантику (например, UP-правила модернизирующие синтаксис).
5. IDE интеграция. VS Code — расширения Ruff и Pylance. PyCharm — встроено. Настрой format-on-save.
Итог: для нового проекта — ruff + mypy + pre-commit в pyproject.toml. В CI — те же три команды. Всё быстро (ruff на Rust), настройка в одном файле, стандарт индустрии 2026.
Частые вопросы — Python для backend
Топ-25 вопросов на middle/senior Python-backend

1. Что такое GIL?

Global Interpreter Lock — mutex в CPython, разрешающий выполнять Python-bytecode только одному потоку в один момент. Из-за него threading не даёт истинного параллелизма для CPU-bound задач (для этого — multiprocessing). Для I/O-bound работает (поток отпускает GIL на I/O). В Python 3.13 экспериментально можно собирать без GIL, но production ещё далеко.

2. Mutable vs immutable — почему важно

Immutable: int, float, str, tuple, bool, None, frozenset. Mutable: list, dict, set. Immutable — можно использовать как ключи в dict/set (потому что есть __hash__). Mutable default arg в функции — классический баг (создаётся ОДИН раз при определении, разделяется между вызовами).

3. list vs tuple

Tuple неизменяем, немного быстрее, может быть ключом dict. Семантически — «фиксированный набор» (координаты, RGB). List — «однородная коллекция, длина меняется» (список пользователей).

4. == vs is

== — сравнение значений (__eq__). is — сравнение идентичности (тот же объект в памяти). Для None — только x is None. Small int'ы (-5..256) и короткие строки кешируются, поэтому a = 5; b = 5; a is b — True, но это implementation detail.

5. *args и **kwargs

Переменное число аргументов. *args — tuple позиционных, **kwargs — dict именованных. Также используются для «прозрачного прокси»: def wrapper(*a, **kw): return func(*a, **kw).

6. Что такое декоратор

Функция, оборачивающая другую функцию. @decorator — сахар над func = decorator(func). Внутри своего декоратора обязательно @functools.wraps(func) — сохраняет метаданные оригинала. Используется для сквозных обязанностей: логирование, retry, кеш, auth.

7. Генератор vs list — когда что

Генератор ленивый — не хранит все элементы в памяти. Для миллионных потоков (лог-файлы, БД-выборки) — генератор. Для повторного обхода / индексации / len() — list. yield в функции превращает её в генератор.

8. Замыкания (closures) и late binding

Внутренняя функция запоминает переменные внешней. Late binding: [lambda: i for i in range(3)] — все lambda ссылаются на ОДНУ i, к моменту вызова = 2. Фикс: lambda i=i: i (захват через default).

9. __init__ vs __new__

__new__ — создаёт объект (возвращает instance). __init__ — инициализирует уже созданный. 95% времени пишешь только __init__. __new__ нужен для immutable-типов (наследники str/tuple) и метаклассов.

10. MRO и множественное наследование

Method Resolution Order — порядок, в котором Python ищет метод в родителях. Алгоритм C3 linearization. Проверить: ClassName.__mro__. При diamond inheritance (D наследует B и C, обе наследуют A) — MRO гарантирует, что A встретится один раз.

11. asyncio vs threading vs multiprocessing

asyncio — concurrent I/O в одном потоке, тысячи «задач» без overhead. threading — I/O-bound + legacy, GIL мешает CPU. multiprocessing — отдельные процессы, обходит GIL, для CPU-heavy (обработка изображений, ML-inference).

12. @classmethod vs @staticmethod

@classmethod — первый аргумент cls, знает свой класс. Используется для альтернативных конструкторов (User.from_dict(...)). @staticmethod — обычная функция в namespace класса, ни self, ни cls.

13. @property

Метод, вызываемый как атрибут. Позволяет добавить валидацию/вычисление без изменения интерфейса. Питонический подход: начинай с публичного атрибута, при необходимости — превращай в property.

14. dataclass vs namedtuple vs Pydantic

dataclass — стандартный способ создать DTO с __init__/__eq__/__repr__. namedtuple — легковесный immutable, доступ по атрибуту и по индексу. Pydantic — тот же dataclass + валидация в runtime, конвертация типов, JSON serialization. Для внешних данных (API-вход) — Pydantic.

15. Type hints — enforcement?

Нет. Python не проверяет их в runtime — только подсказки. Проверяет отдельный tool (mypy). В runtime валидацию делает Pydantic (использует те же аннотации).

16. Оптимизация N+1 в SQLAlchemy / Django

SQLAlchemy: selectinload (2 запроса через IN) / joinedload (JOIN одним запросом). Django: prefetch_related / select_related. Правило: selectinload/prefetch_related для 1-N, joinedload/select_related для N-1 (FK).

17. EAFP vs LBYL

Easier to Ask Forgiveness than Permission — попробуй, поймай исключение. Look Before You Leap — проверь условие ДО. Питонично — EAFP. Пример: try: v = d[k] except KeyError: ... вместо if k in d: v = d[k]. Но d.get(k) ещё лучше.

18. Context manager — with

Гарантированное освобождение ресурсов через __enter__/__exit__. Свой — либо класс с этими методами, либо декоратор @contextmanager над функцией с yield.

19. venv зачем — если можно ставить пакеты глобально

Разные проекты требуют разных версий одних и тех же библиотек. venv — изолированный интерпретатор со своим site-packages/. С 2024 Ubuntu 24.04 глобально ставить вообще запрещено (PEP 668). В 2026 — стандарт uv.

20. FastAPI vs Django — когда что

Django — сайт с админкой, batteries included. FastAPI — REST API / микросервис / AI-backend, async first, автогенерация OpenAPI. Flask — legacy или мини-скрипт. Гибрид тоже норма: Django для сайта + FastAPI для новых API.

21. Duck typing и Protocol

«Если крякает как утка — то утка». Не проверяется isinstance, работает если у объекта есть нужные методы. typing.Protocol — «структурный интерфейс», проверяемый статически (mypy) без наследования.

22. Как сериализовать datetime в JSON

json.dumps({"created": datetime.now()}) кинет TypeError. Решения: default=str в dumps, или предварительно .isoformat(), или использовать Pydantic (сам сериализует).

23. Разница между Exception и BaseException

BaseException — корень всей иерархии, содержит KeyboardInterrupt и SystemExit. Exception — базовый для обычных ошибок. Никогда не ловить BaseException — заблокируешь Ctrl+C.

24. Что такое __slots__

Класс-атрибут, ограничивающий разрешённые instance-атрибуты. Экономит память (нет __dict__), быстрее доступ. Полезно для классов с миллионами инстансов. Ломает multiple inheritance и dynamic attributes. В @dataclass(slots=True) — из коробки.

25. WSGI vs ASGI

WSGI (2003) — sync-интерфейс для веб-серверов (Flask, Django до 3.0). ASGI (2018) — async-версия (FastAPI, Django 3+). ASGI умеет WebSocket, long-polling, HTTP/2. Uvicorn — популярный ASGI-сервер, gunicorn+uvicorn workers — производственная связка.

Общие советы
  • «Как проверить» → всегда пишу тест на pytest, для API — httpx.AsyncClient(app=app).
  • «Как деплоить» → Docker с multi-stage build; runtime — Python slim, зависимости через uv.
  • «Где хранить конфиг» → pydantic-settings: типизированный класс, читает .env и env-переменные.
  • «Как логировать» → logging.getLogger(__name__), JSON-formatter для сбора в ELK/Loki.
  • «Что улучшить в существующем коде» → type hints, ruff+mypy, замена requests→httpx, sync-код блокирующий event loop.
Финальный итог по KB_19: для PHP-разработчика Python — за 2-4 недели становится продуктивным. Стек 2026: uv + ruff + mypy + FastAPI + SQLAlchemy 2 async + Pydantic + pytest + httpx + Docker. Django — если нужен сайт с админкой. AI/ML — отдельная тропа (PyTorch, LangChain, Anthropic SDK).