Підготовка до бекенд-співбесіди з Python

Питання та відповіді для співбесіди з Python 2026

Цілеспрямована добірка питань для співбесіди з Python для Backend-розробників. Використовуйте кожну відповідь як структуру для усної розповіді: почніть з концепції, додайте конкретний приклад, а потім згадайте про компроміси.

Почати тестову співбесіду з PythonБанківська картка не потрібна. Доступна 1 безкоштовна сесія.
Практика технічних співбесід англійськоюРежим для тих, для кого англійська не є рідною, щоб тренувати технічні співбесіди.

Базова модель даних Python

1Які вбудовані типи даних є в Python і які з них змінні?

У Python є скалярні типи, зокрема NoneType, bool, int, float і complex; текстові та бінарні типи, зокрема str, bytes і bytearray; типи послідовностей, зокрема list, tuple і range; а також типи колекцій, зокрема dict, set і frozenset. Важлива для співбесіди відмінність — змінність: list, dict, set і bytearray можна змінювати на місці, тоді як числа, bool, str, bytes, tuple, range, frozenset і None є незмінними. Корисний нюанс: власні посилання на елементи tuple зафіксовані, але внутрішній стан змінного об’єкта, на який є посилання, усе одно може змінюватися.

Відповісти на це запитання з ШІ-тренером

2Чим у реальному коді відрізняються list, tuple, set і dict?

list — це впорядкована змінювана послідовність, тож він підходить для черг завдань, зібраних результатів і впорядкованих даних, що змінюються. tuple — впорядкований, але незмінюваний, тож зручний для фіксованих записів, значень, які повертають функції, і даних, що не повинні переприсвоюватися. set зберігає унікальні хешовані об'єкти і ідеальний для перевірок належності або усунення дублікатів. dict зіставляє хешовані ключі зі значеннями і є звичною структурою для індексів, пошуку та даних у стилі JSON. У сучасному Python dict зберігає порядок вставлення, але його головне призначення — усе ще доступ за ключем.

items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
Відповісти на це запитання з ШІ-тренером

3У чому різниця між is і ==?

== перевіряє, чи два об'єкти рівні за значенням, зазвичай через __eq__. is перевіряє, чи два посилання вказують на той самий об'єкт у пам'яті. У виробничому коді is зазвичай варто залишати для перевірок singleton, наприклад value is None, value is True лише коли справді важлива ідентичність, або для sentinel-об'єктів. Не покладайтеся на деталі реалізації на кшталт кешування малих цілих чи інтернування рядків: через них is може здаватися коректним у прикладах, хоча семантично це хибний вибір.

value = None

if value is None:
    print("missing")
Відповісти на це запитання з ШІ-тренером

4Що таке хешований (hashable) об'єкт і чому це важливо?

Хешований об'єкт має хеш-значення, яке залишається незмінним протягом його часу життя, та поведінку перевірки на рівність, яка узгоджується з цим хешем. Хешованість має значення, оскільки від неї залежать ключі `dict` та елементи `set`. Більшість незмінних (immutable) вбудованих типів є хешованими, але не всі об'єкти всередині незмінного контейнера автоматично стають хешованими: наприклад, кортеж (tuple), що містить список (list), не є хешованим. На співбесідах пов'язуйте це з проєктуванням: якщо об'єкт використовується як ключ, поля, які беруть участь у `__eq__` або `__hash__`, не повинні змінюватися після вставлення, інакше об'єкт може стати недоступним у словнику (`dict`) або множині (`set`).

Відповісти на це запитання з ШІ-тренером

5Як працюють зрізи (slicing), враховуючи від'ємні індекси та кроки?

Зріз sequence[start:stop:step] включає start, виключає stop і рухається з кроком step. Пропущені межі виводяться з напрямку кроку. Від'ємні індекси рахують з кінця, тому s[-1] є останнім елементом. Від'ємний крок рухається справа наліво; s[::-1] створює перевернуту послідовність для багатьох типів послідовностей. Компроміс полягає в тому, що взяття зрізу часто створює новий об'єкт, тому цей синтаксис є виразним, але не завжди безкоштовним з точки зору продуктивності для великих обсягів даних.

text = "interview"

print(text[1:5])
print(text[::-1])
Відповісти на це запитання з ШІ-тренером

Функції та область видимості

6Як Python передає аргументи у функції?

