Lezione passo-passo: Python dalle basi a un piccolo servizio
Cosa saprai fare alla fine
Saprai:
- usare variabili, stringhe, numeri, liste e dizionari;
- prendere decisioni con
ife ripetere operazioni confor; - scrivere funzioni;
- segnalare e gestire errori;
- rappresentare dati con classi e Pydantic;
- usare annotazioni di tipo;
- capire la differenza fra codice sincrono e asincrono;
- costruire e testare un piccolo servizio.
Cosa devi sapere prima
Completa la lezione di setup. Devi saper attivare .venv,
eseguire python -m ... e lanciare pytest.
1. Il problema che risolveremo
Costruiremo un piccolo servizio che riceve una domanda HR e restituisce una risposta. All'inizio userà regole semplici, non un modello AI:
Separare le parti ci permetterà più avanti di sostituire il generatore con un LLM senza riscrivere tutto.
2. Valori e variabili
Un valore è un dato. Una variabile è un nome associato a un valore.
"Anna"è una stringa, cioè testo;12è un intero;Falseè un booleano, cioè vero/falso.
Il simbolo = assegna il valore a destra al nome a sinistra. Non significa
"matematicamente uguale".
Visualizza i valori:
Le variabili distinguono maiuscole e minuscole: name e Name sono nomi diversi.
3. Liste e dizionari
Lista
Una lista conserva valori in ordine:
Output:
L'indice parte da zero. labels[0] è il primo elemento.
Dizionario
Un dizionario associa chiavi e valori:
ticket = {
"id": "t-001",
"text": "Come richiedo le ferie?",
"label": "leave",
}
print(ticket["text"])
Un dizionario assomiglia a un oggetto JSON, ma è un valore Python in memoria.
4. Condizioni
if esegue codice soltanto quando una condizione è vera:
days_requested = 3
days_available = 10
if days_requested <= days_available:
print("Richiesta possibile")
else:
print("Giorni insufficienti")
I quattro spazi iniziali sono indentazione. In Python definiscono quali righe appartengono al blocco. Non sono decorazione.
Operatori utili:
| Operatore | Significato |
|---|---|
== |
uguale |
!= |
diverso |
<, > |
minore, maggiore |
<=, >= |
minore/uguale, maggiore/uguale |
and |
entrambe le condizioni |
or |
almeno una condizione |
not |
nega la condizione |
5. Cicli
Un ciclo ripete un blocco:
questions = [
"Come richiedo le ferie?",
"Quando arriva il cedolino?",
]
for question in questions:
print(question)
Ad ogni giro, question contiene un elemento diverso.
Usa for quando attraversi una collezione. Evita di modificare la stessa lista mentre
la stai attraversando: spesso crea comportamenti difficili da capire.
6. Funzioni
Una funzione è codice con un nome che può ricevere input e restituire output:
Spiegazione:
definizia la definizione;questionè un parametro;: strindica che ci aspettiamo testo;-> strindica che restituiamo testo;strip()rimuove spazi iniziali/finali;lower()converte in minuscolo;returnrestituisce il risultato.
Uso:
Output:
Una funzione dovrebbe avere una responsabilità chiara. "Normalizzare una domanda" è più facile da testare di "fare tutto".
7. Errori ed eccezioni
Un'eccezione segnala che l'operazione non può continuare normalmente.
def validate_question(question: str) -> str:
cleaned = question.strip()
if len(cleaned) < 3:
raise ValueError("La domanda deve contenere almeno 3 caratteri")
return cleaned
raise interrompe la funzione. ValueError dice che il valore ricevuto non è valido.
Il chiamante può gestire un errore previsto:
Non scrivere:
Questo nasconde qualunque errore e fa sembrare riuscita un'operazione fallita.
8. Classi e oggetti
Una classe definisce dati e comportamento di un tipo di oggetto.
Uso:
@dataclass genera automaticamente codice ripetitivo come il costruttore.
frozen=True impedisce modifiche dopo la creazione: utile per valori che devono
rimanere coerenti.
9. Validare input con Pydantic
Pydantic è una libreria che controlla dati ricevuti dall'esterno:
from pydantic import BaseModel, Field
class Question(BaseModel):
text: str = Field(min_length=3, max_length=2_000)
Se il testo è troppo corto o lungo, Pydantic produce un errore dettagliato.
Perché non basta text: str? Le annotazioni di tipo aiutano il type checker, ma Python
non valida automaticamente un JSON ricevuto via rete. Pydantic esegue il controllo a
runtime, cioè mentre il programma gira.
10. Annotazioni di tipo
Le annotazioni descrivono valori attesi:
mypy può segnalare:
perché 42 è un intero e la funzione richiede una stringa.
Le annotazioni:
- documentano il codice;
- aiutano l'editor;
- permettono controlli prima dell'esecuzione;
- non sostituiscono la validazione di input esterni.
11. Dipendere da un comportamento, non da un provider
Un Protocol descrive quali metodi deve offrire un oggetto:
from typing import Protocol
class Generator(Protocol):
async def generate(self, question: str) -> str:
...
Il servizio non sa se la risposta arriva da una regola, da OpenAI o da un altro
provider. Richiede solo un oggetto con generate.
Questa tecnica è dependency injection: la dipendenza viene passata dall'esterno. Non è magia; significa semplicemente:
Nei test potremo passare un fake prevedibile.
12. Sincrono e asincrono
Sincrono
Nel codice sincrono, ogni operazione termina prima della successiva:
Asincrono
Nel codice asincrono, mentre un'operazione aspetta rete o disco, Python può proseguire con altro lavoro:
async def definisce una coroutine:
import asyncio
async def wait_and_answer() -> str:
await asyncio.sleep(0.1)
return "risposta"
print(asyncio.run(wait_and_answer()))
await sospende la coroutine fino al risultato senza bloccare l'intero event loop.
L'event loop è il coordinatore che riprende le coroutine pronte.
Async aiuta operazioni I/O, cioè attese verso rete, file o database. Non rende più veloce un calcolo pesante sulla CPU.
13. Esempio completo
Il file src\ai_mastery\python_service.py contiene:
from typing import Protocol
from pydantic import BaseModel, Field
class Question(BaseModel):
text: str = Field(min_length=3, max_length=2_000)
class Generator(Protocol):
async def generate(self, question: str) -> str:
...
class RuleBasedGenerator:
async def generate(self, question: str) -> str:
normalized = question.lower()
if "ferie" in normalized:
return "Consulta la procedura ferie."
if "cedolino" in normalized:
return "Consulta la procedura payroll."
return "Non conosco ancora la risposta."
class AnswerService:
def __init__(self, generator: Generator) -> None:
self._generator = generator
async def answer(self, question: Question) -> str:
return await self._generator.generate(question.text)
Esegui:
Output:
Flusso:
- il terminale passa il testo al programma;
- Pydantic crea e valida
Question; AnswerServicechiama ilGenerator;RuleBasedGeneratorapplica la regola;- il risultato viene stampato.
14. Testare con un fake
Un fake è un'implementazione semplice usata nei test:
Test:
import pytest
@pytest.mark.asyncio
async def test_service_uses_generator() -> None:
service = AnswerService(FakeGenerator())
result = await service.answer(Question(text="Domanda valida"))
assert result == "fake:Domanda valida"
Il test non usa rete, non costa denaro e restituisce sempre lo stesso risultato.
15. Laboratorio guidato
Passo 1
Esegui i tre input:
python -m ai_mastery.python_service "Come richiedo le ferie?"
python -m ai_mastery.python_service "Dove trovo il cedolino?"
python -m ai_mastery.python_service "Ciao, chi sei?"
Controlla i tre rami della funzione.
Passo 2
Aggiungi una regola per la parola contratto. Prima scrivi un test che fallisce:
@pytest.mark.asyncio
async def test_contract_question() -> None:
generator = RuleBasedGenerator()
assert await generator.generate("Quando scade il contratto?") == (
"Consulta la procedura contratti."
)
Poi implementa la regola e verifica che il test passi.
Passo 3
Prova:
Pydantic deve rifiutare la domanda troppo corta. Leggi l'errore: campo, regola violata e valore ricevuto.
16. Esercizi
Base
Scrivi contains_sensitive_word(text: str) -> bool che restituisce True per
password o token.
Intermedio
Per questo esercizio sostituisci la classe Answer(text, source) della sezione 8 con
una nuova versione e modifica il generatore perché restituisca:
invece di una stringa.
Avanzato
Esegui tre domande contemporaneamente con asyncio.gather, mantenendo l'ordine.
17. Soluzioni
Base
def contains_sensitive_word(text: str) -> bool:
normalized = text.lower()
return "password" in normalized or "token" in normalized
Intermedio
Questa definizione sostituisce, solo per l'esercizio, quella con il campo source
della sezione 8:
Poi modifica il tipo di ritorno da str ad Answer nel Protocol e nelle
implementazioni. mypy aiuta a trovare tutti i punti da aggiornare.
Avanzato
import asyncio
async def main() -> None:
service = AnswerService(RuleBasedGenerator())
results = await asyncio.gather(
service.answer(Question(text="Domanda ferie")),
service.answer(Question(text="Domanda cedolino")),
service.answer(Question(text="Domanda contratto")),
)
print(results)
asyncio.run(main())
gather restituisce i risultati nello stesso ordine degli argomenti.
18. Errori comuni
- Scrivere tutto in una funzione: rende difficile testare e sostituire parti.
- Catturare ogni eccezione: nasconde errori inattesi.
- Usare async per calcolo CPU: non porta parallelismo automatico.
- Chiamare provider nei test unitari: rende test lenti, costosi e instabili.
- Confondere type hint e validazione: servono a momenti diversi.
- Creare il provider dentro il servizio: rende la dipendenza difficile da cambiare.
19. Domande di autoverifica
Che cosa fa una funzione?
Raggruppa un comportamento, riceve parametri e può restituire un risultato.
Che differenza c'è fra lista e dizionario?
La lista usa posizioni ordinate; il dizionario usa chiavi.
Quando useresti Pydantic?
Quando dati esterni devono rispettare campi, tipi e vincoli.
Perché il servizio dipende da Generator?
Per separare la logica dal provider concreto e poter usare fake nei test.
Che cosa significa await?
Sospende la coroutine finché l'operazione asincrona termina, lasciando lavorare
l'event loop.
20. Prossimo passo
Quando esempi e test sono chiari, leggi la guida avanzata Python production-grade. Lì troverai deadline, retry, idempotenza, concorrenza e osservabilità.