Przygotowanie do rozmowy rekrutacyjnej Python Backend

Pytania i odpowiedzi rekrutacyjne z języka Python (2026)

Skoncentrowany zestaw pytań rekrutacyjnych z języka Python dla backend developerów. Traktuj każdą odpowiedź jako schemat wypowiedzi na rozmowie: zacznij od pojęcia, dodaj konkretny przykład, a na koniec wspomnij o kompromisach technologicznych.

Rozpocznij próbną rozmowę (Python)Karta płatnicza nie jest wymagana. Dostępna 1 darmowa sesja.
Ćwiczenie rozmów technicznych po angielskuTryb dla osób, dla których angielski nie jest językiem ojczystym, do ćwiczenia rozmów technicznych.

Podstawowy model danych Pythona

1Jakie wbudowane typy danych posiada Python i które z nich są mutowalne (zmienne)?

Python posiada typy skalarne, takie jak NoneType, bool, int, float i complex; typy tekstowe i binarne, takie jak str, bytes i bytearray; typy sekwencyjne, takie jak list, tuple i range; oraz typy kolekcji, takie jak dict, set i frozenset. Ważnym z punktu widzenia rozmów kwalifikacyjnych rozróżnieniem jest mutowalność: list, dict, set oraz bytearray mogą być modyfikowane w miejscu, podczas gdy liczby, bool, str, bytes, tuple, range, frozenset i None są niemutowalne. Przydatnym niuansem jest to, że same referencje do elementów wewnątrz krotki (tuple) są stałe, ale stan wewnętrzny wskazywanego, mutowalnego obiektu nadal może ulec zmianie.

Odpowiedz na to pytanie z trenerem AI

2Czym różnią się typy list, tuple, set i dict w rzeczywistym kodzie?

Lista (list) to uporządkowana i mutowalna sekwencja, co sprawia, że nadaje się dobrze do kolejek zadań, zestawień wyników i tych uporządkowanych danych, które będą ulegały zmianie. Krotka (tuple) jest uporządkowana, lecz niemutowalna, i to czyni z niej właściwe narzędzie do stałych rekordów, do odczytu danych zwracanych z funkcji i przy takich wartościach, do których nigdy nie należy niczego nowego dopisywać (reassign). Zbiór (set) przechowuje wyłącznie unikalne obiekty haszowalne i doskonale nadaje się do sprawdzania przynależności (membership) czy też eliminacji duplikatów ze zbioru informacji (deduplication). Z kolei słownik (dict) przypisuje odpowiednie wartości pod wskazane haszowalne klucze - jest on standardowym fundamentem struktur indeksów, wszelkiego wyszukiwania w pamięci i obsługi formatów danych zbliżonych np. do JSON (JavaScript Object Notation). W nowym Pythonie słownik bezbłędnie zachowuje kolejność wstawiania, choć jego cel nieustannie opiera się na dostępie przez klucze.

items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
Odpowiedz na to pytanie z trenerem AI

3Jaka jest różnica między operatorem `is` a `==` w Pythonie?

`==` sprawdza, czy dwa obiekty są równe co do wartości, zazwyczaj z użyciem metody `__eq__`. `is` sprawdza, czy dwie referencje wskazują na dokładnie ten sam obiekt w pamięci. W kodzie produkcyjnym operator `is` powinien być zwykle zarezerwowany dla sprawdzeń singletonów, takich jak `value is None`, `value is True` (tylko wtedy, gdy tożsamość ma rzeczywiste znaczenie), lub dla obiektów strażniczych (sentinel objects). Nie należy polegać na detalach implementacyjnych, takich jak buforowanie małych liczb całkowitych (small integer caching) czy internizowanie ciągów znaków (string interning); mogą one sprawiać wrażenie, że operator `is` działa w pewnych przykładach, będąc jednocześnie złym wyborem semantycznym.

value = None

if value is None:
    print("missing")
Odpowiedz na to pytanie z trenerem AI

4Czym jest obiekt haszowalny (hashable) i dlaczego jest to ważne?