Python передає посилання на об'єкти шляхом присвоювання, що часто називають викликом за спільним доступом (call by sharing). Функція отримує локальні імена параметрів, прив'язані до тих самих об'єктів, які передав код виклику. Якщо об'єкт є змінним (mutable), функція може змінити його, і код виклику побачить цю зміну. Якщо функція переприсвоює ім'я параметра, змінюється лише локальна прив'язка. Гарна відповідь на співбесіді чітко розрізняє зміну об'єкта на місці (in-place mutation) від локального переприсвоювання (local rebinding): `items.append(...)` або `items.clear()` змінюють список коду виклику, тоді як `items = []` всередині функції лише спрямовує локальне ім'я на новий список.

def add_item(items):
    items.append("new")

def replace_items(items):
    items = []

values = []
add_item(values)
replace_items(values)

print(values)  # ["new"]
Відповісти на це запитання з ШІ-тренером

7Чому змінні (mutable) аргументи за замовчуванням є небезпечними?

Значення аргументів за замовчуванням обчислюються один раз під час визначення функції, а не щоразу при її виклику. Якщо значенням за замовчуванням є `list` або `dict`, усі виклики можуть використовувати один і той самий об'єкт, що призведе до випадкового витоку стану. Безпечний патерн — це `def f(items=None):`, а потім створення `items = []` всередині, коли `items` дорівнює `None`. Існують рідкісні випадки, коли спільний стан є навмисним, наприклад, невеликий кеш, але тоді це має бути явно вказано та задокументовано, оскільки це може стати несподіванкою для тих, хто читає код.

def add_tag(tag, tags=[]):
    tags.append(tag)
    return tags

print(add_tag("python"))
print(add_tag("backend"))  # Same list reused

def safe_add_tag(tag, tags=None):
    tags = [] if tags is None else tags
    tags.append(tag)
    return tags
Відповісти на це запитання з ШІ-тренером

8Для чого використовуються `*args` та `**kwargs`?

`*args` збирає додаткові позиційні аргументи в кортеж, тоді як `**kwargs` збирає додаткові іменовані аргументи у словник. Вони корисні для обгорток, декораторів, гнучких API (Application Programming Interfaces) та перенаправлення викликів. У місці виклику `*iterable` розгортає позиційні значення, а `**mapping` розгортає іменовані значення. Компромісом є зрозумілість: явні параметри кращі, коли функція має стабільний публічний контракт, тоді як `args` та `kwargs` краще підходять на межах абстракцій, де важливі перенаправлення або сумісність.

def call_api(path, *args, **kwargs):
    print(path, args, kwargs)

call_api("/users", 1, 2, timeout=5)

values = [1, 2]
options = {"timeout": 5}
call_api("/users", *values, **options)
Відповісти на це запитання з ШІ-тренером

9Як Python вирішує імена змінних за правилом LEGB?

Python шукає імена в локальній (Local), охоплюючій (Enclosing), глобальній (Global) та вбудованій (Builtins) областях видимості. Читання імені відбувається саме в такому порядку. Присвоєння імені всередині функції зазвичай створює локальне зв'язування, через що код може викликати помилку `UnboundLocalError`, якщо він читає ім'я до локального присвоєння. Ключове слово `global` перенаправляє присвоєння до області видимості модуля, а `nonlocal` — до охоплюючої області видимості функції. У якісному промисловому коді обома ключовими словами слід користуватися помірно, оскільки вони роблять рух стану (state flow) менш очевидним.

total = 0

def add(value):
    global total
    total += value

def counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment
Відповісти на це запитання з ШІ-тренером

10Що таке замикання (closure) та що таке пізнє зв'язування (late binding)?

Замикання — це функція, яка зберігає доступ до змінних із зовнішньої області видимості після того, як ця зовнішня функція завершила виконання. Це корисно для декораторів, фабрик зворотних викликів (callbacks) та попередньо налаштованих функцій. Пізнє зв'язування означає, що значення вільної змінної шукається під час виклику внутрішньої функції, а не під час її створення. Лямбда-функції, створені в циклі, бачитимуть кінцеве значення циклу. Зафіксувати поточне значення можна за допомогою аргументу за замовчуванням, наприклад, `lambda i=i: i`, або використовуючи невелику фабричну функцію.

callbacks = []

for i in range(3):
    callbacks.append(lambda: i)

print([callback() for callback in callbacks])  # [2, 2, 2]

callbacks = []
for i in range(3):
    callbacks.append(lambda i=i: i)

print([callback() for callback in callbacks])  # [0, 1, 2]
Відповісти на це запитання з ШІ-тренером

ООП, ітерація та керування ресурсами

11Чим змінні класу відрізняються від змінних екземпляра в Python?

