Вопросы на собеседовании Python Senior: 100 вопросов с ответами (2026)

Вопросы на собеседовании Python Senior заметно отличаются от джуниорских: здесь не спрашивают, чем список отличается от кортежа. Проверяют понимание устройства CPython, GIL и конкурентности, asyncio, метапрограммирования, типизации, производительности и архитектуры. Мы собрали 100 реальных вопросов с собеседований Python Senior-разработчика с краткими ответами и примерами кода, актуальных для Python 3.12, 3.13 и 3.14.

Используйте статью как чек-лист для подготовки: прочитайте вопрос, ответьте вслух сами, затем сверьтесь с ответом. Если ответ не получается уложить в 2–3 предложения, эту тему стоит повторить.
Эта подборка — продолжение материала 25 вопросов и ответов для Python-собеседования Middle. Если нужен общий план развития, загляните в Python Roadmap 2026, а первоисточник по новинкам языка — официальный раздел What’s New in Python 3.14.
Содержание: вопросы на собеседовании Python Senior по темам
- Внутреннее устройство CPython и память (1–15)
- GIL, многопоточность, multiprocessing и asyncio (16–32)
- ООП, дескрипторы и метаклассы (33–48)
- Функции, декораторы, генераторы (49–60)
- Типизация в Python (61–70)
- Производительность и профилирование (71–80)
- Стандартная библиотека (81–88)
- Тестирование и архитектура (89–95)
- Продакшн, зависимости, безопасность (96–100)
- FAQ: как подготовиться к собеседованию Python Senior
Внутреннее устройство CPython и управление памятью
С этой темы обычно начинаются вопросы на собеседовании Python Senior: интервьюер проверяет, понимаете ли вы, что происходит «под капотом» интерпретатора.
1. Как устроен объект в CPython?
Любой объект в CPython — это структура PyObject в куче, содержащая счётчик ссылок ob_refcnt и указатель на тип ob_type. Объекты переменной длины (list, tuple, int) используют PyVarObject с полем ob_size. Переменные в Python — это лишь имена, связанные со ссылками на объекты.
2. Как в Python работает управление памятью?
Два механизма: подсчёт ссылок (объект освобождается сразу, как только счётчик становится нулём) и циклический сборщик мусора для объектов, ссылающихся друг на друга. Мелкие объекты (до 512 байт) выделяются аллокатором pymalloc через иерархию арены → пулы → блоки, что снижает фрагментацию и число системных вызовов.
3. Как работает сборщик мусора (модуль gc)?
GC отслеживает только объекты-контейнеры (list, dict, пользовательские классы), способные образовать цикл. Исторически он поколенческий: новые объекты живут в молодом поколении, выжившие переходят в старшие, которые проверяются реже. В Python 3.14 сборщик стал инкрементальным, что уменьшает паузы. Управление: gc.collect(), gc.disable(), gc.freeze() (полезно перед fork в pre-fork серверах).
4. Что такое циклические ссылки и как помогает weakref?
Цикл — ситуация, когда объекты ссылаются друг на друга (родитель ↔ ребёнок), и refcount никогда не падает до нуля; такие объекты удаляет только GC. weakref.ref создаёт ссылку, не увеличивающую счётчик, поэтому обратные ссылки (ребёнок → родитель), кеши и observer-подписки лучше делать слабыми.
5. Чем отличается is от ==? Что такое интернирование?
is сравнивает идентичность (один и тот же объект), == — равенство через __eq__. CPython кеширует малые целые числа от −5 до 256 и интернирует некоторые строки (похожие на идентификаторы), поэтому a is b иногда «случайно» истинно. Использовать is можно только для синглтонов: None, True, False, sentinel-объектов.
6. Почему изменяемый аргумент по умолчанию — ошибка?
Значения по умолчанию вычисляются один раз при определении функции и сохраняются в func.__defaults__. Изменяемый объект будет общим для всех вызовов.
def add(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
7. Как устроен dict в CPython?
Это хэш-таблица с открытой адресацией. С Python 3.6 словарь «компактный»: отдельный разреженный массив индексов и плотный массив записей (хэш, ключ, значение). Поэтому он экономнее по памяти и сохраняет порядок вставки — с 3.7 это гарантия языка. Для экземпляров классов используются key-sharing словари: ключи атрибутов разделяются между объектами одного типа.
8. Какая сложность основных операций у list, dict, set, deque?
- list: доступ по индексу и
append— O(1) амортизированно;insert(0, x),pop(0),x in list— O(n). - dict / set: поиск, вставка, удаление — O(1) в среднем, O(n) в худшем при коллизиях.
- deque:
append/popleftс обеих сторон — O(1), доступ по индексу к середине — O(n).
9. Почему list.append работает за амортизированное O(1)?
Список — динамический массив указателей. При переполнении CPython выделяет память с запасом (over-allocation, примерно +12.5% плюс константа), поэтому дорогая переаллокация происходит редко, а средняя стоимость добавления остаётся константной.
10. Какой контракт между __hash__ и __eq__?
Если a == b, то обязательно hash(a) == hash(b). Хэш не должен меняться за время жизни объекта, поэтому изменяемые объекты (list, dict) не хэшируемы. Если в классе переопределён __eq__ без __hash__, Python устанавливает __hash__ = None, и объект нельзя положить в set или использовать как ключ dict.
11. Что такое байткод и как его посмотреть?
Исходный код компилируется в байткод, который исполняет цикл интерпретатора (ceval). Посмотреть можно через модуль dis. Начиная с Python 3.11 работает специализирующий адаптивный интерпретатор (PEP 659): «горячие» инструкции заменяются специализированными версиями под конкретные типы, что дало заметный прирост скорости.
import dis
dis.dis(lambda x: x + 1)
12. Что делает __slots__ и когда его применять?
__slots__ заменяет __dict__ экземпляра фиксированным набором дескрипторов атрибутов. Это экономит память (актуально при миллионах объектов) и немного ускоряет доступ, но запрещает добавлять новые атрибуты и усложняет множественное наследование. В dataclass включается через @dataclass(slots=True).
13. Почему sys.getsizeof не показывает реальный объём памяти?
Он возвращает размер только самого объекта, без объектов, на которые тот ссылается. Для списка из строк будет учтён массив указателей, но не сами строки. Для реального анализа используют tracemalloc, pympler.asizeof или memray.
14. Что происходит при import module?
Python проверяет кеш sys.modules; если модуля нет — обходит sys.meta_path (finders), находит спецификацию модуля, loader создаёт объект модуля, регистрирует его в sys.modules и исполняет код модуля. Скомпилированный байткод кешируется в __pycache__/*.pyc. Код модуля выполняется только при первом импорте.
15. Как решать проблему циклических импортов?
Лучшее решение — архитектурное: вынести общий код в третий модуль или развернуть зависимость. Тактические варианты: импорт внутри функции, import module вместо from module import name, импорты только для типов под if TYPE_CHECKING:.
GIL, многопоточность, multiprocessing и asyncio
Конкурентность — самый объёмный блок, и вопросы на собеседовании Python Senior здесь почти всегда переходят в обсуждение реальных кейсов из вашего опыта.
16. Что такое GIL и зачем он нужен?
Global Interpreter Lock — мьютекс, который позволяет исполнять байткод Python только одному потоку в процессе одновременно. Он упрощает реализацию подсчёта ссылок и C-расширений, обеспечивая потокобезопасность внутренних структур. Цена — потоки не дают параллелизма на CPU-bound задачах. Вопрос о GIL встречается почти на каждом собеседовании Python Senior.
17. Когда многопоточность в Python всё же полезна?
Для I/O-bound задач: во время сетевых запросов, чтения файлов и time.sleep GIL отпускается. Также многие C-расширения (NumPy, hashlib, zlib, драйверы БД) отпускают GIL во время тяжёлых вычислений, поэтому потоки с ними могут реально работать параллельно.
18. Что такое free-threaded Python (PEP 703)?
Это сборка CPython без GIL. В Python 3.13 она появилась как экспериментальная (python3.13t), в 3.14 получила статус официально поддерживаемой, но по-прежнему не является сборкой по умолчанию. Однопоточный код в ней работает несколько медленнее, а C-расширения должны явно объявлять совместимость. Проверить режим: sys._is_gil_enabled().
19. Как выбрать между threading, multiprocessing и asyncio?
- asyncio — тысячи одновременных I/O-операций (сеть, БД) при наличии асинхронных библиотек.
- threading — I/O с блокирующими библиотеками, небольшое число параллельных задач.
- multiprocessing / ProcessPoolExecutor — CPU-bound вычисления на чистом Python.
20. Какие есть способы запуска процессов в multiprocessing?
fork копирует процесс (быстро, но опасно при наличии потоков и блокировок), spawn запускает чистый интерпретатор (медленнее, безопаснее, требует picklable-аргументов), forkserver — форк от заранее запущенного чистого сервера. В Python 3.14 методом по умолчанию на Linux стал forkserver, на macOS и Windows — spawn.
21. Что такое субинтерпретаторы?
Несколько изолированных интерпретаторов в одном процессе. С Python 3.12 каждый может иметь собственный GIL (PEP 684), что даёт параллелизм без накладных расходов на процессы. В 3.14 появился модуль concurrent.interpreters (PEP 734) и InterpreterPoolExecutor в concurrent.futures.
22. Как работает event loop в asyncio?
Цикл событий в одном потоке крутит очередь готовых callback’ов и опрашивает сокеты через селектор (epoll/kqueue). Корутина выполняется до await на незавершённом Future, после чего управление возвращается циклу. Когда I/O готов, Future завершается, и задача снова ставится в очередь. Это кооперативная многозадачность.
23. Чем отличаются корутина, Task и Future?
Корутина — объект, возвращаемый async def-функцией; сама по себе не выполняется. Future — низкоуровневый объект-обещание результата. Task — подкласс Future, оборачивающий корутину и планирующий её выполнение в цикле событий (asyncio.create_task).
24. Что будет, если вызвать блокирующую функцию внутри async-кода?
Заблокируется весь event loop — все остальные задачи встанут. Блокирующий код нужно выносить в пул потоков через await asyncio.to_thread(func, *args) или loop.run_in_executor, а CPU-bound — в пул процессов.
25. Чем asyncio.TaskGroup лучше asyncio.gather?
TaskGroup (Python 3.11) реализует структурную конкурентность: при ошибке в одной задаче остальные автоматически отменяются, а все исключения собираются в ExceptionGroup. У gather при ошибке остальные задачи продолжают работать «в фоне», что приводит к утечкам.
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch(url1))
t2 = tg.create_task(fetch(url2))
print(t1.result(), t2.result())
26. Как правильно работать с отменой задач?
Отмена реализуется выбрасыванием asyncio.CancelledError в точке await. Его нельзя «проглатывать»: после очистки ресурсов исключение нужно пробросить дальше. Для таймаутов в современном коде используют async with asyncio.timeout(5): (Python 3.11+).
27. Как ограничить число одновременных запросов в asyncio?
sem = asyncio.Semaphore(10)
async def limited_fetch(url):
async with sem:
return await fetch(url)
28. Возможна ли гонка данных в однопоточном asyncio?
Да. Между двумя await другая корутина может изменить общее состояние: «прочитал → await → записал» — классическая гонка. Для защиты используют asyncio.Lock или проектируют код без разделяемого изменяемого состояния.
29. Чем отличаются Lock, RLock, Condition, Event, Semaphore?
Lock — простой мьютекс; RLock — реентерабельный, его может повторно захватить тот же поток; Condition — ожидание условия с уведомлением; Event — флаг для сигнализации; Semaphore — ограничение числа одновременных владельцев. Важно: операция x += 1 не атомарна даже с GIL.
30. Что такое ExceptionGroup и except*?
Появились в Python 3.11 (PEP 654) для случаев, когда одновременно возникает несколько исключений, например в TaskGroup. except* обрабатывает подгруппу исключений заданного типа, остальные пробрасываются дальше.
try:
async with asyncio.TaskGroup() as tg:
...
except* ConnectionError as eg:
log.warning("network errors: %s", eg.exceptions)
31. Зачем нужны contextvars?
contextvars.ContextVar хранит значение, изолированное для каждой задачи asyncio (и для потока). threading.local в asyncio не работает, потому что все корутины живут в одном потоке. Типичное применение — request_id для логирования, текущий пользователь, транзакция БД.
32. Как отлаживать зависшее или медленное asyncio-приложение?
Включить debug-режим (PYTHONASYNCIODEBUG=1 или asyncio.run(main(), debug=True)) — он логирует callback’и дольше 100 мс и забытые корутины. Использовать py-spy для дампа стека. В Python 3.14 появилась команда python -m asyncio ps PID / pstree PID для просмотра дерева задач работающего процесса.
ООП, дескрипторы и метаклассы
33. Что такое MRO и как работает super()?
MRO (Method Resolution Order) — порядок поиска атрибутов в иерархии классов, строится алгоритмом C3-линеаризации; смотреть через Class.__mro__. super() возвращает не «родителя», а следующий класс в MRO текущего объекта, поэтому кооперативное множественное наследование работает, только если все классы вызывают super().
34. Что такое дескрипторы?
Объект, определяющий __get__, __set__ или __delete__, и хранящийся как атрибут класса. Data-дескрипторы (с __set__) имеют приоритет над __dict__ экземпляра, non-data (только __get__) — нет. На дескрипторах построены методы, property, classmethod, staticmethod, __slots__, поля ORM.
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError("must be positive")
setattr(obj, self.name, value)
class Order:
qty = Positive()
35. Как функция превращается в связанный метод?
Функция — non-data дескриптор. При обращении obj.method вызывается function.__get__(obj, type), который возвращает bound method с подставленным self. classmethod подставляет класс, staticmethod возвращает функцию без изменений.
36. Чем отличаются __getattr__ и __getattribute__?
__getattribute__ вызывается при любом обращении к атрибуту; его переопределение опасно рекурсией (используйте super().__getattribute__). __getattr__ вызывается только если атрибут не найден обычным путём — удобно для прокси и ленивых атрибутов.
37. Что такое метакласс и когда он нужен?
Метакласс — «класс класса», по умолчанию type. Он управляет созданием классов: может модифицировать атрибуты, регистрировать подклассы, проверять контракты. Используется в ORM (Django, SQLAlchemy), Enum, ABC. В прикладном коде почти всегда есть более простая альтернатива.
class Registry(type):
classes = {}
def __new__(mcs, name, bases, ns):
cls = super().__new__(mcs, name, bases, ns)
mcs.classes[name] = cls
return cls
38. Чем можно заменить метакласс?
__init_subclass__ (хук при создании подкласса — регистрация плагинов, валидация), __set_name__ у дескрипторов (узнать имя атрибута), декораторы классов. Эти средства проще, композируемы и не конфликтуют при множественном наследовании.
class Plugin:
registry = {}
def __init_subclass__(cls, /, name, **kw):
super().__init_subclass__(**kw)
Plugin.registry[name] = cls
class CSV(Plugin, name="csv"): ...
39. Чем __new__ отличается от __init__?
__new__ — статический метод, создающий и возвращающий экземпляр; __init__ инициализирует уже созданный объект. __new__ переопределяют при наследовании от неизменяемых типов (int, str, tuple), для кеширования экземпляров и в метаклассах.
40. ABC или Protocol — что выбрать?
ABC — номинальная типизация: класс должен явно наследоваться, проверка абстрактных методов в рантайме при создании экземпляра. Protocol — структурная типизация («утиная» для статического анализатора): подходит любой класс с нужными методами без наследования. Protocol лучше для границ модулей и сторонних классов, ABC — когда нужна общая реализация и рантайм-гарантии.
41. Когда использовать dataclass, attrs или pydantic?
dataclasses — стандартная библиотека, нет валидации, быстро; поддерживает frozen, slots, kw_only. attrs — больше возможностей (валидаторы, конвертеры). pydantic — валидация и парсинг внешних данных (API, конфиги), сериализация в JSON. Правило: pydantic на границах системы, dataclass внутри домена.
42. Композиция или наследование?
Предпочтительна композиция: она уменьшает связность и позволяет подменять поведение. Наследование уместно для отношения «является» и расширения фреймворков. Mixin-классы допустимы для небольшого переиспользуемого поведения без состояния, но глубокие иерархии и «ромбы» усложняют поддержку.
43. Как dataclass генерирует __hash__?
При eq=True (по умолчанию) и frozen=False устанавливается __hash__ = None — объект нехэшируем. При frozen=True хэш вычисляется по полям. Параметр unsafe_hash=True принудительно генерирует хэш для изменяемого класса — использовать осторожно.
44. Какие магические методы нужны для своего контейнера?
Минимум — __len__ и __getitem__ (тогда работают итерация и in); лучше явно реализовать __iter__ и __contains__. Удобнее наследоваться от collections.abc.Sequence или Mapping — остальные методы (index, count, keys, items) будут получены автоматически.
45. Как работают контекстные менеджеры?
with вызывает __enter__, а при выходе — __exit__(exc_type, exc, tb), даже при исключении. Если __exit__ вернёт True, исключение подавляется. contextlib.contextmanager превращает генератор в менеджер, ExitStack позволяет динамически управлять множеством ресурсов, для async — __aenter__/__aexit__.
from contextlib import contextmanager
@contextmanager
def timer(name):
start = time.perf_counter()
try:
yield
finally:
print(name, time.perf_counter() - start)
46. Как сделать кеш объектов, не мешающий сборке мусора?
Использовать weakref.WeakValueDictionary или WeakKeyDictionary: запись исчезает, когда на объект не осталось сильных ссылок. Обычный dict держит объекты вечно и создаёт утечку памяти.
47. Как сделать объект неизменяемым?
Варианты: @dataclass(frozen=True), typing.NamedTuple, __slots__ + переопределение __setattr__, наследование от tuple. Полной неизменяемости в Python нет — через object.__setattr__ её можно обойти, но это соглашение для разработчиков.
48. Что такое monkey patching и чем он опасен?
Изменение классов или модулей во время выполнения (замена функции, метода). Применяется в тестах (mock.patch) и gevent. В продакшн-коде это делает поведение неявным, ломается при обновлении библиотек и вызывает трудноуловимые баги из-за порядка импортов.
Функции, замыкания, декораторы и генераторы
49. Что выведет этот код и почему?
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2]
Замыкания захватывают переменную, а не её значение (позднее связывание). К моменту вызова i == 2. Решение — зафиксировать значение аргументом по умолчанию: lambda i=i: i или использовать functools.partial.
50. Как написать декоратор с параметрами?
import functools
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times - 1:
raise
return wrapper
return decorator
functools.wraps копирует __name__, __doc__, __wrapped__ — без него ломаются логи, документация и интроспекция.
51. Можно ли сделать декоратор классом?
Да, класс с методом __call__. Это удобно, если декоратору нужно состояние (счётчик вызовов, кеш). Однако для декорирования методов такой класс должен реализовывать __get__, иначе не будет передан self.
52. Как работают генераторы?
Функция с yield возвращает объект-генератор; при каждом next() выполнение продолжается до следующего yield, а фрейм с локальными переменными сохраняется между вызовами. Генератор поддерживает send() (передать значение внутрь), throw() и close(). Это основа ленивой обработки потоков данных.
53. Что делает yield from?
Делегирует итерацию вложенному генератору, прозрачно пробрасывая send, throw и возвращая значение return подгенератора. Исторически на этом механизме были построены корутины до появления async/await.
54. Чем итератор отличается от итерируемого объекта?
Iterable имеет __iter__, возвращающий новый итератор (list, dict), — его можно обходить многократно. Iterator имеет __next__ и __iter__, возвращающий себя, — он одноразовый. Частый баг: передать генератор в функцию, которая обходит данные дважды.
55. Какие инструменты functools вы используете?
lru_cache/cache — мемоизация; cached_property — ленивый атрибут; partial — частичное применение; singledispatch — перегрузка по типу первого аргумента; reduce, total_ordering. Важно: lru_cache на методе экземпляра хранит self в ключе и не даёт объектам удалиться — утечка памяти.
56. Что такое правило LEGB?
Порядок поиска имён: Local → Enclosing (внешние функции) → Global (модуль) → Built-in. Присваивание внутри функции делает имя локальным, поэтому для изменения внешних переменных нужны nonlocal или global, иначе будет UnboundLocalError.
57. Что означают / и * в сигнатуре функции?
Параметры до / — только позиционные, после * — только именованные.
def connect(host, port, /, *, timeout=10, ssl=True): ...
connect("db", 5432, timeout=5)
Это позволяет менять имена позиционных параметров без поломки API и делает вызовы с флагами читаемыми.
58. Генераторное выражение или list comprehension?
List comprehension создаёт весь список в памяти сразу — быстрее при повторном использовании и небольших объёмах. Генераторное выражение ленивое и использует O(1) памяти — подходит для больших потоков и агрегатов: sum(x * x for x in data).
59. Какие функции itertools полезны на практике?
chain, islice, groupby (работает только по отсортированным данным!), product, combinations, accumulate, tee, pairwise (3.10), batched (3.12) — разбиение на пачки фиксированного размера.
60. Как работает структурное сопоставление match/case?
Появилось в Python 3.10 (PEP 634). Это не switch, а сопоставление с образцом по структуре: литералы, последовательности, словари, классы (через __match_args__), guard-условия. Имя без точки в образце — это захват переменной, а не сравнение с константой.
match event:
case {"type": "click", "pos": (x, y)}:
handle_click(x, y)
case Point(x=0, y=y) if y > 0:
...
case _:
pass
Типизация в Python
61. Зачем нужны аннотации типов, если Python их не проверяет?
Статические анализаторы (mypy, pyright) находят ошибки до запуска, IDE даёт точное автодополнение, типы служат документацией и упрощают рефакторинг больших кодовых баз. Рантайм-валидацию на основе аннотаций делают pydantic, FastAPI, beartype.
62. Как объявлять дженерики в современном Python?
С Python 3.12 (PEP 695) появился новый синтаксис параметров типа без явного TypeVar и оператор type для псевдонимов:
def first[T](items: list[T]) -> T:
return items[0]
class Stack[T]:
def push(self, item: T) -> None: ...
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
63. Что такое Protocol?
typing.Protocol описывает интерфейс структурно: класс соответствует протоколу, если у него есть нужные методы, наследование не требуется. С декоратором @runtime_checkable протокол можно проверять через isinstance (но проверяется только наличие методов, не сигнатуры).
64. Что такое ковариантность и контравариантность?
Если Dog — подтип Animal: ковариантный контейнер Sequence[Dog] — подтип Sequence[Animal] (только чтение). list инвариантен: list[Dog] нельзя передать как list[Animal], иначе туда можно добавить кошку. Типы аргументов Callable контравариантны. Поэтому в сигнатурах функций лучше принимать Sequence/Mapping, а не list/dict.
65. Как типизировать словари с фиксированными ключами?
Через TypedDict. Обязательность ключей регулируется total=False, Required[] и NotRequired[], неизменяемость — ReadOnly[] (Python 3.13, PEP 705). Для внешних данных с валидацией лучше pydantic-модель.
66. Как правильно типизировать декоратор?
С помощью ParamSpec, чтобы сохранить сигнатуру оборачиваемой функции, и Concatenate, если декоратор добавляет или убирает аргументы.
def logged[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs)
return wrapper
67. Для чего нужны Self, Literal, Final, override?
Self (3.11) — возвращаемый тип «текущий класс» для fluent-интерфейсов и фабрик; Literal["r", "w"] — конкретные допустимые значения; Final — константа, которую нельзя переопределить; @override (3.12) — анализатор проверит, что метод действительно переопределяет родительский.
68. Как изменилось вычисление аннотаций в Python 3.14?
По PEP 649/749 аннотации теперь вычисляются лениво — только при обращении к ним. Это устраняет проблему forward references без кавычек и без from __future__ import annotations, а также ускоряет импорт. Для интроспекции появился модуль annotationlib.
69. Чем TypeIs отличается от TypeGuard?
Оба используются в функциях-проверках для сужения типа. TypeGuard сужает тип только в ветке True. TypeIs (Python 3.13, PEP 742) сужает в обеих ветках и требует, чтобы суженный тип был подтипом исходного — его поведение ближе к isinstance и в большинстве случаев предпочтительнее.
70. Как внедрить типизацию в большой legacy-проект?
Постепенно: включить mypy/pyright в CI с мягкой конфигурацией, аннотировать сначала публичные API и новые модули, использовать per-module настройки строгости, добавлять stub-пакеты для зависимостей, запретить регресс (новый код — только типизированный). Инструменты вроде MonkeyType помогают сгенерировать аннотации по реальным вызовам.
Производительность и профилирование
Производительность — тема, где вопросы на собеседовании Python Senior проверяют не знание фактов, а умение измерять и принимать решения.
71. Как найти узкое место в производительности?
Сначала измерить, потом оптимизировать. cProfile + snakeviz — детерминированный профайлер по функциям; py-spy — сэмплирующий, подключается к работающему процессу в продакшне без перезапуска; line_profiler — построчно; Scalene — CPU, память и GPU одновременно.
72. Как искать утечки памяти?
tracemalloc — снимки памяти и сравнение их по местам аллокации; memray — детальный профайлер с flame graph; objgraph и gc.get_referrers() — кто держит ссылку на объект. Типичные причины: глобальные кеши без ограничений, lru_cache на методах, незакрытые замыкания, обработчики событий, циклы с __del__.
73. Как ускорить CPU-bound код на Python?
- Улучшить алгоритм и структуры данных — часто даёт больше всего.
- Векторизация через NumPy/Polars.
- Компиляция горячих участков: Cython, Numba, mypyc.
- Расширения на Rust (PyO3, maturin) или C.
- Параллелизм через процессы или free-threaded сборку.
- Альтернативный интерпретатор PyPy.
74. Почему локальные переменные быстрее глобальных?
Локальные переменные хранятся в массиве фрейма и доступны по индексу (LOAD_FAST), а глобальные и встроенные ищутся в словарях (LOAD_GLOBAL). В горячих циклах иногда кешируют атрибут в локальную переменную: append = result.append. В современных версиях разница уменьшилась благодаря специализации.
75. Почему конкатенация строк в цикле медленная?
Строки неизменяемы, поэтому s += x потенциально создаёт новую строку и копирует данные — O(n²) в худшем случае. Правильно — собрать части в список и сделать "".join(parts) или использовать io.StringIO.
76. Есть ли в CPython JIT-компилятор?
Да, начиная с Python 3.13 в CPython есть экспериментальный JIT на основе техники copy-and-patch (PEP 744). Он выключен по умолчанию и включается при сборке; прирост производительности пока скромный. Основной выигрыш последних версий дали специализирующий интерпретатор и оптимизации 3.11+.
77. Какие уровни кеширования вы применяете?
Внутри процесса — functools.cache/lru_cache, cachetools с TTL; между процессами и инстансами — Redis/Memcached; на уровне HTTP — ETag, CDN. Нужно продумать инвалидацию, защиту от cache stampede (блокировки, ранее обновление) и ограничение размера.
78. Как обработать файл размером 50 ГБ?
Потоково: построчная итерация по файлу, генераторы-конвейеры, чтение чанками (pandas.read_csv(chunksize=...), Polars lazy/streaming), mmap для произвольного доступа. Параллельная обработка чанков в процессах, агрегация результатов. Никогда не использовать read() целиком.
79. Как хранить миллионы мелких объектов экономно?
Использовать __slots__ или dataclass(slots=True), кортежи/NamedTuple вместо словарей, модуль array или NumPy-массивы для чисел, колоночное хранение (Arrow, Polars) вместо списка объектов. Разница по памяти может быть в разы.
80. Как правильно проводить бенчмарки?
Использовать timeit (многократные прогоны, отключённый GC) или pyperf для стабильных измерений с прогревом и статистикой. Измерять на реалистичных данных, учитывать разброс, фиксировать версии, не мерить в отладочном режиме и не делать выводов по одному запуску.
Стандартная библиотека Python
81. Какие структуры из collections вы используете?
defaultdict — группировка без проверок ключа; Counter — подсчёт частот и most_common; deque — очередь и скользящее окно (maxlen); OrderedDict — move_to_end для LRU; ChainMap — слоистые настройки (defaults → env → CLI).
82. Когда использовать heapq и bisect?
heapq — приоритетная очередь, top-K элементов (nlargest), слияние отсортированных потоков (merge). bisect — бинарный поиск и вставка в отсортированный список за O(log n) поиска; с 3.10 поддерживает параметр key.
83. Чем отличаются copy и deepcopy?
copy.copy создаёт поверхностную копию — вложенные объекты общие. copy.deepcopy рекурсивно копирует всё дерево, обрабатывая циклы через memo-словарь. Поведение настраивается через __copy__ и __deepcopy__. deepcopy медленный — в горячем коде лучше неизменяемые структуры.
84. Почему 0.1 + 0.2 != 0.3 и как работать с деньгами?
float — двоичное число IEEE 754, десятичные дроби представляются неточно. Для сравнения — math.isclose. Для денег — decimal.Decimal (создавать из строки!) или хранение в целых копейках. fractions.Fraction — точные рациональные числа.
85. Как правильно работать с датами и часовыми поясами?
Использовать только timezone-aware datetime, хранить и передавать время в UTC, конвертировать в локальный пояс при отображении. Часовые пояса — через стандартный zoneinfo (3.9+) вместо pytz. datetime.utcnow() устарел с 3.12 — используйте datetime.now(timezone.utc).
86. Как правильно настроить логирование?
Логгер на модуль: logger = logging.getLogger(__name__); конфигурация — один раз в точке входа (dictConfig). Ленивое форматирование logger.info("user %s", uid), logger.exception для трейсбеков, структурированные JSON-логи (structlog) для продакшна, контекст (request_id) через contextvars. Библиотеки не должны настраивать хендлеры.
87. Почему pathlib лучше os.path?
Объектно-ориентированный кроссплатформенный API: Path("data") / "file.csv", .read_text(), .glob(), .suffix, .stem. Код получается короче и читаемее, меньше ошибок с разделителями путей.
88. Почему pickle небезопасен?
Десериализация pickle может выполнить произвольный код (через __reduce__), поэтому нельзя загружать pickle из недоверенных источников. Для обмена данными — JSON, MessagePack, Protobuf; для внутреннего кеша pickle допустим, но с учётом совместимости версий классов.
Тестирование и архитектура
89. Какие возможности pytest вы используете?
Фикстуры с разными scope (function, module, session) и yield для teardown, conftest.py, @pytest.mark.parametrize, маркеры, tmp_path, monkeypatch, плагины: pytest-xdist (параллельный запуск), pytest-cov, hypothesis (property-based тестирование).
90. Где патчить объект в unittest.mock?
Патчить нужно там, где объект используется, а не где определён. Если в app/service.py написано from requests import get, патчить надо app.service.get. Используйте autospec=True, чтобы мок проверял сигнатуру. Избыточные моки — признак сильной связности кода.
91. Как тестировать асинхронный код?
С помощью pytest-asyncio или плагина anyio: тесты объявляются как async def, фикстуры тоже могут быть асинхронными. Для моков — unittest.mock.AsyncMock. Для HTTP — respx/aioresponses, для БД — реальная тестовая база в контейнере (testcontainers).
92. Как применяются принципы SOLID в Python?
Single Responsibility — небольшие модули и классы; Open/Closed — расширение через стратегии и регистрацию плагинов; Liskov — подклассы не ломают контракты; Interface Segregation — узкие Protocol; Dependency Inversion — зависимость от абстракций, передача зависимостей через конструктор (dependency injection), а не создание внутри.
93. Как бы вы структурировали крупный Python-сервис?
Слоистая или гексагональная архитектура: доменная модель без зависимостей от фреймворков; слой сценариев (use cases / services); адаптеры — репозитории, HTTP-клиенты, брокеры сообщений; тонкий транспортный слой (FastAPI/Django views). Зависимости направлены внутрь, инфраструктура подменяется в тестах. Главное — не переусложнять для маленьких проектов.
94. Какие паттерны проектирования в Python не нужны или выглядят иначе?
Singleton — модуль уже синглтон; Strategy и Command — просто передача функций; Iterator — встроен в язык; Decorator — синтаксис @; Factory — часто достаточно classmethod или словаря классов. Полезны: Repository, Unit of Work, Adapter, Observer, Template Method.
95. Как правильно обрабатывать исключения?
Своя иерархия исключений от базового исключения приложения; перехват конкретных типов, а не голого except:; сохранение цепочки через raise NewError(...) from exc; обработка на правильном уровне (там, где известно, что делать); add_note() (3.11) для контекста. Не использовать исключения для обычного управления потоком в горячем коде.
Продакшн, зависимости и безопасность
96. Как управлять зависимостями в Python-проекте?
Единый pyproject.toml (PEP 621) для метаданных и зависимостей, lock-файл для воспроизводимых сборок, изолированные окружения. Современный стандарт де-факто — uv (быстрый менеджер пакетов и версий Python), также популярны Poetry и PDM. Обязательно: сканирование уязвимостей (pip-audit), автообновление зависимостей (Dependabot/Renovate).
97. Что такое wheel и sdist?
sdist — архив исходников, требующий сборки при установке. wheel — готовый бинарный пакет (.whl), устанавливается без компиляции; для C-расширений собираются отдельные wheel под платформы (manylinux, macOS, Windows) через cibuildwheel. Сборку описывает build-backend в pyproject.toml (hatchling, setuptools, maturin).
98. Чем WSGI отличается от ASGI?
WSGI — синхронный интерфейс «запрос → ответ» (Flask, Django; серверы gunicorn, uWSGI). ASGI — асинхронный, поддерживает WebSocket, long-polling и тысячи соединений в одном процессе (FastAPI, Starlette, Django async; серверы uvicorn, Hypercorn, Granian). Число воркеров обычно подбирается от количества ядер CPU с учётом характера нагрузки.
99. Какие уязвимости типичны для Python-кода?
eval/execот пользовательского ввода;- десериализация
pickleиyaml.loadбезSafeLoader; - SQL-инъекции при форматировании запросов строками вместо параметров;
subprocessсshell=Trueи пользовательскими данными;- модуль
randomдля токенов — нуженsecrets; - path traversal при работе с файлами, секреты в коде и логах, supply-chain атаки через тайпосквоттинг пакетов.
100. Какие изменения в Python 3.12–3.14 важны для продакшна?
- 3.12: синтаксис дженериков PEP 695, улучшенные f-строки (PEP 701), per-interpreter GIL, более точные сообщения об ошибках,
itertools.batched. - 3.13: экспериментальная free-threaded сборка, экспериментальный JIT, новый интерактивный REPL,
TypeIs,ReadOnly. - 3.14: официальная поддержка free-threaded режима, отложенные аннотации (PEP 649), template strings — t-строки (PEP 750),
concurrent.interpreters, инкрементальный GC,forkserverпо умолчанию на Linux, инспекция asyncio-задач.
Это один из самых частых вопросов на собеседовании Python Senior. Важно не просто перечислить новинки, а объяснить, как они повлияют на ваш проект: стоит ли переходить на free-threaded сборку, какие зависимости к этому готовы и как вы бы провели миграцию.
FAQ: вопросы на собеседовании Python Senior и подготовка
Что спрашивают на собеседовании Python Senior-разработчика?
Устройство CPython и управление памятью, GIL и конкурентность (threading, multiprocessing, asyncio), метапрограммирование (дескрипторы, метаклассы), типизацию, профилирование и оптимизацию, архитектуру сервисов, тестирование, а также system design и опыт принятия технических решений. Кроме теории обычно есть live-coding и разбор реальных кейсов из вашего опыта.
Чем собеседование Senior отличается от Middle?
Middle должен уметь правильно использовать инструменты, Senior — объяснять, как они устроены, выбирать между ними с учётом компромиссов и отвечать за последствия решений. Больше вопросов «почему» и «что будет, если», больше system design, архитектуры и опыта наставничества.
Сколько времени нужно на подготовку?
При наличии коммерческого опыта обычно хватает 2–4 недель: 1–2 часа в день на повторение теории по этому списку, решение задач на алгоритмы и 2–3 пробных system design интервью. Составьте список тем, на которые вы не смогли ответить уверенно, и начните с них.
Нужно ли решать алгоритмические задачи на Senior-позицию?
Во многих компаниях — да, особенно в крупных технологических. Как правило, это задачи уровня easy–medium, где важнее чистота кода, анализ сложности и умение обсуждать решения, чем знание редких алгоритмов.
Полезные материалы для подготовки
- Метапрограммирование: метаклассы, декораторы и динамические DSL — подробнее к вопросам 33–51.
- Лучшие практики написания кода на Python (2026).
- Полный гайд по подготовке к собеседованиям по программированию.
- PEP 703 — официальное описание Python без GIL.
- Документация asyncio и Descriptor HowTo Guide.
Заключение
Эти 100 вопросов на собеседовании Python Senior охватывают ключевые темы, которые проверяют технические интервьюеры в 2026 году: от внутреннего устройства CPython и GIL до asyncio, типизации, производительности и архитектуры. Не заучивайте ответы наизусть — разберитесь в механизмах, проверьте примеры кода в интерпретаторе и подготовьте истории из собственного опыта для каждой темы. Сохраните статью в закладки и возвращайтесь к ней перед каждым собеседованием.





