Подготовка за бекенд интервю по Python

Въпроси и отговори за интервю по Python за 2026 г.

Фокусиран набор от въпроси за интервю по Python за бекенд разработчици. Използвайте всеки отговор като структура за устно изложение: започнете с концепцията, добавете конкретен пример и след това споменете компромисите.

Започнете пробно интервю по 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` обикновено трябва да се запази за проверки на сингълтън стойности като `value is None`, `value is True` (само когато идентичността наистина има значение) или за специални сигнални обекти (sentinel objects). Не разчитайте на детайли от имплементацията като кеширане на малки цели числа или интерниране на низове (string interning); те могат да създадат илюзията, че `is` работи правилно в дадени примери, докато всъщност това е грешен семантичен избор.

value = None

if value is None:
    print("missing")
Опитайте да отговорите на този въпрос с треньор с ИИ

4Какво е хешируем обект и защо е от значение?

Хешируемият обект има хеш стойност, която остава непроменена през целия му живот, и поведение за проверка на равенство, съответстващо на този хеш. Хешируемостта е важна, защото ключовете в `dict` и елементите в `set` зависят от нея. Повечето неизменяеми вградени типове са хешируеми, но не всички обекти в един неизменяем контейнер автоматично стават хешируеми: например `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). Функцията получава локални имена на параметри, свързани със същите обекти, предоставени от извикващия код. Ако обектът е изменяем, функцията може да го промени и извикващият код ще забележи тази промяна. Ако функцията присвои нова стойност на името на параметъра, се променя само локалната връзка. Един добър отговор по време на интервю трябва да разграничава директната промяна на обекта от локалното пренасочване на променливата: `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Защо изменяемите аргументи по подразбиране са опасни?

Стойностите на аргументите по подразбиране се изчисляват еднократно при дефинирането на функцията, а не при всяко нейно извикване. Ако стойността по подразбиране е списък или речник, всички извиквания могат да споделят един и същ обект и случайно да доведат до изтичане на състояние. Безопасният модел е `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` събира допълнителните позиционни аргументи в кортеж (`tuple`), докато `**kwargs` събира допълнителните именувани аргументи в речник (`dict`). Те са полезни за обвиващи функции, декоратори, гъвкави програмни интерфейси и пренасочване на извиквания. На мястото на извикване `*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 пренасочва присвояването към обграждащата област на видимост на функцията. В качествен код за продукционна среда използвайте и двете пестеливо, тъй като те правят потока на състоянието по-малко очевиден.

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)?

Затварянето е функция, която запазва достъп до променливи от обхващащата я област на видимост, след като външната функция е приключила своето изпълнение. Това е полезно за декоратори, фабрики за обратни извиквания и конфигурирани функции. Късно свързване означава, че свободната променлива се търси, когато вътрешната функция бъде извикана, а не когато е създадена. Ламбда функциите, създадени в цикъл, могат да виждат крайната стойност на цикъла. Захванете текущата стойност с аргумент по подразбиране, например 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По какво се различават променливите на класа от променливите на инстанцията?

Променливата на класа се съхранява в обекта на класа и се споделя от инстанциите, освен ако дадена инстанция не я засенчи със собствен атрибут. Променливата на инстанцията се съхранява в конкретен обект, обикновено присвоена като `self.name` в `__init__`. Неизменяемите константи на класа обикновено не представляват проблем, но изменяемите атрибути на класа са често срещана грешка, когато случайно се използват като състояние за всяка отделна инстанция. По време на интервю е добре да споменете реда на търсене: 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Каква е разликата между итерируем обект, итератор и генератор?

Итерируем обект е всеки обект, който може да върне итератор от iter(), като например списък или файлов обект. Итераторът е обект със състояние, който имплементира __next__ и хвърля StopIteration, когато се изчерпи. Генераторът е удобен начин за създаване на итератор чрез yield или генераторен израз. Генераторите са ефективни по отношение на паметта, защото произвеждат стойности отложено, но обикновено са за еднократна употреба и не могат да бъдат индексирани като списък.

def read_lines(path):
    with open(path, encoding="utf-8") as file:
        for line in file:
            yield line.strip()
Опитайте да отговорите на този въпрос с треньор с ИИ