Obiekt haszowalny posiada wartość skrótu (hash), która pozostaje stabilna przez cały czas jego życia, oraz zachowanie podczas porównywania (equality) zgodne z tym skrótem. Haszowalność jest ważna, ponieważ zależą od niej klucze słowników (`dict`) i elementy zbiorów (`set`). Większość niemutowalnych typów wbudowanych jest haszowalna, ale nie wszystkie obiekty wewnątrz niemutowalnego kontenera automatycznie stają się haszowalne: krotka (`tuple`) zawierająca listę nie jest haszowalna. W trakcie rozmów kwalifikacyjnych powiąż to z projektowaniem: jeśli obiekt jest używany jako klucz, pola biorące udział w metodach `__eq__` lub `__hash__` nie mogą się zmieniać po wstawieniu, w przeciwnym razie obiekt może stać się nieosiągalny w słowniku lub zbiorze.

Odpowiedz na to pytanie z trenerem AI

5Jak działa wycinanie (slicing), z uwzględnieniem ujemnych indeksów i kroku (step)?

Wycinek w zapisie `sequence[start:stop:step]` dołącza element początkowy `start`, wyklucza końcowy `stop` i przesuwa się w iteracji o zadany krok `step`. Brakujące granice są wnioskowane na podstawie kierunku wyznaczanego przez krok. Ujemne indeksy oznaczają odliczanie od końca, zatem `s[-1]` wskazuje na ostatni element. Z kolei ujemny krok powoduje iterowanie od prawej do lewej; wyrażenie `s[::-1]` pozwala na utworzenie odwróconej sekwencji dla wielu typów sekwencyjnych. Ograniczenie tego mechanizmu polega na tym, że wycinanie tworzy często nowy obiekt, co jest wygodne i wyraziste w kodzie, ale niesie ze sobą koszt w przypadku operowania na dużych wolumenach danych.

text = "interview"

print(text[1:5])
print(text[::-1])
Odpowiedz na to pytanie z trenerem AI

Funkcje i zasięg zmiennych

6W jaki sposób Python przekazuje argumenty do funkcji?

Python przekazuje referencje do obiektów przez przypisanie, co często bywa nazywane przekazywaniem przez współdzielenie (call by sharing). Funkcja otrzymuje lokalne nazwy parametrów powiązane z tymi samymi obiektami, które zostały dostarczone przez wywołującego. Jeśli obiekt jest mutowalny (mutable), funkcja może go zmodyfikować, a wywołujący zauważy tę zmianę. Jeśli jednak funkcja przypisze do nazwy parametru nową wartość, zmieni się jedynie lokalne powiązanie (local rebinding). Dobra odpowiedź podczas rozmowy rekrutacyjnej odróżnia mutację w miejscu (in-place mutation) od lokalnego przypisania: operacje takie jak items.append(...) czy items.clear() modyfikują listę u wywołującego, podczas gdy instrukcja items = [] wewnątrz funkcji sprawia jedynie, że lokalna nazwa zaczyna wskazywać na nową listę.

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

def replace_items(items):
    items = []

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

print(values)  # ["new"]
Odpowiedz na to pytanie z trenerem AI

7Dlaczego mutowalne domyślne argumenty są niebezpieczne?

Wartości domyślnych argumentów są obliczane tylko raz, podczas definiowania funkcji, a nie za każdym razem, gdy funkcja jest wywoływana. Jeśli domyślnym argumentem jest obiekt `list` lub `dict`, wszystkie wywołania mogą współdzielić ten sam obiekt i przypadkowo doprowadzić do wycieku stanu. Bezpiecznym wzorcem jest `def f(items=None):`, a następnie utworzenie `items = []` wewnątrz, gdy `items` to `None`. Istnieją rzadkie przypadki, w których współdzielony stan jest celowy, na przykład w przypadku niewielkiej pamięci podręcznej (cache), ale wtedy powinno to być jawne i udokumentowane, ponieważ często stanowi zaskoczenie dla osób czytających kod.

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
Odpowiedz na to pytanie z trenerem AI

8Do czego służą *args i **kwargs?