Змінна класу зберігається в об'єкті класу і є спільною для всіх екземплярів, якщо тільки екземпляр не перекриває (shadows) її власним атрибутом. Змінна екземпляра зберігається в конкретному об'єкті і зазвичай призначається як `self.name` у методі `__init__`. Незмінні константи класу зазвичай не викликають проблем, але змінні атрибути класу є частою причиною багів, коли їх випадково використовують як стан окремого екземпляра. На співбесіді варто згадати порядок пошуку (lookup order): Python спочатку шукає атрибут в екземплярі, потім у класі та його предках.

class Cart:
    items = []

first = Cart()
second = Cart()

first.items.append("book")
print(second.items)  # ["book"]

class SafeCart:
    def __init__(self):
        self.items = []
Відповісти на це запитання з ШІ-тренером

12Як працюють успадкування, MRO (Method Resolution Order) та super()?

Python вирішує атрибути за допомогою порядку вирішення методів (MRO), який можна побачити через ClassName.__mro__. При одиночному успадкуванні це відбувається прямолінійно, але при множинному успадкуванні Python використовує C3-лінеаризацію для створення узгодженого порядку. super() не означає буквально "викликати батьківський клас"; це означає "викликати наступну реалізацію в MRO". Це уможливлює кооперативне множинне успадкування, коли методи приймають сумісні аргументи, і кожен клас правильно викликає super().

class A:
    def save(self):
        print("A")

class B(A):
    def save(self):
        print("B")
        super().save()

class C(A):
    def save(self):
        print("C")
        super().save()

class D(B, C):
    pass

D().save()  # B, C, A
Відповісти на це запитання з ШІ-тренером

13У чому різниця між ітерованим об'єктом (iterable), ітератором та генератором?

Ітерований об'єкт (iterable) — це будь-який об'єкт, який може повернути ітератор за допомогою iter(), наприклад, список або файловий об'єкт. Ітератор — це об'єкт зі станом, який реалізує __next__ і викликає StopIteration при вичерпанні. Генератор — це зручний спосіб створення ітератора за допомогою yield або виразу-генератора. Генератори ефективно використовують пам'ять, оскільки створюють значення ліниво, але зазвичай вони є однопрохідними (one-shot) і до них не можна звернутися за індексом, як до списку.

def read_lines(path):
    with open(path, encoding="utf-8") as file:
        for line in file:
            yield line.strip()
Відповісти на це запитання з ШІ-тренером

14Що таке менеджер контексту (context manager) і чому оператор `with` є корисним?

Менеджер контексту визначає поведінку під час входу та виходу за допомогою методів `__enter__` та `__exit__`, або його можна створити за допомогою `contextlib.contextmanager`. Оператор `with` робить керування ресурсами явним та надійним: файли закриваються, блокування знімаються, а транзакції можуть бути підтверджені (commit) або скасовані (roll back) навіть тоді, коли виникають винятки. Метод `__exit__` отримує інформацію про виняток і може придушити його, повернувши значення true, хоча придушення помилок слід використовувати обережно.

from contextlib import contextmanager

with open("data.txt", encoding="utf-8") as file:
    data = file.read()

@contextmanager
def temporary_setting():
    print("enter")
    try:
        yield
    finally:
        print("exit")
Відповісти на це запитання з ШІ-тренером

15Чим відрізняються поверхнева копія (shallow copy) та глибока копія (deep copy)?

Поверхнева копія створює новий зовнішній контейнер, але зберігає посилання на ті самі вкладені об'єкти. Глибока копія рекурсивно копіює вкладені об'єкти, коли це можливо. Для простих плоских списків поверхневої копії часто буває достатньо. Однак для вкладених змінюваних структур поверхнева копія все одно використовуватиме спільний стан, що може викликати несподіванки. Глибока копія є більш витратною за ресурсами і може завершитися з помилками `TypeError` або `PicklingError` для таких ресурсів, як відкриті файли, сокети, сесії бази даних або об'єкти з поведінкою, залежною від їхньої ідентичності.

from copy import copy, deepcopy

original = [[1], [2]]
shallow = copy(original)
deep = deepcopy(original)

original[0].append(99)

print(shallow)  # [[1, 99], [2]]
print(deep)     # [[1], [2]]
Відповісти на це запитання з ШІ-тренером

Середовище виконання, асинхронність та код у продакшені

16Що таке GIL (Global Interpreter Lock) і як він впливає на бекенд-код?

У стандартному CPython GIL (Global Interpreter Lock) дозволяє лише одному потоку виконувати байт-код Python в один момент часу. Це означає, що потоки зазвичай не прискорюють код Python, який інтенсивно використовує процесор (CPU-bound). Потоки залишаються корисними для завдань, пов'язаних із вводом/виводом (I/O-bound), оскільки GIL знімається під час очікування багатьох блокувальних операцій. Для завдань із високим навантаженням на CPU (Central Processing Unit) використовуйте багатопроцесорність (`multiprocessing`), нативні розширення, векторизовані нативні бібліотеки або окремі сервіси. Для співбесід у 2026 році також варто згадати збірки CPython без жорсткої прив'язки до потоків (free-threaded) для Python 3.13+: вони роблять GIL необов'язковим для сумісного коду, але все ще вимагають тестування продуктивності та сумісності залежностей.

Відповісти на це запитання з ШІ-тренером

17Коли слід обирати потоки, процеси або `asyncio`?

Використовуйте потоки, коли у вас є блокувальний ввід-вивід (I/O) або бібліотеки, які надають лише синхронні API (Application Programming Interface). Використовуйте процеси для завдань, що інтенсивно використовують процесор (CPU-bound), для ізоляції або обмеження наслідків збоїв, зважаючи на накладні витрати на серіалізацію та використання пам'яті. Використовуйте `asyncio`, коли у вас є багато одночасних операцій вводу-виводу (I/O) та сумісні з асинхронністю бібліотеки, такі як асинхронні драйвери баз даних або HTTP-клієнти. Практичний вибір також залежить від вашого фреймворку та залежностей: змішування асинхронного коду з блокувальними викликами може погіршити продуктивність.

Відповісти на це запитання з ШІ-тренером

18Для чого корисні async/await у вебсервісах на Python?

async та await забезпечують кооперативну багатозадачність. Корутина повертає керування в цикл подій (event loop), коли вона очікує на операції вводу/виводу, дозволяючи виконуватися іншим завданням. Це цінно для API (Application Programming Interfaces), які очікують на бази даних, виклики HTTP (Hypertext Transfer Protocol), черги або сокети. Це не робить магічним чином код, обмежений процесором (CPU-bound), швидшим; робота з високим навантаженням на центральний процесор блокує цикл подій, якщо її не перемістити у робочий потік (worker), процес або нативний код. У FastAPI звичайний def ендпоінт (endpoint) виконується в пулі потоків, тоді як блокувальні виклики всередині async def можуть заблокувати цикл подій; async допомагає лише тоді, коли шлях виклику використовує неблокувальний асинхронний ввід/вивід (async I/O).

import asyncio

async def load_user():
    await asyncio.sleep(0.1)
    return {"id": 1}

async def main():
    user, settings = await asyncio.gather(
        load_user(),
        load_user(),
    )
    return user, settings
Відповісти на це запитання з ШІ-тренером

19Як слід обробляти винятки в production-коді на Python?

Перехоплюйте найспецифічніший виняток, який ви можете обробити, і робіть це на тому рівні, який може відновити роботу, додати контекст або перетворити його на доменну помилку. Уникайте порожніх блоків except і не проковтуйте помилки мовчки, оскільки це приховує втрату даних та проблеми в роботі. Використовуйте raise для повторного генерування того самого винятку і raise NewError(...) from exc, щоб зберегти контекст причинно-наслідкового зв'язку. Хороший production-код також веде логування на межах системи, повертає відповідні помилки HTTP (Hypertext Transfer Protocol) і зберігає повідомлення про винятки корисними, не розкриваючи при цьому секретних даних.

try:
    result = parse_payload(payload)
except ValueError as exc:
    raise InvalidRequest("Invalid payload") from exc
Відповісти на це запитання з ШІ-тренером

20Як би ви структурували та тестували Python-сервіс для продакшену?

Відокремлюйте доменну логіку від коду фреймворку та інфраструктури, щоб основну поведінку можна було тестувати без сервера або бази даних. Використовуйте конфігурацію із середовища або типізовані налаштування, логування замість `print`, впровадження залежностей (dependency injection) на межах системи, а також явні тайм-аути для мережевих викликів. Для тестів поєднуйте модульні тести (unit tests) для чистої логіки, інтеграційні тести для бази даних або зовнішніх меж, та кілька наскрізних (end-to-end) перевірок для критичних потоків виконання. Імітуйте (mock) зовнішні системи на чітких межах, але тестуйте спостережувану поведінку, а не деталі приватної реалізації.

Відповісти на це запитання з ШІ-тренером