Стандартная библиотека: что в ней есть и почему это стоит знать
Есть характерный симптом: разработчик пишет функцию chunked(items, n) на восемь строк, потом
group_by, потом свой класс для путей и наконец retry-декоратор. Через полгода в проекте лежит
две тысячи строк «утилит», каждая из которых на 90 % повторяет что-то из itertools, functools
или pathlib — только без тестов и с парой краевых случаев, которые всплывут в проде.
Обратный симптом не лучше: двадцать зависимостей, половина ради одной функции. Каждая — это supply-chain-риск, обновления, конфликты версий и лишние мегабайты в образе (см. «Цепочка поставок»).
Знание стандартной библиотеки — способ не платить ни ту, ни другую цену: она уже установлена,
протестирована на миллионах машин и версионируется вместе с интерпретатором. Но есть и обратная
сторона: часть модулей спроектирована в конце девяностых и с тех пор только обрастала
совместимостью. Эта статья — про то, что брать не задумываясь, что с оговорками, а что не брать
вовсе. Модули конкурентности вынесены в следующую статью;
устройство dict, list, set — в «Коллекциях»; декораторы,
на которых стоит functools, — в «Функциях».
Как устроена stdlib
Формально это всё, что документировано в The Python Standard Library и приезжает с интерпретатором. Технически — три очень разных пласта:
| Пласт | Где живёт | Примеры | Следствие |
|---|---|---|---|
| Встроено в интерпретатор | C, вкомпилировано | builtins, sys, gc |
всегда есть, не отключить |
| C-модули расширения | .so / .pyd рядом с бинарём |
_socket, _sqlite3, _ssl, math |
могут отсутствовать в урезанной сборке |
| Чистый Python | Lib/*.py в дереве CPython |
pathlib, dataclasses, logging, argparse |
читаемо и отлаживаемо |
Последний пласт — недооценённый учебник. Исходники лежат в cpython/Lib, и почти любой вопрос «а как это работает» решается одной строкой:
import dataclasses, inspect
print(inspect.getsource(dataclasses.field)[:120]) # реальная реализация field()
Про C-модули стоит помнить в контейнерах: python:3.12-alpine без dev-пакетов может приехать без
_ssl или _sqlite3, и import sqlite3 упадёт в рантайме, а не на сборке.
Официальный указатель огромен, поэтому держите в голове карту по задачам:
Данные и алгоритмы: где перестать писать свои циклы
collections — не «дополнительные типы», а точные ответы на вопросы производительности.
from collections import Counter, defaultdict, deque, ChainMap
lines = ["GET /api 200", "GET /api 500", "POST /pay 200", "GET /api 500"]
codes = Counter(line.rsplit(maxsplit=1)[-1] for line in lines) # частоты за один проход
print(codes.most_common(2), codes["404"]) # [('200', 2), ('500', 2)] 0 — без KeyError
by_method = defaultdict(list) # группировка без проверок на существование
for method, path, _ in (l.split() for l in lines):
by_method[method].append(path)
print(dict(by_method)) # {'GET': ['/api', '/api', '/api'], 'POST': ['/pay']}
window = deque(maxlen=3) # скользящее окно: старое вытесняется само
for x in range(6):
window.append(x)
print(list(window)) # [3, 4, 5]
cfg = ChainMap({"timeout": 5}, {"timeout": 30, "retries": 3}) # слои конфигурации
print(cfg["timeout"], cfg["retries"]) # 5 3 — поиск слева направо, без копирования
Сложность операций (структуры подробно — в «Стеки, очереди, деки»):
| Операция | list |
deque |
Комментарий |
|---|---|---|---|
append / pop справа |
O(1) амортиз. | O(1) | равнозначно |
insert(0, x) / pop(0) |
O(n) | O(1) | главная причина брать deque |
Доступ d[i] |
O(1) | O(n) в середине | дек — блочный список, не массив |
Типичная ошибка: очередь задач на list с pop(0). На десяти тысячах элементов это уже десятки
миллионов сдвигов указателей — квадратичное поведение, невидимое в тестах на сотне записей.
heapq и bisect: две библиотеки вместо сортировки
import heapq
from bisect import bisect_right
# Топ-K из потока: O(n log k) времени и O(k) памяти вместо O(n log n) и O(n).
# Не материализует поток — работает и на файле в 50 ГБ.
print(heapq.nlargest(2, [("a", 3), ("b", 9), ("c", 1), ("d", 7)], key=lambda r: r[1]))
# [('b', 9), ('d', 7)]
# Приоритетная очередь: (приоритет, счётчик, объект) — счётчик снимает сравнение
# несравнимых объектов при равных приоритетах.
pq: list[tuple[int, int, str]] = []
for i, (prio, task) in enumerate([(2, "email"), (1, "pay"), (2, "log")]):
heapq.heappush(pq, (prio, i, task)) # O(log n)
print([heapq.heappop(pq)[2] for _ in range(3)]) # ['pay', 'email', 'log']
# bisect: попадание в интервал за O(log n) без единого if
print("FDCBA"[bisect_right([60, 70, 80, 90], 85)]) # 'B'
С 3.10 bisect принимает key=, что убирает старый трюк с параллельными списками. Теория —
в «Бинарном поиске» и
«Кучах».
itertools: ленивые комбинаторы
Всё работает с итераторами: ничего не материализуется, память O(1) на большинстве операций — именно это позволяет обрабатывать файл больше оперативной памяти.
from itertools import batched, chain, islice, pairwise, accumulate, groupby
for chunk in batched(range(1000), 100): # 3.12+: нарезка на пачки для bulk-insert
... # chunk — кортеж, промежуточных списков нет
print([b - a for a, b in pairwise([10, 12, 11, 15])]) # [2, -1, 4] — дельты соседей
print(list(accumulate([100, -30, 50]))) # [100, 70, 120] — нарастающий итог
print(list(islice(chain([1, 2], [3, 4, 5]), 3))) # [1, 2, 3] — срез итератора
Главная грабля модуля — groupby: он группирует подряд идущие элементы, как uniq в Unix,
а не как GROUP BY в SQL.
rows = [("db", 12), ("db", 3), ("api", 7), ("db", 1)]
for key, grp in groupby(rows, key=lambda r: r[0]):
print(key, [v for _, v in grp])
# db [12, 3]
# api [7]
# db [1] <- третья группа "db": вход не был отсортирован
Вторая грабля: группа — итератор, живущий до следующего шага внешнего цикла, поэтому
list(groupby(...)) вернёт пустые группы. Часто defaultdict(list) понятнее и не требует
сортировки. Внизу страницы документации есть блок
itertools recipes — десятки
готовых функций (sliding_window, unique_everseen, roundrobin); их промышленная версия —
more-itertools.
functools: кеш, частичное применение, диспетчеризация
import functools
@functools.cache # 3.9+: то же, что lru_cache(maxsize=None)
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(80), fib.cache_info()) # 23416728348467685 CacheInfo(hits=78, misses=81, ...)
@functools.singledispatch # диспетчеризация по типу первого аргумента
def render(value) -> str:
return str(value)
@render.register
def _(value: list) -> str: # тип берётся из аннотации
return ", ".join(render(v) for v in value)
print(render([1, "два", 3.0])) # '1, два, 3.0'
Грабли кеша, регулярно приводящие к утечкам памяти:
lru_cacheна методе держит сильную ссылку наself: объект не соберётся никогда, а вместе с ним останутся соединение с БД, буферы. Лечится выносом в функцию модуля,cached_propertyили явнымweakref.maxsize=Noneна функции с внешним вводом — неограниченный рост. Кешируйте по ключам из ограниченного множества, а не по пользовательским строкам.- Аргументы обязаны быть хешируемыми:
listиdictкеш не примет.
Отдельно запомните functools.wraps (обязателен в любом декораторе, иначе теряются __name__,
__doc__ и сигнатура для inspect) и functools.partial — она создаёт объект, который корректно
пиклится и потому работает с multiprocessing, в отличие от lambda.
Протоколы коллекций: collections.abc
Когда функция принимает «список», ей почти всегда нужен не list, а последовательность или вообще
итерируемое. collections.abc даёт и ABC для isinstance, и типы для аннотаций.
Практическая ценность: наследование от Mapping даёт восемь методов (get, keys, items,
__contains__, __eq__…) в обмен на три реализованных, а isinstance(x, Iterable) честнее, чем
isinstance(x, list) (см. «ООП в Python» про протоколы).
Файлы, пути и слои ввода-вывода
from pathlib import Path
cfg = Path("/srv/app") / "conf" / "app.toml" # оператор / вместо os.path.join
print(cfg.suffix, cfg.stem, cfg.with_suffix(".bak")) # .toml app /srv/app/conf/app.bak
text = cfg.read_text(encoding="utf-8") # чтение целиком без конструкции with
for dirpath, dirnames, filenames in Path("/srv/app").walk(): # Path.walk — 3.12+
dirnames[:] = [d for d in dirnames if d != ".git"] # обрезаем обход на месте
for name in filenames:
...
За os и shutil остались os.replace() (атомарная замена), os.fsync(), права, copytree,
rmtree, which, disk_usage. Модули дополняют друг друга, а не дублируют.
Главное же, что стоит понять про I/O: open() возвращает не «файл», а стопку из трёх объектов.
Она объясняет и потерянные данные, и mojibake, и почему buffering=0 не работает в текстовом режиме.
from pathlib import Path
import csv, os, tempfile
# 1. ВСЕГДА указывайте encoding: без него берётся кодировка локали —
# UTF-8 на Linux и cp1251 на Windows с русской локалью.
Path("out.txt").write_text("привет", encoding="utf-8")
# 2. Атомарная запись конфига: временный файл рядом, fsync, os.replace.
def write_atomic(path: Path, data: str) -> None:
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=path.name, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(data)
f.flush()
os.fsync(f.fileno()) # без этого при потере питания файл будет пустым
os.replace(tmp, path) # атомарно в пределах ФС: читатель видит старое или новое
except BaseException:
os.unlink(tmp)
raise
# 3. Для CSV обязателен newline="" — иначе на Windows появятся пустые строки.
with Path("report.csv").open("w", encoding="utf-8", newline="") as f:
csv.writer(f).writerows([("id", "sum"), (1, "3.14")])
Запустите код с python -X warn_default_encoding — интерпретатор покажет EncodingWarning везде,
где кодировка не задана явно (PEP 597). В перспективе
PEP 686 сделает UTF-8 режимом по умолчанию, но рассчитывать
на это в коде, живущем сегодня, нельзя. Ещё две ловушки: tempfile.mktemp() (без s) устарел
из-за гонки между проверкой имени и созданием файла, а shutil.rmtree без поддержки нужных
syscalls уязвим к подмене каталога симлинком — есть флаг shutil.rmtree.avoids_symlink_attacks.
Время: единственный модуль, где интуиция подводит всех
from datetime import datetime, timedelta, UTC
from zoneinfo import ZoneInfo
import time
# 1. Момент времени всегда aware. datetime.utcnow() устарел в 3.12 именно потому,
# что возвращал naive-объект, притворявшийся UTC.
now = datetime.now(UTC)
msk = now.astimezone(ZoneInfo("Europe/Moscow")) # база IANA, PEP 615
# 2. Арифметика с tzinfo идёт «по стенным часам», а не по абсолютному времени.
tz = ZoneInfo("Europe/Berlin")
start = datetime(2026, 3, 28, 23, 30, tzinfo=tz)
print(start + timedelta(hours=3))
# 2026-03-29 02:30:00+01:00 — такого локального времени в этот день не существует
print((start.astimezone(UTC) + timedelta(hours=3)).astimezone(tz))
# 2026-03-29 03:30:00+02:00 — правильно: считать в UTC, конвертировать в конце
# 3. Интервалы — только по монотонным часам: time.time() прыгает после синхронизации NTP.
t0 = time.perf_counter()
elapsed = time.perf_counter() - t0
Рефлексы, которые стоит закрепить:
- Хранить и передавать в UTC, конвертировать в местное только на границе с пользователем;
в БД — тип с таймзоной (
timestamptz). - Таймзона — не смещение.
+03:00не эквивалентноEurope/Moscow: правила меняются, и историческая дата с фиксированным смещением превращается в ошибку. - В Windows и slim-контейнерах нет системной базы IANA — нужен пакет
tzdata, иначе
ZoneInfoNotFoundError. monotonic()— для таймаутов,perf_counter()— для замеров,time()— «который час».- Разбор произвольных строк stdlib не умеет:
datetime.fromisoformatс 3.11 глотает почти весь ISO 8601, а"12 мая 2026"— это ужеdateutilилиpendulum.
Форматы, сериализация и хранение
и важен объём?"} Q0 -->|"Остаются у нас"| Q2{"Нужен запрос
по данным?"} Q0 -->|"Это конфигурация"| CFG{"Кто её пишет?"} Q1 -->|Да| BIN["protobuf, msgpack, Arrow
вне stdlib"] Q1 -->|"Нет, важна отладка"| JSON["json: универсально и читаемо,
медленно на гигабайтах"] Q2 -->|"Да, фильтры и агрегаты"| SQLITE["sqlite3: транзакции,
индексы, один файл"] Q2 -->|"Нет, ключ-значение"| DBM["dbm или shelve
для кеша на диске"] CFG -->|Человек| TOML["tomllib 3.11+ — только чтение,
запись через tomli-w"] CFG -->|"Оператор через env"| ENV["os.environ
плюс своя валидация"] JSON --> WARN["Ловушки: int-ключи станут строками,
NaN нарушает RFC 8259,
float не годится для денег"] BIN --> NOPICKLE["pickle — НЕ формат обмена:
исполняет код при загрузке"] style NOPICKLE stroke:#d95f5f style WARN stroke:#d98d3f
import json
from decimal import Decimal
print(json.dumps({1: "a", "amount": 0.1 + 0.2, "nan": float("nan")}))
# {"1": "a", "amount": 0.30000000000000004, "nan": NaN}
# ^ ключ стал строкой ^ двоичный float ^ невалидный JSON по RFC 8259
print(json.loads('{"sum": 10.10}', parse_float=Decimal)["sum"] * 3) # 30.30 — точно
Для денег — Decimal и строки в JSON, никогда не float. Для скорости на больших телах — orjson
или msgspec (в 10–20 раз быстрее), но ценой нативной зависимости.
pickle умеет сохранить почти любой объект и выполняет произвольный код при загрузке:
pickle.loads от недоверенного источника — это RCE, о чём прямо предупреждает
документация. Допустим только внутри доверенного
периметра (кеш на диске, обмен между процессами одного приложения) и никогда как формат хранения:
он ещё и привязан к структуре ваших классов, поэтому переименование поля ломает чтение старых данных.
sqlite3: недооценённый инструмент
Полноценная транзакционная СУБД внутри процесса, без сервера, в одном файле — для кешей, локальных индексов, тестов и CLI-утилит (подробно — «SQLite и встраиваемые БД»).
import sqlite3
conn = sqlite3.connect("cache.db", isolation_level=None) # автокоммит, ручные BEGIN
conn.execute("PRAGMA journal_mode=WAL") # параллельное чтение во время записи
conn.execute("CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v TEXT)")
with conn: # это транзакция, а НЕ закрытие соединения!
conn.executemany("INSERT OR REPLACE INTO kv VALUES (?, ?)", [("a", "1"), ("b", "2")])
print(conn.execute("SELECT v FROM kv WHERE k = ?", ("a",)).fetchone()[0]) # 1
conn.close()
Три вещи ломают людей: with conn управляет транзакцией, а не закрытием; соединение по умолчанию
нельзя использовать из другого потока; параметры подставляются через ?, а форматирование строкой —
это SQL-инъекция.
Хеши, секреты, идентификаторы
import hashlib, hmac, secrets, uuid
token = secrets.token_urlsafe(32) # random — это Mersenne Twister, он предсказуем
ok = hmac.compare_digest(token, "подобранное") # сравнение за постоянное время
key = hashlib.scrypt(b"pass", salt=secrets.token_bytes(16), n=2**14, r=8, p=1) # не sha256!
print(uuid.uuid4()) # случайный, плохо ложится в B-tree индекс
print(uuid.uuid7()) # 3.14+: сортируемый по времени
Прикладная криптография — вне stdlib: hashlib/hmac дают примитивы, для подписей и шифрования
берите cryptography; контекст — в
«Прикладной криптографии».
Процессы, ОС и внешний мир
import subprocess
res = subprocess.run( # канонический вызов: список аргументов, без shell
["git", "rev-parse", "--short", "HEAD"],
capture_output=True, text=True, timeout=10, check=True,
)
print(res.stdout.strip()) # e3f1a2c
Четыре правила: никогда shell=True с пользовательскими данными (f"grep {q} f.txt" при
q = "; rm -rf /" делает ровно то, что вы подумали; если shell нужен — shlex.quote()); всегда
timeout=, иначе зависший ребёнок вешает воркер; check=True, иначе ненулевой код возврата
пройдёт молча; и не читайте stdout после wait() — это классический дедлок:
Из соседнего: os.cpu_count() возвращает число ядер хоста, а не лимит контейнера. Пул на 64
воркера в поде с квотой в 2 CPU — это не ускорение, а тротлинг. С 3.13 есть
os.process_cpu_count() (учитывает affinity, но не cgroup-квоту); в Kubernetes надёжнее читать
лимит из переменной окружения, которую вы прокинули сами.
socket, ssl, ipaddress хороши и нужны. А urllib.request — это API 2000-х: без пула
соединений, без HTTP/2, без ретраев; для HTTP-клиента берите
httpx или requests, а http.server документация прямым текстом
называет непригодным для продакшена.
Логирование и диагностика
logging ругают за многословность, но заменить его почти невозможно: его иерархию понимают все
библиотеки экосистемы. Разобравшись один раз в модели «логгер → фильтры → хендлеры → подъём к
предку», вы перестанете гадать, почему логи дублируются или пропадают.
Продакшн-конфигурация — одним dictConfig на старте, а не разбросанными basicConfig:
import logging.config, contextvars
request_id = contextvars.ContextVar("request_id", default="-")
class RequestIdFilter(logging.Filter):
"""Фильтр как способ обогатить запись, а не отбросить её."""
def filter(self, record: logging.LogRecord) -> bool:
record.request_id = request_id.get()
return True
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": False, # иначе замолчат логгеры библиотек
"filters": {"rid": {"()": RequestIdFilter}},
"formatters": {"plain": {
"format": "%(asctime)s %(levelname)-8s [%(request_id)s] %(name)s: %(message)s"}},
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "plain",
"filters": ["rid"], "stream": "ext://sys.stderr"}},
"root": {"level": "INFO", "handlers": ["console"]},
"loggers": {"urllib3": {"level": "WARNING"}}, # чужой шум — отдельным уровнем
})
log = logging.getLogger(__name__) # ВСЕГДА __name__, никогда не root напрямую
request_id.set("7f3c")
log.info("заказ обработан: %s за %.3f c", "A-17", 0.412) # %s, а не f-строка!
# 2026-07-16 10:00:00,123 INFO [7f3c] __main__: заказ обработан: A-17 за 0.412 c
try:
1 / 0
except ZeroDivisionError:
log.exception("не смог посчитать конверсию") # ERROR + полный traceback
Почему %s, а не f-строка: при отключённом уровне подстановка не выполняется вовсе, а одинаковый
record.msg позволяет группировать события в системах вроде Sentry. contextvars здесь
принципиален — он корректно работает и с потоками, и с asyncio-задачами, в отличие от
threading.local. Для медленных приёмников (файл, сеть) ставьте QueueHandler плюс
QueueListener в отдельном потоке, иначе логирование начнёт блокировать обработку запросов.
Остальной диагностический арсенал:
| Модуль | Задача | Когда достаёте |
|---|---|---|
traceback |
форматирование стека вручную | свой обработчик ошибок, отправка в очередь |
faulthandler |
дамп стека по сигналу или при падении C-кода | зависший или упавший по SIGSEGV процесс |
tracemalloc |
кто и где выделил память | поиск утечек через сравнение снимков |
gc |
доступ к сборщику циклов | gc.freeze() перед fork ради copy-on-write |
timeit |
микробенчмарк с прогревом | сравнение двух реализаций |
cProfile + pstats |
профиль по функциям | «почему ручка отвечает 900 мс» |
dis / inspect |
байт-код, сигнатуры, кадры стека | фреймворки, DI, отладка магии |
warnings |
предупреждения о deprecation | миграции; -W error в CI |
import timeit
setup = "from collections import deque; d = deque(range(10_000)); l = list(range(10_000))"
print(timeit.timeit("l.pop(0)", setup, number=10_000)) # ~0.006 c — O(n) на вызов
print(timeit.timeit("d.popleft()", setup, number=10_000)) # ~0.0002 c — O(1), в 30 раз быстрее
Глубже — в «Производительности»; unittest, doctest и
unittest.mock разбираются в «Тестировании».
Практика: полезная утилита без единой зависимости
Показательный масштаб задачи, которую stdlib закрывает целиком: CLI-инвентаризация каталога с параллельным хешированием, хранением в SQLite и выгрузкой в CSV.
#!/usr/bin/env python3
"""inv.py — инвентаризация каталога. Зависимостей: ноль."""
from __future__ import annotations
import argparse, csv, hashlib, logging, os, sqlite3, sys
from collections.abc import Iterator
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
log = logging.getLogger("inv")
def digest(path: Path) -> tuple[str, int, str]:
"""Потоковый sha256: файл на 10 ГБ не окажется в памяти целиком."""
h = hashlib.sha256()
with path.open("rb") as f:
while chunk := f.read(1 << 20): # читаем по мегабайту
h.update(chunk)
return str(path), path.stat().st_size, h.hexdigest()
def iter_files(root: Path, skip: set[str]) -> Iterator[Path]:
for dirpath, dirnames, filenames in root.walk(): # Path.walk — 3.12+
dirnames[:] = [d for d in dirnames if d not in skip] # обрезаем обход на месте
for name in filenames:
yield dirpath / name
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(prog="inv", description="Инвентаризация каталога")
p.add_argument("root", type=Path)
p.add_argument("--db", type=Path, default=Path("inv.sqlite3"))
p.add_argument("--csv", type=Path, help="выгрузить результат в CSV")
p.add_argument("--skip", action="append", default=[".git", "__pycache__"])
p.add_argument("-v", "--verbose", action="count", default=0)
args = p.parse_args(argv)
logging.basicConfig(
level=logging.WARNING - 10 * min(args.verbose, 2), # -v → INFO, -vv → DEBUG
format="%(asctime)s %(levelname)-8s %(message)s", stream=sys.stderr,
)
conn = sqlite3.connect(args.db)
conn.execute("CREATE TABLE IF NOT EXISTS files("
"path TEXT PRIMARY KEY, size INTEGER, sha256 TEXT)")
files = list(iter_files(args.root, set(args.skip)))
log.info("найдено файлов: %d", len(files))
workers = min(os.process_cpu_count() or 1, 8) # 3.13+; ранее os.cpu_count()
with ProcessPoolExecutor(max_workers=workers) as pool, conn:
# хеширование CPU-bound → процессы, а не потоки;
# chunksize амортизирует накладные расходы на межпроцессное общение
rows = pool.map(digest, files, chunksize=16)
conn.executemany("INSERT OR REPLACE INTO files VALUES (?, ?, ?)", rows)
if args.csv:
with args.csv.open("w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["path", "size", "sha256"])
w.writerows(conn.execute("SELECT * FROM files ORDER BY size DESC"))
conn.close()
return 0
if __name__ == "__main__": # обязателен: пул импортирует модуль в дочерних процессах
raise SystemExit(main())
Восемь модулей stdlib, ноль зависимостей, работает на любой машине с Python 3.12+. Именно так
выглядит внутренняя утилита, которую не надо ни собирать, ни ставить в venv на чужом сервере
(а если надо раздать одним файлом — есть zipapp).
Где stdlib честно проигрывает
Батарейки в комплекте — это ещё и батарейки, которые нельзя выкинуть: модуль, попавший в stdlib, обязан хранить обратную совместимость и обновляется раз в год. Для быстро эволюционирующих задач это фатально.
| Задача | stdlib | Что берут в проде | Почему |
|---|---|---|---|
| HTTP-клиент | urllib.request |
httpx, requests |
пул соединений, HTTP/2, ретраи |
| HTTP-сервер | http.server |
uvicorn, gunicorn |
stdlib-версия однопоточная и без защиты |
| Тесты | unittest |
pytest |
фикстуры, параметризация, простые ассерты |
| Валидация | ручные проверки | pydantic, msgspec |
схемы, приведение типов, скорость |
| Парсинг дат | datetime |
dateutil, pendulum |
произвольные форматы, арифметика с DST |
| CLI | argparse |
click, typer |
вложенные команды, типизация, автодополнение |
| JSON на объёмах | json |
orjson, msgspec |
10–20× на больших телах |
| Числа и таблицы | array, statistics |
numpy, polars |
векторизация вместо интерпретируемого цикла |
| Структурные логи | logging |
structlog над logging |
поля вместо строк из коробки |
Обратите внимание: почти все «замены» не выкидывают stdlib, а надстраиваются над ней — httpx
использует ssl, structlog — logging, pytest умеет запускать unittest-тесты. Знание
базового слоя не обесценивается: оно позволяет чинить надстройку.
PEP 594 формализовал удаление модулей, которые никто не
поддерживал десятилетиями. Практический вывод: держите в CI -W error::DeprecationWarning — тогда
удаление модуля вы увидите за год, а не в день релиза. Раздел
What’s New читается за пятнадцать минут и экономит дни.
Типичные ошибки: чек-лист
| Симптом | Причина | Как правильно |
|---|---|---|
| Кракозябры при чтении файла | open() без encoding |
всегда encoding="utf-8" |
| Пустые строки в CSV на Windows | нет newline="" |
open(..., newline="") |
| Файл пуст после сбоя питания | нет fsync и os.replace |
атомарная запись |
| Рост RSS без утечки в коде | lru_cache на методе держит self |
кеш на функции модуля или weakref |
groupby теряет данные |
вход не отсортирован по ключу | sorted перед ним или defaultdict |
| Очередь тормозит на 10к задач | list.pop(0) — O(n) |
collections.deque |
| Токены угадываются | random вместо secrets |
secrets.token_urlsafe |
| RCE при загрузке кеша | pickle.loads внешних данных |
JSON или подписанный формат |
| Дочерний процесс висит | Popen.wait() при stdout=PIPE |
communicate(timeout=…) |
| Замер времени «прыгает» | time.time() для интервалов |
time.perf_counter() |
DeprecationWarning про utcnow |
naive-datetime | datetime.now(UTC) |
| Логи дублируются | хендлеры и на потомке, и на предке | хендлеры только на root или app |
| Логи молчат | не настроен ни один хендлер | dictConfig на старте |
| 64 воркера душат под | os.cpu_count() вместо лимита cgroup |
лимит из env |
Мини-итог
Стандартная библиотека — не «набор мелочей», а несколько десятков модулей, каждый из которых
закрывает класс задач целиком. collections, itertools и functools убирают половину
самописных утилит; pathlib и слои io объясняют, где реально живут ваши данные между write()
и диском; datetime с zoneinfo спасают от классических ошибок со временем; sqlite3 даёт
настоящую БД без сервера; logging — общую для экосистемы модель наблюдаемости; subprocess,
hashlib и secrets — безопасный доступ к ОС и криптопримитивам.
Правило выбора: задача стабильна и цена ошибки высока — берите stdlib; задача быстро
эволюционирует — берите зрелую библиотеку, но понимайте, на чём она стоит. Всё, что не попадает
ни туда, ни туда, обычно означает, что вы пишете собственный itertools — проверьте документацию,
скорее всего это уже есть.
Источники
- The Python Standard Library — первоисточник; начинайте с оглавления, а не с поиска.
- cpython/Lib — исходники модулей на чистом Python, лучший учебник по идиоматике.
- Doug Hellmann, Python Module of the Week и книга The Python 3 Standard Library by Example — модуль за модулем.
- itertools recipes и more-itertools.
- Logging HOWTO и
Logging Cookbook — включая
QueueHandlerи конфигурацию для многопроцессных приложений. - PEP 594 (мёртвые батарейки),
PEP 597 (
EncodingWarning), PEP 686 (UTF-8 по умолчанию), PEP 615 (zoneinfo). - What’s New in Python — что добавили и что убрали.
- Luciano Ramalho, Fluent Python, 2nd ed. —
главы про
collections.abc, итераторы иfunctools. - David Beazley, Brian K. Jones, Python Cookbook, 3rd ed. — рецепты по файлам, датам, конкурентности и парсингу.
- sqlite3 и SQLite: appropriate uses.
- Security considerations — сводный список модулей stdlib с предупреждениями о безопасности.
Что дальше
Конкурентность: GIL, threading, multiprocessing, asyncio — что когда —
разберём, почему потоки в CPython не ускоряют вычисления, но спасают на I/O, чем процессы платят
за параллелизм и где asyncio даёт выигрыш в десятки раз, а где только усложняет код.