*args zbiera dodatkowe argumenty pozycyjne do krotki (tuple), podczas gdy **kwargs zbiera dodatkowe argumenty nazwane do słownika (dict). Są one przydatne do tworzenia funkcji opakowujących (wrappers), dekoratorów, elastycznych API (Application Programming Interface) oraz do przekazywania wywołań. W miejscu wywołania, *iterable rozwija wartości pozycyjne, a **mapping rozwija wartości nazwane. Związany jest z tym kompromis dotyczący czytelności: jawne parametry są lepsze, gdy funkcja ma stabilny kontrakt publiczny, natomiast args i kwargs sprawdzają się lepiej na granicach modułów, gdzie znaczenie ma przekazywanie wywołań lub kompatybilność.

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)
Odpowiedz na to pytanie z trenerem AI

9W jaki sposób Python rozwiązuje nazwy zmiennych zgodnie z regułą LEGB?

Python szuka nazw w zakresach: lokalnym (Local), okalającym (Enclosing), globalnym (Global) i wbudowanym (Builtins). Odczyt nazwy następuje w tej właśnie kolejności. Przypisanie wartości do nazwy wewnątrz funkcji standardowo tworzy powiązanie lokalne, dlatego kod może zgłosić wyjątek `UnboundLocalError`, jeśli odczyta nazwę przed lokalnym przypisaniem. Słowo kluczowe `global` przekierowuje przypisanie do zakresu modułu, a `nonlocal` do zakresu okalającej funkcji. W dobrym kodzie produkcyjnym należy używać obu z ostrożnością, ponieważ sprawiają one, że przepływ stanu staje się mniej oczywisty.

total = 0

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

def counter():
    count = 0

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

    return increment
Odpowiedz na to pytanie z trenerem AI

10Czym jest domknięcie (closure) i czym jest późne wiązanie (late binding)?

Domknięcie to funkcja, która zachowuje dostęp do zmiennych z otaczającego ją zakresu nawet po tym, jak zewnętrzna funkcja zakończy działanie. Jest to przydatne w przypadku dekoratorów, fabryk wywołań zwrotnych (callbacks) oraz wstępnie skonfigurowanych funkcji. Późne wiązanie oznacza, że wolna zmienna jest wyszukiwana w momencie wywołania funkcji wewnętrznej, a nie w momencie jej tworzenia. Funkcje anonimowe (lambdas) tworzone w pętli mogą widzieć końcową wartość zmiennej z pętli. Aby przechwycić bieżącą wartość, można użyć argumentu domyślnego, na przykład `lambda i=i: i`, lub wykorzystać małą funkcję fabrykującą.

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]
Odpowiedz na to pytanie z trenerem AI

Programowanie obiektowe (OOP), iteracja i zarządzanie zasobami

11Czym różnią się zmienne klasowe od zmiennych instancyjnych?

Zmienna klasowa jest przechowywana w obiekcie klasy i współdzielona przez jej instancje, chyba że instancja przysłoni ją własnym atrybutem. Zmienna instancyjna jest przechowywana w konkretnym obiekcie, zazwyczaj przypisywana jako `self.name` w metodzie `__init__`. Niemutowalne stałe klasowe są zazwyczaj bezpieczne, jednak mutowalne atrybuty klasowe często prowadzą do błędów, gdy są przypadkowo używane jako stan pojedynczej instancji. Podczas rozmowy kwalifikacyjnej warto wspomnieć o kolejności wyszukiwania (lookup order): Python sprawdza najpierw instancję, a następnie klasę i jej przodków.

class Cart:
    items = []

first = Cart()
second = Cart()

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

class SafeCart:
    def __init__(self):
        self.items = []
Odpowiedz na to pytanie z trenerem AI

12Jak działają dziedziczenie, MRO (Method Resolution Order) i super()?

Python rozwiązuje atrybuty poprzez kolejność rozwiązywania metod (MRO), która jest widoczna przy użyciu ClassName.__mro__. W przypadku pojedynczego dziedziczenia jest to proste, ale przy wielokrotnym dziedziczeniu Python używa linearyzacji C3 (C3 linearization), aby stworzyć spójną kolejność. Funkcja super() nie oznacza dokładnie 'wywołaj mojego rodzica'; oznacza 'wywołaj kolejną implementację w MRO'. Umożliwia to kooperatywne wielokrotne dziedziczenie (cooperative multiple inheritance), pod warunkiem, że metody przyjmują kompatybilne argumenty, a każda klasa poprawnie wywołuje 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
Odpowiedz na to pytanie z trenerem AI

