Привет, Хабр! Меня зовут Бартенев Евгений, я автор курса «Python-разработчик» в Яндекс Практикуме и практикующий разработчик. Я прособеседовал сотни кандидатов и сам неоднократно побывал в роли собеседуемого, поэтому хорошо представляю, какие именно знания проверяют на интервью и где чаще всего «проваливаются» не только джуны, но и опытные разработчики.
Первая часть этой статьи вызвала оживлённое обсуждение. В комментариях читатели делились собственным опытом, напоминали о тонкостях, которые я упустил, и задавали отличные уточняющие вопросы. В этой части я продолжу разбирать очередные «подводные камни» в Python.

Как и раньше, я не буду разбирать заезженные вопросы вроде «чем отличается список от кортежа». Вместо этого — разберём реальные особенности Python, на которых строятся самые коварные задачи и вопросы. И это не абстрактные рассуждения — все примеры взяты из реальных собеседований и mock-интервью, которые я провожу публично на своём YouTube-канале с начинающими разработчиками.
Разница между равенством и тождеством
Традиционно начнём с одного из самых популярных и часто задаваемых на собеседованиях вопросов — про различие между == и is. На первый взгляд кажется, что эти два оператора делают одно и то же: сравнивают объекты. Но в Python они работают совершенно по-разному, и путаница здесь случается даже у опытных разработчиков.
Оператор == проверяет равенство значений.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True
Здесь списки a и b равны по содержимому, поэтому результат — True.
А вот оператор is проверяет тождественность объектов: указывают ли обе переменные на один и тот же объект в памяти.
a = [1, 2, 3]
b = [1, 2, 3]
print(a is b) # False
Хотя содержимое совпадает, это два разных объекта, созданных независимо.
Правило простое:
используйте
==, когда хотите проверить, что значения совпадают;используйте
is, только если нужно проверить, что это один и тот же объект (например,value is None).
На собеседовании такой вопрос специально задают, чтобы проверить, понимаете ли вы разницу между равенством и тождеством в Python.
Цепочные присваивания
Практически для каждого начинающего разработчика запись вида a = b = c = 10 кажется чем-то особенным. Чуть более опытные из них обычно предполагают, что тут одновременно создаются три независимые переменные с одним значением. Однако в Python это всего лишь синтаксический сахар.
Фактически этот код работает так: интерпретатор выполняет операции присваивания справа налево.
c = 10
b = c
a = b
То есть:
10создаётся как объект в памяти,cссылается на этот объект,bполучает ту же ссылку,aтоже получает эту ссылку.
В результате все три переменные указывают на один и тот же объект.
Тем не менее для изменяемых и неизменяемых типов данных результат будет отличаться. Например, для списка будет так:
a = b = c = [1, 2, 3]
a.append(4)
print(b, c) # [1, 2, 3, 4] [1, 2, 3, 4]
Все три переменные ссылаются на один и тот же список, и изменения через любую из них видны через остальные. Потому что список — изменяемый объект.
Если необходимо создать независимые списки, пусть даже с одинаковыми значениями, присваивайте их явно:
a, b, c = [1, 2, 3], [1, 2, 3], [1, 2, 3]
Для неизменяемых объектов (числа, строки, кортежи) эта особенность не работает:
a = b = c = 10
a += 1
print(a, b, c) # 11 10 10
Здесь a += 1 создаёт новый объект со значением 11 и переназначает a, а ссылки для b и c остаются на старом объекте 10.
Итоги:
Запись
a = b = c = valueне создаёт три независимых объекта, а просто назначает одну ссылку трём переменным.Для изменяемых объектов это может привести к неожиданным последствиям.
Хорошая практика — использовать такую запись только с неизменяемыми типами (числа, строки, кортежи).
Особенность функции round()
Функция round() в Python кажется простой: округляет число до ближайшего целого (или до указанного количества знаков). Но у новичков она часто вызывает недоумение, когда речь заходит о значениях вида 3.5, 4.5 и так далее.
Попробуем:
print(round(3.5)) # 4
print(round(4.5)) # 4 (!)
Вспоминая математику, кажется, что оба результата должны быть округлены вверх — к 4 и 5. Но Python (как и многие другие языки и библиотеки) использует не «школьное округление», а так называемое «банковское округление» (или round half to even).
Суть в том, что если число оканчивается на .5, Python округляет его к ближайшему чётному целому:
3.5 → 4 (чётное)
4.5 → 4 (чётное)
5.5 → 6 (чётное)
Зачем так сделано?
Школьное правило «.5 — всегда вверх» приводит к статистическому смещению: среднее значение при большом количестве округлений систематически завышается. Банковское округление устраняет эту проблему и используется в математике, статистике и финансовых расчётах.
Если же вам нужно именно «обычное» округление «вверх при .5», можно использовать decimal.Decimal с нужным режимом округления:
from decimal import Decimal, ROUND_HALF_UP
print(Decimal("3.5").quantize(0, rounding=ROUND_HALF_UP)) # 4
print(Decimal("4.5").quantize(0, rounding=ROUND_HALF_UP)) # 5
Итоги:
Функция
round()в Python использует банковское округление (до ближайшего чётного).Это важно помнить, особенно в задачах с подсчётами или статистикой.
Для классического округления вверх используйте
decimalи нужный режим.
Конструкция try / except / finally и возврат из функции
Конструкция try / except / finally в Python знакома почти всем: она нужна для обработки ошибок и выполнения финального блока кода. Но не все знают, что произойдёт, если внутри try и finally одновременно указана инструкция return. Это как раз тот случай, когда код работает не так, как ожидаешь.
Рассмотрим пример:
def func():
try:
return "из try"
finally:
return "из finally"
print(func()) # ???
Известно, что инструкция return останавливает выполнение функции. На первый взгляд кажется, что функция должна вернуть из try, ведь мы попадаем туда первыми. Но результат будет из finally.
Почему так?
В Python блок finally выполняется всегда — даже если в try уже был return. При этом return из finally имеет приоритет: он перезаписывает результат из try.
А что если return есть только в try, а в finally просто какой-то код?
def func():
try:
return "из try"
finally:
print("финальный блок")
print(func())
То вас ждёт снова неожиданный результат.
финальный блок
из try
То есть:
Возврат из
tryфиксируется.finallyвсё равно выполняется.После этого функция действительно возвращает значение из
try.
Главные правила:
Блок
finallyвыполняется всегда: даже если вtryестьreturn,break,continueили даже исключение.Если и в
try, и вfinallyестьreturn, то сработаетreturnизfinally.Если в
finallyнетreturn, то значение изtryсохраняется, но блокfinallyбудет выполнен первым.
Поэтому хороший стиль — не писать return внутри finally. Это почти всегда делает код менее предсказуемым и может «поглотить» результат из try или даже исключение из except.
Функции str(o), repr(o) и методы o.str(), o.repr()
Начинающему разработчику обычно кажется, что функции str() и repr() в Python делают одно и то же — превращают объект в строку. Но если копнуть глубже, окажется, что за ними скрываются разные цели, разные магические методы, а поведение в реальных ситуациях может заметно отличаться.
Начнём с простого примера:
x = 42
print(str(x)) # 42
print(repr(x)) # 42
На целых числах разницы нет — оба вернули строку 42.
Но если изначально взять строку, то результат будет другим:
s = "hello"
print(str(s)) # hello
print(repr(s)) # 'hello'
Видите разницу? Функция repr() вернула строку с кавычками.
То есть работают они по-разному.
Обе функции под капотом обращаются к специальным методам: str(o) вызывает o.__str__(), а repr(o) вызывает o.__repr__().
Если__str__() не определён, Python пытается использовать __repr__(). А если не определён и он, то вернётся стандартная строка вида <MyClass object at 0x...>.
Пример:
class User:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Пользователь {self.name}"
def __repr__(self):
return f"User(name={self.name!r})"
u = User("Alice")
print(str(u)) # Пользователь Alice
print(repr(u)) # User(name='Alice')
Однако у repr() есть особая цель: она должна возвращать такое строковое представление, которое максимально точно описывает объект и, по возможности, может быть использовано для его восстановления при помощи функции eval().
Иными словами, чтобы по этой строке было понятно, какой именно объект скрывается внутри. На практике этого правила придерживаются не всегда, но именно такова изначальная идея.
Даже если вы не пишете свои классы, с этими функциями вы сталкиваетесь постоянно. Например, функция print() вызывает именно str(), а интерпретатор Python в интерактивной консоли показывает результат через repr().
x = "hello"
print(x) # hello → str
x # 'hello' → repr
Различие проявляется и в форматировании строк:
s = "hi"
print(f"{s}") # hi → использует str
print(f"{s!r}") # 'hi' → использует repr
Итоги:
str()— «красивое» строковое представление объекта.repr()— «техническое» строковое представление объекта.В интерактивной консоли и при отладке чаще работает
repr().
Хорошая практика — в __repr__() возвращать максимально информативную строку, пригодную для отладки, а в __str__() — удобную для чтения.
На собеседовании этот вопрос обычно задают, чтобы проверить, понимаете ли вы, почему два разных метода нужны одновременно.
Функции exec() и eval(): скрытые ловушки
В Python есть две малоиспользуемые, но очень коварные функции — exec() и eval(). На собеседованиях их любят упоминать, потому что они вроде бы делают простую вещь — исполняют строку как код, но работают по-разному и таят несколько подводных камней.
Функция eval(expr) вычисляет строковое выражение и возвращает результат. Оно подходит только для выражений — то есть того, что может вернуть значение: арифметика, литералы, вызовы функций.
eval("2 + 3 * 4") # 14
Функция exec(code) исполняет строку как полноценный фрагмент Python-кода. Внутри могут быть циклы, условные операторы, объявления функций и классов. Но в отличие от eval, функция ничего не возвращает.
code = """
def f(x):
return x * 2
"""
result = exec(code)
print(result) # None
Чтобы получить конкретные данные, нужно работать через пространство имён, например вот так.
ns = {}
exec("a = 1 + 2", ns)
print(ns["a"]) # 3
Важно помнить, что обе функции исполняют строку как Python-код. Если туда попадёт вредоносный код от пользователя, то вы получите уязвимость уровня code injection. На практике это почти всегда означает «так делать нельзя».
Однако если очень хочется, то можно. В интервью это часто проверяют вопросом про безопасные альтернативы. Так, для безопасного разбора простых строк-литералов используют функцию ast.literal_eval.
Функция literal_eval разбирает строку как Python-код и разрешает только литералы: числа, строки, кортежи, списки, словари, булевы значения и None. Если в строке есть что-то исполняемое (функция, вызов, импорт и т. п.), literal_eval выбросит исключение ValueError или SyntaxError.
Поэтому, в отличие от eval(), функция literal_eval безопасна для разбора данных, пришедших извне.
import ast
print(ast.literal_eval("{'a': 1, 'b': [2, 3]}"))
# {'a': 1, 'b': [2, 3]}
# Опасная попытка выполнить код
print(ast.literal_eval("__import__('os').system('rm -rf /')"))
# ValueError: malformed node or string
И ещё один момент. В примерах с функцией repr() я рассказал, что она возвращает «машинное» представление объекта. В идеале оно должно быть таким, чтобы по нему можно было воссоздать объект через eval().
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={self.name!r})"
object_1 = User("Alice")
print(object_1) # User(name='Alice')
object_2 = eval(repr(object_1))
print(object_2) # User(name='Alice')
Но важно помнить:
Это правило не универсальное. У многих объектов
repr()даёт лишь отладочную строку<MyClass object at 0x...>, а не конструктор.Даже если
repr()выглядит как корректный код, использованиеeval()остаётся небезопасным, если строка приходит из ненадёжного источника.
Итоги:
eval()вычисляет выражение и возвращает результат.exec()исполняет произвольный код, но сам по себе ничего не возвращает.Оба инструмента исполняют строки как код, и это делает их потенциально опасными.
На собеседованиях ждут, что вы знаете про эти функции, понимаете их различия и риски, а также умеете объяснить, когда их можно использовать, а когда — нельзя.
Неочевидный else в циклах
Условный оператор else в Python известен всем, но далеко не все знают, что его можно использовать и вместе с циклами for и while. На собеседованиях про это часто спрашивают именно потому, что это выглядит непривычно для разработчиков из других языков.
Посмотрим на пример:
for i in range(3):
print(i)
else:
print("Цикл завершён")
Вывод:
0
1
2
Цикл завершён
Что произошло?
Блок else в цикле выполняется, только если цикл завершился нормально, без break.
Теперь добавим break:
for i in range(3):
if i == 1:
break
print(i)
else:
print("Цикл завершён")
Вывод:
0
Блок else не выполнился, потому что цикл прервался раньше времени.
Где это может быть полезно? Самый классический пример — поиск элемента:
nums = [2, 4, 6, 8]
for n in nums:
if n % 2 != 0:
print("Нашли нечётное:", n)
break
else:
print("Все числа чётные")
Здесь else выполнится лишь в том случае, когда цикл перебрал весь список и не нашёл нечётных значений.
Итоги:
У
forиwhileповедениеelseабсолютно одинаковое.Инструкция
elseпосле цикла выполняется, если цикл завершился безbreak.Это удобно для «поисковых» сценариев: нашли элемент →
break; не нашли → отработалelse.
Такая конструкция делает код компактным и читаемым, хотя для новичков выглядит непривычно.
Почему во множестве не могут одновременно существовать True и 1
Множества в Python устроены так, что они могут содержать различные по типу, но только уникальные элементы. Однако уникальность определяется не только по значению, но и по правилам сравнения объектов.
Здесь и кроется неожиданность:
s = {True, 1}
print(s) # {True}
Хотя кажется, что мы добавили два разных элемента, в итоге остался только один. То же самое произойдёт с False и 0:
s = {False, 0}
print(s) # {False}
Python оставляет тот, который был добавлен первым.
Почему так?
В Python логический тип bool — это всего лишь подкласс int.
print(isinstance(True, int)) # True
print(True == 1) # True
print(False == 0) # True
То есть True и 1 равны с точки зрения сравнения (==), а множества (как и словари) используют именно равенство и хеши для проверки уникальности. У этих значений совпадают и ==, и hash():
print(hash(True), hash(1)) # Одинаковые числа.
print(hash(False), hash(0)) # Одинаковые числа.
Поэтому в множестве они считаются одним и тем же элементом.
Итоги:
В множествах
Trueи1— это одно и то же.То же самое с
Falseи0.Причина в том, что bool наследуется от int и у этих значений одинаковые
==иhash().
На собеседовании это часто спрашивают, чтобы проверить, знаете ли вы, что True и 1 в Python — не «два разных значения», а фактически одна сущность с разным представлением.
Обычное и целочисленное деление: / и //
На собеседованиях любят проверять не только работу со строками и коллекциями, но и простейшую арифметику в Python. Особенно часто кандидаты путаются с целочисленным делением и его поведением на отрицательных числах.
Начнём с простого:
оператор
/всегда выполняет обычное деление и возвращает float;оператор
//выполняет целочисленное деление с округлением вниз (floor division).
Пример:
print(5 / 2) # 2.5
print(5 // 2) # 2
Но с отрицательными числами ситуация неожиданная:
print(-3 / 2) # -1.5
print(-3 // 2) # -2 (!)
Почему так?
Оператор // не просто отбрасывает дробную часть, а именно округляет результат вниз (к минус бесконечности).
Для положительных чисел «вниз» совпадает с «в сторону нуля», поэтому разницы мы не видим. А для отрицательных чисел «вниз» означает «ещё меньше», то есть значение будет меньше ожидаемого «школьного» усечения.
Чтобы убедиться:
print(-5 // 2) # -3
print(-5 / 2) # -2.5
Итоги:
/всегда возвращает float.//делает целочисленное деление с округлением вниз (к -∞).
Поэтому -3 // 2 == -2, а не -1.
На собеседованиях это любят спрашивать, потому что большинство кандидатов автоматически думают, что // — это просто «отбрасывание дробной части». На самом деле это не так.
Порядок элементов в словарях: мифы и реальность
Ещё один классический вопрос для собеседования — про порядок элементов в словаре. Многим наверняка знакомо утверждение: «В словарях порядок ключей не гарантирован, на него нельзя полагаться». И раньше это действительно было так. Но ситуация изменилась.
В Python до версии 3.6 словари реализовывались как хэш-таблицы, и порядок элементов зависел только от внутреннего расположения ключей в памяти. Поэтому результат был непредсказуемым.
Начиная с Python 3.7 (а в CPython 3.6 — как побочный эффект реализации) порядок вставки ключей сохраняется, и Python гарантирует сохранность этого порядка.
Теперь:
d = {"a": 1, "b": 2, "c": 3}
print(d) # {'a': 1, 'b': 2, 'c': 3}
Ключи будут выводиться в том порядке, в котором они были добавлены.
Теперь можно безопасно полагаться на порядок словаря — например, при сериализации данных в JSON.
Итоги:
До Python 3.6 порядок словаря был непредсказуемым.
С Python 3.7 порядок вставки ключей гарантирован на уровне языка.
На собеседованиях это отличный вопрос: многие до сих пор уверены, что «в словарях порядок случайный».
Вниз по кроличьей норе
В процессе подготовки к собеседованию легко попасть в ловушку — делать фокус на готовые вопросы и пытаться заучить «правильные ответы». Велика вероятность, что этот подход не сработает: интервьюеры придумывают десятки вариаций на одну и ту же тему, и выехать на шаблонных решениях не получится.
Куда важнее фокусироваться не на вопросах, а на самих особенностях языка. Ведь каждая особенность Python — будь то работа с изменяемыми объектами, тонкости деления или поведение try/ finally — это фундамент, на котором можно построить огромное количество разных задач. Понимая принцип, вы сможете ответить на любой вариант вопроса, даже если формулировка окажется неожиданной.
Поэтому не гонитесь за количеством выученных вопросов. Разбирайтесь в устройстве Python, тренируйтесь находить подвохи и объяснять их своими словами — и тогда даже самые каверзные вопросы не собьют вас с толку.
baldr
А ещё это приведёт к SyntaxWarning исключению в Python 3.14: PEP765 и не просто "хороший стиль", а не рекомендуется официально.
bartenev_ev Автор
Спасибо за комментарий!
Однако добавлю, что это пока не исключение, а предупреждение (SyntaxWarning) — просто на уровне языка теперь обращают внимание на такие случаи.
Вообще, 3.14 принесла много интересных изменений, надо будет отдельно про них написать!