Una serie mirata di domande da colloquio su Python per sviluppatori backend. Usa ogni risposta come struttura per l'esposizione orale: inizia col concetto, aggiungi un esempio concreto e poi menziona eventuali compromessi.
1Quali tipi di dati integrati possiede Python e quali sono mutabili?
Python ha tipi scalari come `NoneType`, `bool`, `int`, `float` e `complex`; tipi testuali e binari come `str`, `bytes` e `bytearray`; tipi sequenza come `list`, `tuple` e `range`; e tipi collezione come `dict`, `set` e `frozenset`. La distinzione importante in sede di colloquio è la mutabilità: `list`, `dict`, `set` e `bytearray` possono essere modificati sul posto, mentre i numeri, `bool`, `str`, `bytes`, `tuple`, `range`, `frozenset` e `None` sono immutabili. Una sfumatura utile è che i riferimenti agli elementi all'interno di un `tuple` sono fissi, ma lo stato interno di un oggetto mutabile a cui fanno riferimento può comunque cambiare.
2In che modo list, tuple, set e dict differiscono nel codice reale?
Una list è una sequenza mutabile ordinata, quindi è adatta per code di lavoro, raccolta di risultati e dati ordinati che cambiano nel tempo. Una tuple è ordinata ma immutabile, pertanto è utile per record fissi, valori restituiti da funzioni e dati che non dovrebbero essere riassegnati. Un set archivia oggetti univoci e hashable ed è ideale per i controlli di appartenenza o la deduplicazione. Un dict associa chiavi hashable a valori ed è la struttura tipica per indici, ricerche e dati in stile JSON. Nel Python moderno, il dict preserva l'ordine di inserimento, ma il suo scopo principale rimane l'accesso basato su chiave.
items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
`==` verifica se due oggetti sono uguali per valore, solitamente tramite `__eq__`. `is` verifica se due riferimenti puntano esattamente allo stesso oggetto in memoria. Nel codice di produzione, l'uso di `is` dovrebbe solitamente essere riservato ai controlli di singleton, come `value is None`, o `value is True` solo quando l'identità è davvero importante, oppure per gli oggetti sentinella. Non fare affidamento sui dettagli di implementazione come il caching dei piccoli interi o l'interning delle stringhe; questi meccanismi possono far sembrare che `is` funzioni negli esempi, pur essendo la scelta semantica sbagliata.
Un oggetto hashable ha un valore di hash che rimane stabile durante il suo ciclo di vita e un comportamento di uguaglianza coerente con tale hash. Essere hashable è importante perché le chiavi dei `dict` e gli elementi dei `set` dipendono da questo. La maggior parte dei tipi integrati immutabili è hashable, ma non tutti gli oggetti all'interno di un contenitore immutabile diventano automaticamente hashable: una tupla che contiene una lista non è hashable. Nei colloqui, collega questo aspetto alla progettazione: se un oggetto viene usato come chiave, i campi che partecipano in `__eq__` o `__hash__` non devono cambiare dopo l'inserimento, altrimenti l'oggetto può diventare irraggiungibile in un `dict` o in un `set`.
5Come funziona lo slicing, includendo gli indici negativi e i passi (step)?
Uno slice `sequence[start:stop:step]` include `start`, esclude `stop` e si sposta secondo lo `step`. I limiti mancanti vengono dedotti dalla direzione dello `step`. Gli indici negativi contano dalla fine, quindi `s[-1]` è l'ultimo elemento. Uno `step` negativo percorre la sequenza da destra a sinistra; `s[::-1]` crea una sequenza invertita per molti tipi di sequenza. Il compromesso è che lo slicing crea spesso un nuovo oggetto, quindi è molto espressivo ma non sempre privo di costi computazionali per grandi moli di dati.
text = "interview"
print(text[1:5])
print(text[::-1])
6In che modo Python passa gli argomenti alle funzioni?
Python passa i riferimenti agli oggetti per assegnazione, un meccanismo spesso chiamato chiamata per condivisione (call by sharing). La funzione riceve i nomi dei parametri locali associati agli stessi oggetti forniti dal chiamante. Se l'oggetto è mutabile, la funzione può modificarlo e il chiamante ne osserverà il cambiamento. Se la funzione riassegna il nome del parametro, cambia solo l'associazione locale. Una risposta eccellente in un colloquio distingue la mutazione sul posto dalla riassegnazione locale: `items.append(...)` o `items.clear()` modifica la lista del chiamante, mentre scrivere `items = []` all'interno della funzione reindirizza semplicemente il nome locale verso una nuova lista.
7Perché gli argomenti predefiniti mutabili sono pericolosi?
I valori predefiniti degli argomenti vengono valutati una volta sola quando la funzione viene definita, non ogni volta che la funzione viene chiamata. Se un valore predefinito è una lista o un dizionario, tutte le chiamate possono condividere lo stesso oggetto e far trapelare lo stato accidentalmente. Il pattern sicuro è `def f(items=None):`, per poi creare `items = []` all'interno della funzione quando `items` è `None`. Ci sono casi rari in cui lo stato condiviso è intenzionale, come per una piccola cache, ma in tal caso dovrebbe essere esplicito e documentato perché risulta inaspettato per chi legge il codice.
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
*args raccoglie argomenti posizionali aggiuntivi in una tupla, mentre **kwargs raccoglie argomenti nominati aggiuntivi in un dict. Sono utili per wrapper, decoratori, API flessibili e per l'inoltro di chiamate. Nel punto di chiamata, *iterable espande i valori posizionali e **mapping espande i valori nominati. Il compromesso è la chiarezza: i parametri espliciti sono migliori quando la funzione ha un contratto pubblico stabile, mentre args e kwargs sono migliori ai confini in cui contano l'inoltro o la compatibilità.
9In che modo Python risolve i nomi delle variabili tramite la regola LEGB?
Python cerca i nomi negli ambiti Local (Locale), Enclosing (Racchiuso), Global (Globale) e Builtins (Integrato). La lettura di un nome segue esattamente questo ordine. L'assegnazione di un nome all'interno di una funzione crea normalmente un'associazione locale, motivo per cui il codice può sollevare un'eccezione UnboundLocalError se legge un nome prima dell'assegnazione locale. La keyword global reindirizza l'assegnazione all'ambito del modulo, e nonlocal reindirizza l'assegnazione all'ambito di una funzione racchiusa. In codice di produzione di alta qualità, è consigliabile utilizzare entrambi con parsimonia, poiché rendono meno evidente il flusso dello stato.
total = 0
def add(value):
global total
total += value
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
Una closure è una funzione che mantiene l'accesso alle variabili di un ambito circostante (enclosing scope) anche dopo che la funzione esterna ha terminato l'esecuzione. Questo è utile per decoratori, factory di funzioni di callback e funzioni configurate. Late binding significa che la variabile libera viene valutata nel momento in cui la funzione interna viene chiamata, non quando viene creata. Le lambda create all'interno di un ciclo possono così vedere tutte il valore finale del ciclo. È possibile catturare il valore corrente utilizzando un argomento predefinito, ad esempio `lambda i=i: i`, o utilizzando una piccola funzione factory.
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]
11In cosa differiscono le variabili di classe dalle variabili di istanza?
Una variabile di classe è memorizzata sull'oggetto classe ed è condivisa dalle istanze, a meno che un'istanza non la nasconda (shadowing) con il proprio attributo. Una variabile di istanza è memorizzata su un oggetto specifico, solitamente assegnata come `self.name` in `__init__`. Le costanti di classe immutabili in genere non creano problemi, ma gli attributi di classe mutabili sono causa di bug frequenti quando vengono usati accidentalmente come stato della singola istanza. In un colloquio, è bene menzionare l'ordine di risoluzione: Python controlla prima l'istanza, poi la classe e infine i suoi antenati.
class Cart:
items = []
first = Cart()
second = Cart()
first.items.append("book")
print(second.items) # ["book"]
class SafeCart:
def __init__(self):
self.items = []
12Come funzionano l'ereditarietà, la MRO (Method Resolution Order) e super()?
Python risolve gli attributi attraverso la MRO (Method Resolution Order), visibile tramite ClassName.__mro__. Nell'ereditarietà singola è semplice, ma nell'ereditarietà multipla Python utilizza la linearizzazione C3 per creare un ordine coerente. super() non significa esattamente 'chiama la classe genitore'; significa 'chiama la successiva implementazione nella MRO'. Questo rende possibile l'ereditarietà multipla cooperativa quando i metodi accettano argomenti compatibili e ogni classe chiama super() correttamente.
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
13Qual è la differenza tra iterabile, iteratore e generatore?
Un iterabile è un qualsiasi oggetto in grado di restituire un iteratore tramite `iter()`, come una lista o un oggetto file. Un iteratore è un oggetto con stato che implementa `__next__` e solleva `StopIteration` quando viene esaurito. Un generatore è un modo pratico per creare un iteratore tramite `yield` o un'espressione generatore. I generatori sono efficienti dal punto di vista della memoria perché producono valori in modo pigro, ma di solito sono monouso e non possono essere indicizzati come una lista.
def read_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.strip()
14Cos'è un context manager e perché `with` è utile?
Un context manager definisce il comportamento di ingresso e uscita con `__enter__` e `__exit__`, oppure può essere creato con `contextlib.contextmanager`. L'istruzione `with` rende la gestione delle risorse esplicita e affidabile: i file vengono chiusi, i lock vengono rilasciati e le transazioni possono eseguire il commit o il rollback anche quando si verificano eccezioni. `__exit__` riceve le informazioni sull'eccezione e può sopprimerla restituendo `True`, sebbene la soppressione degli errori debba essere usata con cautela.
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")
15In cosa differiscono una copia superficiale (shallow copy) e una copia profonda (deep copy)?
Una copia superficiale crea un nuovo contenitore esterno, ma mantiene i riferimenti agli stessi oggetti annidati. Una copia profonda copia ricorsivamente gli oggetti annidati quando possibile. Per semplici liste piatte, una copia superficiale è spesso sufficiente. Per strutture mutabili annidate, la copia superficiale può comunque condividere lo stato e causare comportamenti inattesi. La copia profonda è più costosa e può fallire con `TypeError` o `PicklingError` per risorse come file aperti, socket, sessioni di database o oggetti con un comportamento sensibile all'identità.
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]]
16Cos'è il GIL (Global Interpreter Lock) e come influisce sul codice backend?
Nello standard CPython, il GIL (Global Interpreter Lock) consente a un solo thread alla volta di eseguire il bytecode Python. Ciò significa che solitamente i thread non velocizzano il codice Python CPU-bound. I thread sono comunque utili per le operazioni I/O-bound, poiché il GIL viene rilasciato durante l'attesa di molte operazioni bloccanti. Per i carichi di lavoro ad alta intensità di calcolo (CPU-heavy), è consigliabile utilizzare il multiprocessing, estensioni native, librerie native vettorializzate o servizi separati. Per i colloqui a partire dal 2026, vale la pena menzionare le build CPython "free-threaded" di Python 3.13+: rendono il GIL opzionale per il codice compatibile, ma richiedono comunque test mirati su dipendenze e prestazioni.
17Quando dovresti scegliere thread, processi o `asyncio`?
Usa i thread quando hai I/O bloccante o librerie che espongono solo API sincrone. Usa i processi per carichi di lavoro legati alla CPU (CPU-bound), per l'isolamento o per il contenimento dei crash, accettando l'overhead di serializzazione e di memoria. Usa `asyncio` quando hai molte operazioni di I/O concorrenti e librerie compatibili con la programmazione asincrona, come driver di database asincroni o client HTTP. La scelta pratica dipende anche dal tuo framework e dalle tue dipendenze: mescolare codice asincrono con chiamate bloccanti può peggiorare le prestazioni.
18A cosa servono async/await nei servizi web in Python?
async e await abilitano la concorrenza cooperativa. Una coroutine restituisce il controllo all'event loop quando attende l'I/O, permettendo l'esecuzione di altri task. Questo è utile per le API che attendono database, chiamate HTTP, code o socket. Non rende magicamente più veloce il codice legato alla CPU; il lavoro intensivo per la CPU blocca l'event loop a meno che non venga spostato in un worker, in un processo separato o in codice nativo. In FastAPI, un normale endpoint definito con def viene eseguito in un pool di thread, mentre le chiamate bloccanti all'interno di async def possono bloccare l'event loop; async è utile solo quando il percorso di chiamata utilizza I/O asincrono non bloccante.
19Come dovrebbero essere gestite le eccezioni nel codice Python in produzione?
Cattura l'eccezione più specifica che sei in grado di gestire e fallo a un livello in cui sia possibile riprendersi dall'errore, aggiungere contesto o tradurla in un errore di dominio. Evita i blocchi except generici ed evita di sopprimere silenziosamente gli errori, poiché ciò nasconde la perdita di dati e i problemi operativi. Usa raise per rilanciare la stessa eccezione e raise NewError(...) from exc per preservare il contesto causale. Un buon codice di produzione registra inoltre i log ai confini del sistema, restituisce errori HTTP appropriati e mantiene utili i messaggi di eccezione senza far trapelare segreti.
try:
result = parse_payload(payload)
except ValueError as exc:
raise InvalidRequest("Invalid payload") from exc
20Come struttureresti e testeresti un servizio Python per la produzione?
Separa la logica di dominio dal codice del framework e dell'infrastruttura, in modo che il comportamento principale possa essere testato senza un server o un database. Utilizza la configurazione tramite variabili d'ambiente o impostazioni tipizzate, il logging al posto di `print`, la dependency injection ai confini dell'architettura e timeout espliciti per le chiamate di rete. Per i test, combina test unitari per la logica pura, test di integrazione per il database o i confini esterni e alcuni controlli end-to-end per i flussi critici. Usa i mock per i sistemi esterni definendo confini chiari, ma testa il comportamento osservabile anziché i dettagli di implementazione privata.