13Jaka jest różnica między obiektem iterowalnym (iterable), iteratorem a generatorem?

Obiekt iterowalny (iterable) to dowolny obiekt, który może zwrócić iterator z funkcji iter(), np. lista lub obiekt pliku. Iterator to obiekt przechowujący stan, który implementuje metodę __next__ i rzuca wyjątek StopIteration po wyczerpaniu elementów. Generator to wygodny sposób na tworzenie iteratora z użyciem słowa kluczowego yield lub wyrażenia generatora (generator expression). Generatory są wydajne pod względem zużycia pamięci, ponieważ produkują wartości leniwie (lazily), ale zazwyczaj są jednorazowego użytku i nie można ich indeksować tak jak list.

def read_lines(path):
    with open(path, encoding="utf-8") as file:
        for line in file:
            yield line.strip()
Odpowiedz na to pytanie z trenerem AI

14Czym jest menedżer kontekstu (context manager) i dlaczego instrukcja `with` jest przydatna?

Menedżer kontekstu definiuje zachowanie przy wejściu i wyjściu za pomocą metod `__enter__` i `__exit__`, lub może zostać utworzony przy użyciu `contextlib.contextmanager`. Instrukcja `with` sprawia, że zarządzanie zasobami staje się wyraźne i niezawodne: pliki są zamykane, blokady zwalniane, a transakcje mogą zostać zatwierdzone (commit) lub wycofane (roll back) nawet wtedy, gdy wystąpią wyjątki. Metoda `__exit__` otrzymuje informacje o wyjątku i może go stłumić poprzez zwrócenie wartości true, chociaż tłumienie błędów powinno być stosowane ostrożnie.

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")
Odpowiedz na to pytanie z trenerem AI

15Czym różni się płytka kopia (shallow copy) od głębokiej kopii (deep copy)?

Płytka kopia (shallow copy) tworzy nowy, zewnętrzny kontener, ale zachowuje referencje do tych samych, zagnieżdżonych obiektów. Głęboka kopia (deep copy) rekursywnie kopiuje obiekty zagnieżdżone, o ile jest to możliwe. Dla prostych, płaskich list często wystarcza płytka kopia. Jednak w przypadku mutowalnych, zagnieżdżonych struktur, płytka kopia powoduje uwspólnienie stanu, co może rodzić nieoczekiwane zachowania i usterki. Głęboka kopia jest z reguły bardziej kosztowna i potrafi skończyć się błędem `TypeError` lub `PicklingError` dla takich zasobów jak otwarte pliki, gniazda (sockets), sesje bazy danych, czy obiekty zachowujące się różnie w zależności od tożsamości w pamięci.

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]]
Odpowiedz na to pytanie z trenerem AI

Środowisko uruchomieniowe (runtime), asynchroniczność i kod produkcyjny

16Czym jest GIL (Global Interpreter Lock) i jak wpływa na kod backendowy?

W standardowej implementacji CPython, GIL pozwala tylko jednemu wątkowi na wykonywanie kodu bajtowego (bytecode) języka Python w danym momencie. Oznacza to, że wątki zazwyczaj nie przyspieszają kodu w języku Python, który silnie obciąża CPU (Central Processing Unit). Wątki są jednak nadal przydatne do zadań intensywnie korzystających z operacji I/O, ponieważ GIL jest zwalniany podczas oczekiwania na wiele operacji blokujących. Do zadań silnie obciążających procesor należy używać modułu multiprocessing, natywnych rozszerzeń, wektoryzowanych bibliotek natywnych lub osobnych usług. Podczas rozmów rekrutacyjnych prowadzonych w 2026 roku warto również wspomnieć o wersjach CPython bez blokady (free-threaded builds) wprowadzonych w Python 3.13+: sprawiają one, że GIL staje się opcjonalny dla kompatybilnego kodu, ale wciąż wymagają sprawdzania zależności i testów wydajnościowych.

Odpowiedz na to pytanie z trenerem AI