14Какво е мениджър на контекст и защо операторът `with` е полезен?

Мениджърът на контекст дефинира поведение при влизане и излизане чрез методите `__enter__` и `__exit__`, или може да бъде създаден с `contextlib.contextmanager`. Операторът `with` прави управлението на ресурсите явно и надеждно: файловете се затварят, ключалките се освобождават и транзакциите могат да бъдат потвърдени или отменени, дори когато възникнат изключения. Методът `__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) позволява само на една нишка да изпълнява Python байткод в даден момент. Това означава, че нишките обикновено не ускоряват Python код, който е силно зависим от процесора. Нишките все пак са полезни за задачи, свързани с входно-изходни операции, тъй като GIL се освобождава при изчакване на множество блокиращи операции. За интензивна процесорна работа използвайте мултипроцесинг, нейтив разширения, векторизирани нейтив библиотеки или отделни услуги. За интервюта през 2026 г. споменете и `free-threaded` компилациите на `CPython` от Python 3.13+: те правят GIL опционален за съвместим код, но все пак изискват тестване на зависимостите и производителността.

Опитайте да отговорите на този въпрос с треньор с ИИ

17Кога трябва да изберете нишки, процеси или asyncio?

Използвайте нишки, когато имате блокиращи входно-изходни операции или библиотеки, които предоставят само синхронни API интерфейси. Използвайте процеси за задачи, интензивни за процесора, за изолация или ограничаване на сривове, като приемете допълнителните разходи за сериализация и памет. Използвайте asyncio, когато имате много едновременни входно-изходни операции и съвместими с асинхронен код библиотеки, като например асинхронни драйвери за бази данни или HTTP клиенти. Практическият избор зависи също така от използваната работна рамка и зависимости: смесването на асинхронен код с блокиращи извиквания може да влоши производителността.

Опитайте да отговорите на този въпрос с треньор с ИИ

18За какво са полезни `async/await` в уеб услугите на Python?

`async` и `await` позволяват кооперативна паралелност. Корутината връща контрола на цикъла на събитията, когато изчаква входно-изходни операции, позволявайки на други задачи да се изпълняват. Това е ценно за API интерфейси, които изчакват бази данни, HTTP заявки, опашки или сокети. Това не прави магически по-бърз кода, ограничен от процесора; тежката за процесора работа блокира цикъла на събитията, освен ако не бъде преместена в работна нишка, отделен процес или платформено-зависим код. Във FastAPI обикновена `def` крайна точка се изпълнява в пул от нишки, докато блокиращите извиквания вътре в `async def` могат да блокират цикъла на събитията; `async` помага само когато пътят на извикване използва неблокиращ асинхронен вход-изход.

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Как трябва да се обработват изключенията в Python код за продукционна среда?

Прихващайте най-специфичното изключение, което можете да обработите, и го прихващайте на слой, който може да се възстанови от него, да добави контекст или да го преобразува в домейн грешка. Избягвайте `except` без указан тип и безшумно поглъщане на грешки, тъй като това прикрива загуба на данни и оперативни проблеми. Използвайте `raise`, за да хвърлите отново същото изключение, и `raise NewError(...) from exc`, за да запазите причинно-следствения контекст. Добрият продукционен код също така логва на границите на модулите, връща подходящи HTTP грешки и запазва съобщенията за изключенията полезни, без да издава чувствителна информация.

try:
    result = parse_payload(payload)
except ValueError as exc:
    raise InvalidRequest("Invalid payload") from exc
Опитайте да отговорите на този въпрос с треньор с ИИ

20Как бихте структурирали и тествали Python услуга за продукционна среда?

Разделете бизнес логиката от кода на рамката и инфраструктурата, за да може основното поведение да се тества без сървър или база данни. Използвайте конфигурация от средата или типизирани настройки, логиране вместо `print`, внедряване на зависимости по границите на системата и изрични времена за изчакване за мрежови извиквания. За тестовете комбинирайте единични тестове за чистата логика, интеграционни тестове за базата данни или външните услуги, и няколко теста от край до край за критичните пътища. Замествайте с mock обекти външните системи на ясно определени граници, но тествайте наблюдаемото поведение, а не скритите детайли на имплементацията.

Опитайте да отговорите на този въпрос с треньор с ИИ