Вопросы на собеседовании Python-разработчика: 36 вопросов с ответами
На собеседовании Python Junior проверяют четыре вещи: понимаете ли вы, как устроены типы данных и функции, владеете ли ООП и стандартными инструментами языка, можете ли написать запрос к базе данных и тест, и способны ли рассуждать вслух над небольшой задачей. Ниже — 36 вопросов с короткими ответами, сгруппированные по семи темам, с кодом, который можно запустить и проверить самому, а также практические задачи, типичные ошибки кандидатов и план подготовки на две недели.
Я преподаю программирование больше 12 лет и веду в Prog Academy курсы по Java и Python. Перед собеседованиями мы со студентами разбираем именно такие вопросы: на них быстро видно, понимает человек механику языка или выучил определение. Список — мой преподавательский отбор, а не рейтинг по частоте: в конкретной компании набор вопросов будет своим, но темы повторяются.
Примеры кода запущены на Python 3.12.2; тесты pytest 8.3.2 и пример с ORM на Django 4.2.1 — на Python 3.9.6. Вывод в комментариях — реальный. Описанное поведение одинаково во всех поддерживаемых версиях Python 3; там, где версия важна, она названа. Фрагмент FastAPI приведён по официальной документации. Отбор вопросов — авторский, это не статистика собеседований.
Как проходит собеседование Python-разработчика?
Обычно отбор состоит из четырёх этапов: короткий разговор с рекрутером, техническое интервью, практическое задание и финальная встреча с руководителем или командой. Состав и порядок этапов зависят от компании: где-то тестовое задание дают до технического интервью, где-то его заменяет live coding. Уточняйте процесс у рекрутера на первом же созвоне.
| Этап | Что проверяют | Как готовиться |
|---|---|---|
| Скрининг с рекрутером | Соответствие вакансии, мотивацию, ожидания по зарплате и формату работы, английский | Рассказ о себе на две минуты, понятный ответ «почему Python и почему эта компания», заранее продуманная вилка |
| Техническое интервью | Знание языка, SQL, основ веба и тестирования; умение объяснять | Проговорить вслух ответы на вопросы из этой статьи, каждый пример запустить самому |
| Практическое задание | Умение написать работающий код: live coding или тестовое задание на дом | Решать небольшие задачи с таймером и вслух; для тестового — README, тесты, чистая история коммитов |
| Финальная встреча | Как вы работаете в команде, реагируете на замечания, чего хотите от работы | Примеры из учёбы и проектов: что не получалось и как вы это решили; вопросы к команде |
Про зарплатные ожидания лучше говорить с цифрами под рукой: медианные зарплаты по уровням собраны на странице зарплаты Python-разработчиков. Как выглядит путь до первого собеседования целиком, разобрано в статье как стать Python-разработчиком с нуля.
Какие темы проверяют на Junior и Middle?
На уровне Junior проверяют основы языка, ООП, SQL и умение написать простой тест; на Middle к этому добавляются конкурентность, производительность и архитектурные решения. Граница условна: в таблице указан уровень, начиная с которого тему, по моему опыту, спрашивают всерьёз.
| Тема | Что проверяют | Уровень |
|---|---|---|
| Типы данных и изменяемость | Изменяемые и неизменяемые типы, == и is, копирование, хешируемость | Junior |
| Функции | Передача аргументов, *args и **kwargs, значения по умолчанию, замыкания, области видимости | Junior |
| ООП | Классы и экземпляры, dunder-методы, наследование, super(), dataclass | Junior; MRO и __hash__ — ближе к Middle |
| Итераторы и генераторы | Протокол итерации, yield, включения, ленивые вычисления | Junior |
| Декораторы и контекстные менеджеры | Как устроены, зачем functools.wraps, with | Junior; с параметрами — Middle |
| Исключения и окружение | try/except/else/finally, свои исключения, venv, аннотации типов | Junior |
| Конкурентность | GIL, потоки, процессы, asyncio | Junior — общее понимание; Middle — подробно |
| SQL и ORM | JOIN, GROUP BY, параметризованные запросы, проблема N+1 | Junior; оптимизация запросов — Middle |
| Веб | HTTP-методы и коды ответа, REST, Django и FastAPI на уровне концепций | Junior |
| Тестирование | pytest: тесты, фикстуры, параметризация | Junior |
Типы данных и изменяемость
С этого блока начинается почти любое техническое интервью по Python: изменяемость объясняет половину «странного» поведения языка. Первоисточник — раздел Data model документации.
Какие типы данных в Python изменяемые, а какие нет?
Неизменяемые — int, float, bool, str, tuple, frozenset, bytes; изменяемые — list, dict, set, bytearray и по умолчанию экземпляры ваших классов. Изменяемый объект можно поменять на месте, и это увидят все переменные, которые на него ссылаются. «Изменение» неизменяемого объекта на самом деле создаёт новый.
a = [1, 2]
b = a # второе имя того же списка
b.append(3)
print(a) # [1, 2, 3]
s = "py"
t = s
t += "thon" # создаётся новая строка
print(s, t) # py python
Что хотят услышать: переменная в Python — это имя, привязанное к объекту, а не «коробка со значением». Присваивание ничего не копирует.
Чем отличается == от is?
== сравнивает значения (вызывает метод __eq__), is проверяет, что это один и тот же объект в памяти. Два разных списка с одинаковым содержимым равны, но не идентичны.
a = [1, 2]
b = [1, 2]
c = a
print(a == b) # True
print(a is b) # False
print(a is c) # True
is используют для сравнения с None: if value is None. Типичная ошибка — сравнивать через is числа и строки: иногда это «работает», потому что CPython переиспользует некоторые объекты, но это деталь реализации, на которую нельзя полагаться.
Чем список отличается от кортежа?
Список изменяемый, кортеж — нет: после создания в кортеже нельзя заменить, добавить или удалить элемент. Поэтому кортеж можно использовать как ключ словаря (если все его элементы хешируемы), а список нельзя. По смыслу список — это набор однородных элементов переменной длины, кортеж — запись фиксированной структуры, например координаты.
point = (1, [2, 3])
point[1].append(4) # сам кортеж не меняется, меняется список внутри него
print(point) # (1, [2, 3, 4])
point[0] = 10 # TypeError: 'tuple' object does not support item assignment
Что хотят услышать: неизменяемость кортежа касается только ссылок, которые он хранит. Кортеж со списком внутри остаётся кортежем, но перестаёт быть хешируемым.
Что может быть ключом словаря?
Ключом может быть любой хешируемый объект: у него есть хеш, который не меняется в течение жизни объекта, и его можно сравнить с другими через ==. Это числа, строки, кортежи из хешируемых элементов, frozenset. Списки, словари и множества ключами быть не могут.
prices = {("Kyiv", "Lviv"): 540}
print(prices[("Kyiv", "Lviv")]) # 540
prices = {["Kyiv", "Lviv"]: 540} # TypeError: unhashable type: 'list'
Те же требования у элементов множества. Требование вытекает из устройства словаря: это хеш-таблица, и позиция элемента вычисляется по хешу ключа.
Чем поверхностная копия отличается от глубокой?
Поверхностная копия создаёт новый контейнер, но кладёт в него ссылки на те же вложенные объекты; глубокая рекурсивно копирует и вложенные объекты. Разница заметна только при вложенных изменяемых структурах. Подробности — в документации модуля copy.
import copy
original = [[1, 2], [3]]
shallow = copy.copy(original) # то же, что original.copy() или original[:]
deep = copy.deepcopy(original)
original[0].append(99)
print(shallow) # [[1, 2, 99], [3]]
print(deep) # [[1, 2], [3]]
Сохраняет ли словарь порядок элементов и сколько стоит поиск по ключу?
Да: начиная с Python 3.7 словарь гарантированно хранит ключи в порядке добавления — это записано в документации dict. Поиск, вставка и удаление по ключу в словаре и множестве в среднем выполняются за O(1), а проверка x in some_list — за O(n), потому что список приходится просматривать целиком.
d = {"b": 1, "a": 2}
d["c"] = 3
print(list(d)) # ['b', 'a', 'c']
print({"a": 1, "b": 2} == {"b": 2, "a": 1}) # True — порядок не влияет на равенство
Множество порядок не гарантирует. Практический вывод: если в цикле вы много раз проверяете «есть ли элемент», превратите список в множество.
Функции, аргументы и замыкания
В этом блоке проверяют, понимаете ли вы, что происходит с объектом при передаче в функцию и когда вычисляются значения по умолчанию.
Как Python передаёт аргументы в функцию: по ссылке или по значению?
Ни то ни другое в чистом виде: аргументы передаются присваиванием. Параметр функции становится ещё одним именем того же объекта. Если объект изменяемый и функция меняет его на месте, это видно снаружи; если функция привязывает параметр к новому объекту, вызывающий код этого не заметит. Формулировка взята из официального FAQ.
def add_item(items):
items.append(1) # меняет объект вызывающего кода
def rebind(items):
items = [0] # перепривязывает только локальное имя
data = []
add_item(data)
rebind(data)
print(data) # [1]
Что такое *args и **kwargs?
*args собирает лишние позиционные аргументы в кортеж, **kwargs — лишние именованные в словарь. Имена args и kwargs — соглашение, работают звёздочки. При вызове те же звёздочки делают обратное: распаковывают последовательность и словарь в аргументы.
def report(title, *args, sep=", ", **kwargs):
print(title, args, sep, kwargs)
report("Sales", 10, 20, sep="; ", debug=True)
# Sales (10, 20) ; {'debug': True}
nums = [1, 2, 3]
options = {"sep": "-"}
print(*nums, **options) # 1-2-3
Параметры после *args (здесь sep) можно передать только по имени. Чаще всего *args, **kwargs встречаются в декораторах — чтобы обёртка принимала любые аргументы.
Почему нельзя использовать изменяемый объект как значение аргумента по умолчанию?
Потому что значение по умолчанию вычисляется один раз — в момент определения функции, а не при каждом вызове. Список, заданный по умолчанию, становится общим для всех вызовов и накапливает данные. Это описано в учебнике Python.
def add(item, bucket=[]):
bucket.append(item)
return bucket
print(add(1)) # [1]
print(add(2)) # [1, 2] — тот же список, что и в первом вызове
def add_fixed(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(add_fixed(1)) # [1]
print(add_fixed(2)) # [2]
Что хотят услышать: причину (момент вычисления) и стандартное решение — None по умолчанию и создание объекта внутри функции.
Что такое замыкание?
Замыкание — это вложенная функция, которая запоминает переменные из внешней функции и продолжает ими пользоваться после того, как внешняя функция завершилась. На замыканиях построены декораторы и фабрики функций.
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter = make_counter()
print(counter(), counter()) # 1 2
Типичная ловушка — позднее связывание: замыкание хранит не значение переменной, а саму переменную, и читает её в момент вызова.
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2]
funcs = [lambda i=i: i for i in range(3)] # значение фиксируется как аргумент по умолчанию
print([f() for f in funcs]) # [0, 1, 2]
Как Python ищет переменные и зачем нужны global и nonlocal?
Имя ищется по правилу LEGB: сначала в локальной области функции (Local), затем во внешних функциях (Enclosing), затем в модуле (Global), затем среди встроенных имён (Built-in). Читать внешнюю переменную можно свободно, но любое присваивание внутри функции делает имя локальным для всей функции. global и nonlocal говорят интерпретатору, что присваивать нужно переменной модуля или внешней функции.
total = 0
def broken_inc():
total += 1 # UnboundLocalError: имя total считается локальным
def inc():
global total
total += 1
inc()
print(total) # 1
Типичная ошибка: решать задачу через global. На собеседовании лучше сказать, что изменяемое глобальное состояние усложняет тестирование, и передать значение аргументом.
ООП: классы, dunder-методы, наследование
Здесь проверяют не определения инкапсуляции и полиморфизма, а то, как объектная модель работает в самом Python.
Чем атрибут класса отличается от атрибута экземпляра?
Атрибут класса хранится в самом классе и общий для всех экземпляров; атрибут экземпляра создаётся (обычно в __init__) для каждого объекта отдельно. При чтении obj.attr Python сначала ищет атрибут в экземпляре, потом в классе и его родителях.
class Team:
members = [] # общий список для всех экземпляров
def __init__(self, name):
self.name = name # свой у каждого экземпляра
a = Team("backend")
b = Team("frontend")
a.members.append("Anna")
print(b.members) # ['Anna']
Это та же ловушка, что и с изменяемым аргументом по умолчанию. Исправление — создавать список в __init__: self.members = [].
Что такое dunder-методы и чем __str__ отличается от __repr__?
Dunder-методы (double underscore, «магические») — это методы вида __init__, __len__, __eq__, которые Python вызывает сам в ответ на синтаксис языка: создание объекта, len(x), x == y, x + y. __repr__ — однозначное представление для разработчика, __str__ — читаемое для пользователя; если __str__ не определён, используется __repr__.
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __repr__(self):
return f"Money({self.amount!r}, {self.currency!r})"
def __str__(self):
return f"{self.amount} {self.currency}"
def __add__(self, other):
return Money(self.amount + other.amount, self.currency)
price = Money(10, "USD")
print(price) # 10 USD
print([price]) # [Money(10, 'USD')] — контейнеры показывают repr
print(price + Money(5, "USD")) # 15 USD
Что такое MRO и как работает super()?
MRO (method resolution order) — порядок, в котором Python обходит классы при поиске метода. Он строится алгоритмом C3 и доступен в атрибуте __mro__. super() вызывает метод не «родителя», а следующего класса в MRO — при множественном наследовании это может быть «сосед», а не предок. Алгоритм описан в документации.
class A:
def hello(self):
return "A"
class B(A):
def hello(self):
return "B > " + super().hello()
class C(A):
def hello(self):
return "C > " + super().hello()
class D(B, C):
pass
print(D().hello()) # B > C > A
print([cls.__name__ for cls in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']
Что хотят услышать: из B вызов super().hello() попал в C, хотя B от C не наследуется, — потому что порядок определяется MRO класса объекта, то есть D.
Чем отличаются @staticmethod, @classmethod и @property?
Обычный метод получает первым аргументом экземпляр (self). @classmethod получает класс (cls) и чаще всего служит альтернативным конструктором. @staticmethod не получает ни того ни другого — это обычная функция, которая лежит в классе по смыслу. @property превращает метод в вычисляемый атрибут, к которому обращаются без скобок.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32
@classmethod
def from_fahrenheit(cls, value):
return cls((value - 32) * 5 / 9)
@staticmethod
def is_valid(celsius):
return celsius >= -273.15
t = Temperature.from_fahrenheit(212)
print(t.celsius, t.fahrenheit) # 100.0 212.0
print(Temperature.is_valid(-300)) # False
Свойство без сеттера доступно только для чтения: присваивание t.fahrenheit = 1 вызовет AttributeError.
Что такое dataclass и что он генерирует?
Декоратор @dataclass по аннотациям полей генерирует __init__, __repr__ и __eq__, избавляя от шаблонного кода в классах, которые в основном хранят данные. Параметр frozen=True запрещает менять поля после создания и делает объект хешируемым, order=True добавляет методы сравнения. Полный список — в документации dataclasses.
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Point:
x: int
y: int = 0
@dataclass
class Order:
id: int
items: list[str] = field(default_factory=list)
print(Point(1)) # Point(x=1, y=0)
print(Point(1) == Point(1, 0)) # True
print(Order(7)) # Order(id=7, items=[])
Изменяемое значение по умолчанию dataclass задать не даст: items: list = [] вызывает ValueError и подсказывает использовать default_factory — защита от ловушки из блока о функциях.
Что произойдёт, если определить __eq__, но не определить __hash__?
Экземпляры класса перестанут быть хешируемыми: Python автоматически установит __hash__ в None, и объект нельзя будет положить в множество или сделать ключом словаря. Так язык защищает правило: равные объекты обязаны иметь одинаковый хеш. Оно описано в разделе о __hash__.
class User:
def __init__(self, email):
self.email = email
def __eq__(self, other):
return isinstance(other, User) and self.email == other.email
print(User("[email protected]") == User("[email protected]")) # True
users = {User("[email protected]")} # TypeError: unhashable type: 'User'
Чтобы вернуть хешируемость, определите __hash__ по тем же полям, что участвуют в сравнении: return hash(self.email). Эти поля после этого не должны меняться. Вопрос ближе к уровню Middle.
Итераторы, генераторы, декораторы, контекстные менеджеры
Это инструменты, по которым отличают человека, писавшего на Python, от человека, читавшего о нём: их используют в каждом рабочем проекте.
Чем итерируемый объект отличается от итератора?
Итерируемый объект (iterable) умеет отдать итератор — у него есть метод __iter__. Итератор — это объект с методом __next__, который выдаёт следующий элемент и бросает StopIteration, когда элементы закончились. Список — итерируемый, но не итератор; по нему можно пройти много раз, а итератор одноразовый. Протокол описан в документации.
numbers = [1, 2]
iterator = iter(numbers)
print(next(iterator), next(iterator)) # 1 2
next(iterator) # StopIteration
print(iter(iterator) is iterator) # True — итератор возвращает сам себя
print(iter(numbers) is numbers) # False — список каждый раз создаёт новый итератор
Цикл for делает именно это: вызывает iter(), затем next() до StopIteration.
Что такое генератор и чем yield отличается от return?
Генератор — это функция с yield: при вызове она не выполняется, а возвращает итератор. return завершает функцию, а yield отдаёт значение и приостанавливает её, сохраняя локальные переменные до следующего next(). Значения вычисляются по одному, поэтому генератор не держит всю последовательность в памяти.
def countdown(n):
while n > 0:
yield n
n -= 1
gen = countdown(3)
print(next(gen)) # 3
print(list(gen)) # [2, 1]
print(list(gen)) # [] — генератор исчерпан
Что хотят услышать: где это полезно — чтение большого файла построчно, постраничная выгрузка из API, конвейеры обработки данных.
Чем списковое включение отличается от генераторного выражения?
Списковое включение [...] сразу создаёт весь список в памяти; генераторное выражение (...) создаёт ленивый итератор, который вычисляет элементы по мере запроса и проходится один раз. Синтаксис отличается только скобками.
import sys
squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(squares_list) > sys.getsizeof(squares_gen)) # True
print(sum(squares_gen)) # 333332833333500000
print(sum(squares_gen)) # 0 — второй раз пройти нельзя
words = ["api", "orm", "api"]
print({w: len(w) for w in words}) # {'api': 3, 'orm': 3} — включение словаря
Если результат нужен один раз — для sum, any, max — берите генераторное выражение. Если по нему нужно ходить несколько раз или обращаться по индексу — список.
Что такое декоратор и зачем нужен functools.wraps?
Декоратор — это функция, которая принимает функцию и возвращает новую, обычно обёртку с дополнительным поведением: логированием, проверкой прав, кешированием, повтором при ошибке. Запись @log_calls над функцией равносильна add = log_calls(add). functools.wraps копирует в обёртку имя и документацию исходной функции — без него add.__name__ вернул бы wrapper.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
print(f"{func.__name__}{args} -> {result}")
return result
return wrapper
@log_calls
def add(a, b):
"""Return the sum of two numbers."""
return a + b
add(2, 3) # add(2, 3) -> 5
print(add.__name__) # add
Декоратор с параметрами — это ещё один уровень вложенности: функция, которая возвращает декоратор.
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(times=2)
def ping():
print("ping")
ping() # ping выводится дважды
Что такое контекстный менеджер и как работает with?
Контекстный менеджер — объект с методами __enter__ и __exit__. Оператор with вызывает __enter__ на входе в блок и гарантированно вызывает __exit__ на выходе, даже если внутри возникло исключение. Так закрывают файлы, соединения и блокировки. Проще всего написать свой через contextlib.contextmanager.
from contextlib import contextmanager
class Connection:
def __enter__(self):
print("open")
return self
def __exit__(self, exc_type, exc, traceback):
print("close")
return False # не подавлять исключение
@contextmanager
def connection():
print("open")
try:
yield
finally:
print("close")
with Connection():
print("work")
# open
# work
# close
Обе формы равноценны: во втором варианте код до yield играет роль __enter__, а блок finally — роль __exit__.
Эти темы — в программе курса Python
Изменяемые и неизменяемые типы, *args и **kwargs, итераторы и генераторы, декораторы, исключения, ООП и наследование, SQL и Django входят в модули курса Python в Prog Academy: AI Start, Python Start, Python для Data Analytics, Python + Django и SQL. Курс длится 4,5 месяца и заканчивается дипломным проектом для портфолио.
Программа курса Python →Исключения, модули, окружение и типизация
Блок о том, как код ведёт себя при ошибках и как устроен проект вокруг кода: модули, зависимости, аннотации типов.
Как работают try, except, else и finally?
try содержит код, который может упасть; except обрабатывает указанный тип исключения; else выполняется, только если исключения не было; finally выполняется всегда — и после ошибки, и после return. Подробно — в главе Errors and Exceptions учебника.
def parse(value):
try:
number = int(value)
except ValueError as error:
print("error:", error)
return None
else:
print("parsed")
return number
finally:
print("finally")
print(parse("42"))
# parsed
# finally
# 42
print(parse("abc"))
# error: invalid literal for int() with base 10: 'abc'
# finally
# None
Что хотят услышать: зачем нужен else — чтобы в try оставался только тот код, ошибку которого вы действительно хотите перехватить.
Почему нельзя писать голый except и как создать своё исключение?
Голый except: перехватывает всё, включая KeyboardInterrupt и SystemExit, которые наследуются от BaseException, а не от Exception: программу становится невозможно остановить, а настоящие ошибки прячутся. Перехватывайте конкретные типы. Своё исключение — это класс, унаследованный от Exception; иерархия встроенных исключений есть в документации.
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
super().__init__(f"balance {balance}, requested {amount}")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
withdraw(100, 250)
except InsufficientFundsError as error:
print(error, "| missing:", error.amount - error.balance)
# balance 100, requested 250 | missing: 150
Типичная ошибка: except Exception: pass. Проглоченное исключение хуже упавшей программы: ошибка есть, а следов нет.
Что делает конструкция if __name__ == "__main__"?
Она отделяет код, который должен выполняться только при запуске файла как программы, от кода, который выполняется при импорте. Когда файл запускают напрямую, переменная __name__ равна "__main__"; когда его импортируют — имени модуля. См. документацию __main__.
def main():
print("run as script")
if __name__ == "__main__":
main()
При запуске python m.py функция main() выполнится, при import m — нет. Без этой проверки импорт модуля в тестах запускал бы всю программу.
Зачем нужно виртуальное окружение и чем venv отличается от pip?
Виртуальное окружение — это изолированный каталог со своим интерпретатором и своими пакетами, чтобы зависимости одного проекта не конфликтовали с другим и с системным Python. venv создаёт окружение, pip устанавливает в него пакеты — это разные инструменты, которые работают в паре. Модуль venv входит в стандартную библиотеку.
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -r requirements.txt
python -m pip freeze > requirements.txt
Каталог .venv в Git не коммитят: в репозитории лежит список зависимостей (requirements.txt или pyproject.toml), по которому окружение воссоздаётся на любой машине. Основы работы с репозиторием — в статье Git для начинающих.
Что такое аннотации типов и проверяет ли их Python во время выполнения?
Аннотации — это подсказки о типах параметров и результата функции. Сам интерпретатор их не проверяет: в документации typing прямо сказано, что среда выполнения не применяет аннотации. Их читают статические анализаторы (mypy, pyright), редактор кода и библиотеки вроде Pydantic и FastAPI.
def find_user(user_id: int) -> dict[str, str] | None:
users = {1: {"name": "Anna"}}
return users.get(user_id)
print(find_user(1)) # {'name': 'Anna'}
print(find_user("abc")) # None — строка вместо int, но ошибки нет
Запись X | None доступна с Python 3.10, а dict[str, str] без импорта из typing — с 3.9. Что хотят услышать: Python остаётся языком с динамической типизацией, аннотации — инструмент для людей и анализаторов.
GIL, потоки, процессы и asyncio
От Junior здесь ждут общего понимания — какой инструмент для какой задачи; подробности про синхронизацию и устройство цикла событий спрашивают уже у Middle.
Что такое GIL?
GIL (global interpreter lock) — это блокировка в интерпретаторе CPython, из-за которой в каждый момент времени байт-код Python выполняет только один поток. Она упрощает управление памятью, но не даёт потокам ускорять вычисления на нескольких ядрах. На время блокирующего ввода-вывода (сеть, диск) поток GIL отпускает, поэтому для таких задач потоки полезны. Определение — в глоссарии документации.
Важное уточнение для 2026 года: начиная с Python 3.13 существует отдельная сборка CPython без GIL (free-threaded), в 3.14 она стала официально поддерживаемой, но остаётся опциональной — обычная сборка по-прежнему работает с GIL. Подробности — в руководстве Python support for free threading.
Типичная ошибка: «из-за GIL в Python нет многопоточности». Потоки есть и работают; ограничение касается параллельного выполнения Python-кода на нескольких ядрах.
Чем отличаются threading, multiprocessing и asyncio?
Потоки и asyncio подходят для задач, которые в основном ждут ввода-вывода; процессы — для тяжёлых вычислений. Потоки переключает операционная система, корутины asyncio переключаются сами в точках await внутри одного потока, а у каждого процесса свой интерпретатор и своя память, поэтому GIL им не мешает.
| Инструмент | Для каких задач | Особенности |
|---|---|---|
threading | Ввод-вывод: запросы к API, файлы, база данных | Общая память, нужны блокировки для общих данных; вычисления на Python не ускоряет из-за GIL |
multiprocessing | Вычисления, нагружающие процессор | Отдельные процессы и память; запуск и передача данных между процессами стоят дороже |
asyncio | Много одновременных сетевых операций | Один поток, кооперативное переключение; нужны асинхронные библиотеки и нельзя блокировать цикл событий |
На практике потоки и процессы чаще запускают не напрямую, а через пулы из модуля concurrent.futures: ThreadPoolExecutor и ProcessPoolExecutor.
Что такое корутина и что делает await?
Корутина — это функция, объявленная через async def; её вызов не выполняет код, а возвращает объект-корутину, который должен запустить цикл событий. await приостанавливает текущую корутину до готовности результата и на это время отдаёт управление циклу событий, чтобы тот выполнял другие задачи. Справка — в документации asyncio.
import asyncio
import time
async def fetch(name, delay):
await asyncio.sleep(delay) # имитация сетевого запроса
return name
async def main():
start = time.perf_counter()
results = await asyncio.gather(fetch("users", 1), fetch("orders", 1))
print(results, round(time.perf_counter() - start))
asyncio.run(main()) # ['users', 'orders'] 1
Два «запроса» по секунде выполнились за одну секунду, а не за две. Если заменить await asyncio.sleep(delay) на блокирующий time.sleep(delay), тот же код отработает за две секунды: блокирующий вызов останавливает весь цикл событий. Блокирующие функции выносят в поток через asyncio.to_thread(). Углублённо тема разобрана в курсе асинхронный Python.
SQL, ORM, веб и тестирование
Python-разработчик почти всегда работает с базой данных и HTTP, поэтому этот блок спрашивают даже на позиции без опыта. Если SQL нужно повторить с начала, поможет статья SQL для начинающих.
Чем INNER JOIN отличается от LEFT JOIN?
INNER JOIN возвращает только строки, для которых нашлось соответствие в обеих таблицах. LEFT JOIN возвращает все строки левой таблицы, а там, где соответствия справа нет, подставляет NULL. Пример запущен через встроенный модуль sqlite3.
import sqlite3
con = sqlite3.connect(":memory:")
con.executescript("""
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, total INTEGER);
INSERT INTO users VALUES (1, 'Anna'), (2, 'Oleh');
INSERT INTO orders VALUES (1, 1, 300), (2, 1, 200);
""")
inner = "SELECT u.name, o.total FROM users u INNER JOIN orders o ON o.user_id = u.id ORDER BY o.id"
left = "SELECT u.name, o.total FROM users u LEFT JOIN orders o ON o.user_id = u.id ORDER BY u.id, o.id"
print(con.execute(inner).fetchall()) # [('Anna', 300), ('Anna', 200)]
print(con.execute(left).fetchall()) # [('Anna', 300), ('Anna', 200), ('Oleh', None)]
Частое продолжение вопроса — «посчитайте заказы и сумму по каждому пользователю, включая тех, у кого заказов нет»:
SELECT u.name, COUNT(o.id) AS orders, COALESCE(SUM(o.total), 0) AS total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY total DESC;
-- Anna | 2 | 500
-- Oleh | 0 | 0
Что такое SQL-инъекция и как от неё защититься?
SQL-инъекция — это уязвимость, при которой данные пользователя подставляются в текст запроса и выполняются как часть SQL. Защита — параметризованные запросы: значения передаются отдельно от текста запроса, и драйвер базы данных сам их экранирует. Никогда не собирайте запрос через f-строку или конкатенацию; об этом предупреждает и документация sqlite3.
name = "x' OR '1'='1"
unsafe = f"SELECT * FROM users WHERE name = '{name}'"
print(len(con.execute(unsafe).fetchall())) # 2 — вернулись все пользователи
safe = "SELECT * FROM users WHERE name = ?"
print(len(con.execute(safe, (name,)).fetchall())) # 0
Пример продолжает предыдущий: в таблице users две строки. ORM строит параметризованные запросы сам, но это не защищает код, в котором «сырой» SQL собран из строк.
Что такое ORM и что такое проблема N+1?
ORM (object-relational mapping) — это слой, который отображает таблицы на классы, а строки — на объекты, чтобы работать с базой данных через код на Python, а не через текст запросов. Примеры — Django ORM и SQLAlchemy. Проблема N+1 возникает, когда один запрос получает список объектов, а затем для каждого объекта выполняется ещё один запрос за связанными данными.
# N+1: один запрос за книгами и по одному запросу на автора каждой книги
for book in Book.objects.all():
print(book.title, book.author.name)
# один запрос с JOIN
for book in Book.objects.select_related("author"):
print(book.title, book.author.name)
На тестовых данных из трёх книг первый цикл выполнил 4 запроса, второй — 1. Здесь Book — модель Django с внешним ключом author. Для связей «один ко многим» и «многие ко многим» в Django служит prefetch_related; оба метода описаны в справочнике QuerySet.
Чем отличаются методы HTTP и что такое REST?
GET читает ресурс, POST создаёт новый, PUT заменяет целиком, PATCH изменяет частично, DELETE удаляет. REST — это стиль построения API, в котором адрес обозначает ресурс (/users/42), действие задаётся методом HTTP, сервер не хранит состояние клиента между запросами, а результат сообщается кодом ответа.
- Идемпотентность. Повтор
GET,PUTиDELETEдаёт то же состояние сервера, что и один запрос;POSTиPATCHидемпотентными не считаются. Определения — в RFC 9110. - Коды ответа. 2xx — успех (200 OK, 201 Created, 204 No Content), 3xx — перенаправление, 4xx — ошибка клиента (400, 401, 403, 404, 422), 5xx — ошибка сервера (500).
- 401 и 403. 401 — сервер не знает, кто вы (нет или неверные учётные данные); 403 — знает, но прав на действие у вас нет.
Чем Django отличается от FastAPI?
Django — фреймворк «всё включено»: ORM, миграции, админ-панель, авторизация и шаблоны идут в комплекте. FastAPI — лёгкий фреймворк для API: он опирается на аннотации типов, проверяет по ним входные данные, сам строит документацию OpenAPI и изначально рассчитан на асинхронный код; ORM и остальное подключают отдельно.
| Критерий | Django | FastAPI |
|---|---|---|
| Назначение | Веб-приложения целиком: страницы, формы, админка, API | API и сервисы |
| Работа с базой данных | Встроенная ORM и миграции | Не входит; обычно подключают SQLAlchemy |
| Проверка данных | Формы и сериализаторы | Модели Pydantic по аннотациям типов |
| Асинхронность | Поддерживается, но исторически фреймворк синхронный | Основной режим работы |
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
Этот минимальный пример повторяет учебник FastAPI: аннотация item_id: int заставляет фреймворк преобразовать и проверить параметр пути. Что хотят услышать: не «что лучше», а под какие задачи подходит каждый и на чём вы писали сами.
Что такое фикстуры и параметризация в pytest?
Фикстура — это функция с декоратором @pytest.fixture, которая готовит данные или окружение для теста; тест получает её результат, указав имя фикстуры в параметрах. Параметризация запускает один и тот же тест на нескольких наборах данных. Обе возможности описаны в документации pytest: fixtures и parametrize.
import pytest
def apply_discount(price, percent):
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return price * (100 - percent) / 100
@pytest.fixture
def cart():
return {"apple": 2, "pear": 1}
def test_cart_counts_items(cart):
assert sum(cart.values()) == 3
@pytest.mark.parametrize(
"price, percent, expected",
[(100, 0, 100), (100, 25, 75), (80, 50, 40)],
)
def test_apply_discount(price, percent, expected):
assert apply_discount(price, percent) == expected
def test_invalid_percent():
with pytest.raises(ValueError):
apply_discount(100, 150)
Команда python -m pytest находит файлы test_*.py и сообщает 5 passed: параметризованный тест считается за три. Что такое модульное тестирование и чем оно отличается от интеграционного, объяснено в глоссарии.
Практические задачи с решениями
На live coding чаще дают не алгоритмические головоломки, а небольшие задачи на словари, строки и списки — проверяют, знаете ли вы стандартную библиотеку и думаете ли о крайних случаях. Ниже четыре задачи такого типа; решения запущены, вывод в комментариях реальный.
Задача 1. Найти самые частые слова в тексте
Нужно вернуть n самых частых слов без учёта регистра и знаков препинания. Ожидаемое решение — collections.Counter; вручную то же делается словарём со счётчиками.
import re
from collections import Counter
def top_words(text, n=3):
words = re.findall(r"[a-z']+", text.lower())
return Counter(words).most_common(n)
print(top_words("The cat and the dog. The end, and THE start!", 2))
# [('the', 4), ('and', 2)]
Задача 2. Найти два числа с заданной суммой
Дан список чисел и целевая сумма; вернуть индексы двух элементов, которые её дают. Решение «в лоб» — два вложенных цикла, O(n²). Ожидаемое решение — один проход со словарём «число → индекс», O(n).
def two_sum(numbers, target):
seen = {}
for index, number in enumerate(numbers):
if target - number in seen:
return seen[target - number], index
seen[number] = index
return None
print(two_sum([2, 7, 11, 15], 9)) # (0, 1)
print(two_sum([3, 3], 6)) # (0, 1)
print(two_sum([1, 2], 10)) # None
Задача 3. Проверить баланс скобок
Строка содержит скобки трёх видов; определить, правильно ли они расставлены. Классическая задача на стек: открывающую скобку кладём, закрывающую сверяем с вершиной.
def is_balanced(text):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for char in text:
if char in pairs.values():
stack.append(char)
elif char in pairs:
if not stack or stack.pop() != pairs[char]:
return False
return not stack
print(is_balanced("{[()]}"), is_balanced("([)]"), is_balanced("(("), is_balanced(""))
# True False False True
Задача 4. Развернуть вложенный список
Из списка произвольной вложенности получить плоскую последовательность. Задача проверяет рекурсию и генераторы: yield from передаёт наружу все значения вложенного генератора.
def flatten(items):
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
print(list(flatten([1, [2, [3, 4]], [], 5]))) # [1, 2, 3, 4, 5]
В каждой задаче после решения назовите сложность и крайние случаи: пустой ввод, один элемент, повторяющиеся значения. Потренироваться на основах можно в бесплатном курсе по Python.
Типичные ошибки кандидатов
Чаще всего кандидатов подводит не незнание отдельного факта, а манера отвечать: заученные определения без примера и молчание во время решения задачи. Список — из моего опыта подготовки студентов.
- Определение без примера. «Декоратор — это функция, которая принимает функцию» звучит верно, но интервьюер ждёт, что вы тут же напишете простейший декоратор.
- Молчаливое решение задачи. Оценивают ход мысли. Проговаривайте: что уточняете, какой план, почему выбрали словарь, а не список.
- Код без крайних случаев. Пустой список,
None, отрицательные числа, повторяющиеся значения — назовите их раньше, чем о них спросят. - Угадывание вместо «не знаю». Уверенная выдумка вскрывается следующим вопросом и подрывает доверие к остальным ответам.
- Чужой проект в портфолио. Код, сгенерированный ассистентом или переписанный с урока, который вы не можете объяснить построчно.
- Пробел в SQL. Сильный Python не спасает, если кандидат не может написать
JOINсGROUP BY. - Путаница в базовых терминах. Аргумент и параметр, итератор и итерируемый объект, поток и процесс, 401 и 403.
- Спор с интервьюером вместо проверки. Если не согласны — предложите запустить код и посмотреть.
- Отсутствие вопросов к компании. Это читается как отсутствие интереса к работе.
Какие вопросы задать интервьюеру?
Спрашивайте о том, как будет выглядеть ваша работа в первые месяцы: задачи, наставник, процесс ревью и критерии оценки. Эти ответы помогают сравнить предложения и показывают, что вы выбираете осознанно.
- Какими задачами я буду заниматься в первые один-три месяца?
- Будет ли у меня наставник и как устроено введение в проект?
- Как проходит код-ревью и кто ревьюит код начинающих разработчиков?
- Какие версии Python и какие фреймворки используются в проекте? Есть ли тесты и как они запускаются?
- Как устроен рабочий процесс: спринты, планирование, релизы?
- По каким критериям оценивают результат испытательного срока?
- Как в команде относятся к использованию AI-ассистентов в работе?
- Как выглядит рост от Junior до Middle в вашей компании и что для этого нужно показать?
- Какие следующие шаги после этого собеседования и когда ждать ответа?
План подготовки на две недели
Двух недель по одному-два часа в день хватает, чтобы повторить темы и натренировать ответы вслух, — при условии, что сам язык вы уже изучали. Это ориентир преподавателя, а не норматив: подстраивайте план под свои слабые места.
| Дни | Тема | Что сделать |
|---|---|---|
| 1–2 | Типы данных и изменяемость | Запустить все примеры блока, изменить их и предсказать вывод до запуска |
| 3–4 | Функции и замыкания | Написать по памяти функцию с *args и **kwargs, счётчик на замыкании, объяснить ловушку со списком по умолчанию |
| 5–6 | ООП | Спроектировать три-четыре связанных класса, добавить __repr__, __eq__, dataclass; нарисовать MRO для ромбовидного наследования |
| 7 | Итераторы, генераторы, декораторы | Написать генератор для чтения файла построчно и два декоратора: с параметрами и без |
| 8 | Исключения, модули, типы | Добавить в свой проект собственное исключение, аннотации типов и проверить структуру requirements.txt |
| 9 | GIL и asyncio | Своими словами объяснить, когда нужны потоки, процессы и asyncio; запустить пример с gather |
| 10–11 | SQL, ORM, HTTP | Десять запросов с JOIN и GROUP BY на своей базе; повторить методы и коды ответа; найти N+1 в своём проекте |
| 12 | Тестирование | Покрыть тестами pytest один модуль проекта: фикстура, параметризация, проверка исключения |
| 13 | Практические задачи | Решить четыре задачи из этой статьи и ещё пять похожих с таймером и вслух |
| 14 | Проект и пробное интервью | Рассказать о проекте за пять минут, пройти пробное собеседование с другом или записать себя на видео |
Если вы ещё выбираете первый язык, а не готовитесь к конкретному собеседованию, начните со сравнения Python или JavaScript: что учить первым.
Частые вопросы
Сколько времени нужно на подготовку к собеседованию на Junior Python-разработчика?
Если основы языка, SQL и свой проект уже есть, на повторение и тренировку ответов вслух обычно хватает двух недель по одному-два часа в день. Если тем из этой статьи вы ещё не проходили, это уже не подготовка к собеседованию, а обучение, и оно занимает месяцы. Это оценка автора как преподавателя, а не статистика.
Будет ли на собеседовании live coding?
Скорее всего да, в том или ином виде: небольшая задача в общем редакторе, разбор готового фрагмента кода или вопрос «что выведет эта программа». Формат зависит от компании, поэтому его стоит уточнить у рекрутера заранее. Готовьтесь решать задачи вслух: сначала уточнить условие, потом назвать идею и только после этого писать код.
Нужно ли знать алгоритмы и структуры данных на собеседовании Junior?
На базовом уровне да: чем список отличается от словаря и множества, сколько стоит поиск в каждом из них, что такое стек и очередь, как оценить сложность цикла и вложенного цикла. Сложные алгоритмы на графах и динамическое программирование на позицию Junior спрашивают заметно реже, но это зависит от компании.
Можно ли пользоваться AI-ассистентом на собеседовании?
Только если интервьюер прямо это разрешил. Одни компании просят решать задачу без подсказок, другие, наоборот, смотрят, как кандидат работает с ассистентом и проверяет его ответ. Если правила не названы, спросите в начале. Пользоваться подсказками тайно нельзя: это заметно по ответам и заканчивает собеседование.
Что делать, если не знаю ответа на вопрос?
Сказать об этом прямо и показать ход рассуждения: что вы знаете рядом с этой темой, как бы проверили гипотезу, где посмотрели бы в документации. Честное «с этим не работал, но предположу вот что и вот почему» воспринимается лучше, чем уверенная выдумка, которую интервьюер разберёт следующим же вопросом.
Нужен ли английский на собеседовании Python-разработчика?
Английский для чтения нужен в любом случае: документация, сообщения об ошибках и названия в коде написаны на английском. Разговорную часть проверяют там, где команда или заказчик работают на английском, обычно это несколько минут разговора о себе и своём проекте. Требуемый уровень указан в вакансии, ориентируйтесь на неё.
Стоит ли выполнять тестовое задание?
Да, если объём разумный и вакансия вам интересна: для кандидата без опыта это возможность показать код вместо строчек в резюме. Перед началом уточните срок и критерии оценки. Сдавайте с README, тестами и аккуратной историей коммитов и будьте готовы объяснить каждое решение на следующем этапе.
Что спрашивают про собственные проекты?
Что делает проект, как он устроен, почему выбраны именно эти библиотеки и структура базы данных, что было самым сложным и что вы переделали бы сейчас. Нередко просят открыть репозиторий и объяснить конкретный фрагмент. Поэтому в портфолио должен быть только код, который вы понимаете до последней строки.
Чем собеседование на Middle отличается от собеседования на Junior?
От Junior ждут уверенного знания языка и умения решить небольшую задачу. На Middle к этому добавляются вопросы о том, почему решение устроено именно так: конкурентность и GIL, производительность запросов к базе данных, проектирование API, тестируемость кода, опыт разбора ошибок в работающем сервисе.
Источники
- Python documentation: Data model
- Python documentation: Built-in Types
- Python documentation: copy — Shallow and deep copy operations
- Python documentation: Programming FAQ
- The Python Tutorial: Default Argument Values
- Python documentation: The Python 2.3 Method Resolution Order
- Python documentation: dataclasses — Data Classes
- Python documentation: functools.wraps
- Python documentation: contextlib
- The Python Tutorial: Errors and Exceptions
- Python documentation: Exception hierarchy
- Python documentation: __main__ — Top-level code environment
- Python documentation: venv — Creation of virtual environments
- Python documentation: typing — Support for type hints
- Python documentation: Glossary — global interpreter lock
- Python documentation: Python support for free threading
- Python documentation: asyncio — Asynchronous I/O
- Python documentation: concurrent.futures
- Python documentation: sqlite3
- Django documentation: QuerySet API reference
- FastAPI: Path Parameters
- RFC 9110: HTTP Semantics
- pytest documentation: How to use fixtures
- pytest documentation: How to parametrize fixtures and test functions