17Kiedy należy wybrać wątki, procesy, a kiedy asyncio?

Używaj wątków, gdy masz do czynienia z blokującymi operacjami I/O lub korzystasz z bibliotek udostępniających wyłącznie synchroniczne interfejsy API. Wybieraj procesy do zadań silnie obciążających CPU, dla izolacji lub powstrzymywania awarii, akceptując przy tym narzut związany z serializacją i zużyciem pamięci. Stosuj moduł asyncio, gdy występuje wiele współbieżnych operacji I/O i dysponujesz bibliotekami kompatybilnymi z podejściem asynchronicznym, takimi jak asynchroniczne sterowniki bazy danych lub klienci HTTP (Hypertext Transfer Protocol). Praktyczny wybór zależy również od używanego frameworka i zależności: mieszanie kodu asynchronicznego z wywołaniami blokującymi może znacznie pogorszyć wydajność.

Odpowiedz na to pytanie z trenerem AI

18Do czego przydaje się `async/await` w serwisach webowych w Pythonie?

Słowa kluczowe `async` i `await` umożliwiają współbieżność kooperatywną. Korutyna oddaje kontrolę z powrotem do pętli zdarzeń, gdy oczekuje na operacje wejścia/wyjścia, co pozwala na uruchomienie innych zadań. Jest to cenne dla API, które oczekują na bazy danych, żądania HTTP, kolejki lub gniazda. Nie sprawia to w magiczny sposób, że kod obciążający procesor działa szybciej; zadania mocno obciążające procesor blokują pętlę zdarzeń, chyba że zostaną przeniesione do procesu roboczego, oddzielnego procesu lub kodu natywnego. W FastAPI normalny punkt końcowy zdefiniowany przez `def` jest uruchamiany w puli wątków, podczas gdy blokujące wywołania wewnątrz `async def` mogą zablokować pętlę zdarzeń; `async` pomaga tylko wtedy, gdy ścieżka wywołania wykorzystuje nieblokujące, asynchroniczne operacje wejścia/wyjścia.

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
Odpowiedz na to pytanie z trenerem AI

19Jak powinno się obsługiwać wyjątki w produkcyjnym kodzie w języku Python?

Przechwytuj najbardziej specyficzny wyjątek, z którym potrafisz sobie poradzić, na poziomie, który pozwala na odzyskanie stanu, dodanie kontekstu lub przetłumaczenie go na błąd domenowy. Unikaj pustego `except` i cichego połykania błędów, ponieważ ukrywa to utratę danych oraz problemy operacyjne. Używaj `raise`, aby ponownie rzucić ten sam wyjątek, oraz `raise NewError(...) from exc`, aby zachować kontekst przyczynowy. Dobry kod produkcyjny dodatkowo loguje zdarzenia na granicach systemów, zwraca odpowiednie błędy HTTP i dba o to, by komunikaty wyjątków były przydatne, nie ujawniając przy tym poufnych danych.

try:
    result = parse_payload(payload)
except ValueError as exc:
    raise InvalidRequest("Invalid payload") from exc
Odpowiedz na to pytanie z trenerem AI

20Jak ustrukturyzowałbyś i przetestował produkcyjną usługę w języku Python?

Należy oddzielić logikę domenową od kodu frameworka i infrastruktury, aby kluczowe zachowania można było testować bez udziału serwera czy bazy danych. Zastosuj konfigurację ze zmiennych środowiskowych lub typowanych ustawień, korzystaj z mechanizmu logowania zdarzeń zamiast funkcji `print`, wstrzykiwania zależności (dependency injection) na granicach architektonicznych oraz jawnych limitów czasu (timeouts) dla wywołań sieciowych. W testach należy łączyć testy jednostkowe dla czystej logiki, testy integracyjne dla bazy danych lub zewnętrznych interfejsów, a także kilka testów całościowych (end-to-end) dla krytycznych przepływów. Zewnętrzne systemy powinno się zastępować atrapami (mock) na wyraźnych granicach, jednak w głównym nurcie należy testować obserwowalne zachowania, a nie prywatne szczegóły implementacyjne.

Odpowiedz na to pytanie z trenerem AI