Python Стандартная библиотека: что в ней есть и почему это стоит знать
0%

Стандартная библиотека: что в ней есть и почему это стоит знать

Стандартная библиотека: что в ней есть и почему это стоит знать

Есть характерный симптом: разработчик пишет функцию 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 не работает в текстовом режиме.

Слои файлового ввода-вывода в Python

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

Рефлексы, которые стоит закрепить:

  1. Хранить и передавать в UTC, конвертировать в местное только на границе с пользователем; в БД — тип с таймзоной (timestamptz).
  2. Таймзона — не смещение. +03:00 не эквивалентно Europe/Moscow: правила меняются, и историческая дата с фиксированным смещением превращается в ошибку.
  3. В Windows и slim-контейнерах нет системной базы IANA — нужен пакет tzdata, иначе ZoneInfoNotFoundError.
  4. monotonic() — для таймаутов, perf_counter() — для замеров, time() — «который час».
  5. Разбор произвольных строк stdlib не умеет: datetime.fromisoformat с 3.11 глотает почти весь ISO 8601, а "12 мая 2026" — это уже dateutil или pendulum.

Форматы, сериализация и хранение

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, structloglogging, 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 — проверьте документацию, скорее всего это уже есть.

Источники

Что дальше

Конкурентность: GIL, threading, multiprocessing, asyncio — что когда — разберём, почему потоки в CPython не ускоряют вычисления, но спасают на I/O, чем процессы платят за параллелизм и где asyncio даёт выигрыш в десятки раз, а где только усложняет код.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов