Prog Academy
RU UA
Питання на співбесіді Python-розробника: 36 питань із відповідями
Кар'єра в IT

Питання на співбесіді Python-розробника: 36 питань із відповідями

На співбесіді Python Junior перевіряють чотири речі: чи розумієте ви, як влаштовані типи даних і функції, чи володієте ООП та стандартними інструментами мови, чи можете написати запит до бази даних і тест, і чи здатні міркувати вголос над невеликою задачею. Нижче — 36 питань із короткими відповідями, згруповані за сімома темами, з кодом, який можна запустити й перевірити самостійно, а також практичні задачі, типові помилки кандидатів і план підготовки на два тижні.

Я викладаю програмування понад 12 років і веду в Prog Academy курси з Java та Python. Перед співбесідами ми зі студентами розбираємо саме такі питання: на них швидко видно, чи розуміє людина механіку мови, чи вивчила визначення. Список — мій викладацький добір, а не рейтинг за частотою: у конкретній компанії набір питань буде свій, але теми повторюються.

Актуально на жовтень 2026 року

Приклади коду запущено на 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(), dataclassJunior; MRO та __hash__ — ближче до Middle
Ітератори та генераториПротокол ітерації, yield, включення, ліниві обчисленняJunior
Декоратори та контекстні менеджериЯк влаштовані, навіщо functools.wraps, withJunior; з параметрами — Middle
Винятки та середовищеtry/except/else/finally, власні винятки, venv, анотації типівJunior
КонкурентністьGIL, потоки, процеси, asyncioJunior — загальне розуміння; Middle — докладно
SQL та ORMJOIN, GROUP BY, параметризовані запити, проблема N+1Junior; оптимізація запитів — 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__.

PY

Ці теми — у програмі курсу 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 та решту підключають окремо.

КритерійDjangoFastAPI
ПризначенняВебзастосунки повністю: сторінки, форми, адмінка, APIAPI та сервіси
Робота з базою данихВбудована 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
9GIL та asyncioСвоїми словами пояснити, коли потрібні потоки, процеси та asyncio; запустити приклад із gather
10–11SQL, 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 →

Фото автора: Олександр Цимбалюк

Про автора

Тренер Prog Academy з Java та Python

Викладає програмування понад 12 років, автор офлайн- та онлайн-курсів Prog Academy з Java і Python. У блозі пише про Java, Python та аналіз даних на Python.

Контакт

Записатися на консультацію

Вкажіть актуальний номер, ми зателефонуємо в будь яку країну :)