Usare il pattern MVC in Python

Il pattern Model-View-Controller (MVC) è un modo di organizzare il codice che separa in modo netto la logica di business, la presentazione e la gestione degli input. Non è legato a un linguaggio specifico, quindi può essere applicato anche in Python, sia per applicazioni a riga di comando, sia per interfacce grafiche o applicazioni web.

1. Cos è il pattern MVC

MVC suddivide l applicazione in tre componenti principali:

  • Model: gestisce i dati e la logica di business. Non conosce nulla di come i dati verranno mostrati all utente.
  • View: si occupa solo di mostrare informazioni all utente (console, HTML, GUI, API). Non contiene logica di business, al massimo un po di logica di presentazione.
  • Controller: riceve gli input dell utente, coordina Model e View, decide cosa fare e quali viste mostrare.

Separando questi ruoli si ottengono codice più pulito, più facile da testare e più semplice da estendere.

2. Strutturare un progetto MVC in Python

Una possibile struttura di cartelle per un piccolo progetto MVC in Python potrebbe essere:

mvc_todo/
  models/
    __init__.py
    todo.py
  views/
    __init__.py
    console_view.py
  controllers/
    __init__.py
    todo_controller.py
  main.py

Non esiste un unica struttura corretta, ma separare fisicamente i file in base al loro ruolo aiuta a mantenere l ordine man mano che l applicazione cresce.

3. Un esempio completo: gestore di TODO a riga di comando

Vediamo un esempio completo di una piccola applicazione a riga di comando che gestisce una lista di attività (TODO). L obiettivo è mostrare concretamente come suddividere Model, View e Controller.

3.1 Il Model: gestione dei dati

Il Model rappresenta le attività e la logica per aggiungerle, rimuoverle, segnarle come completate. Per semplicità useremo una lista in memoria, ma lo stesso concetto si applica se i dati vengono salvati in un file o in un database.

# file: models/todo.py

from dataclasses import dataclass, field
from typing import List


@dataclass
class TodoItem:
    id: int
    descrizione: str
    completato: bool = False


class TodoModel:
    def __init__(self) -> None:
        self._items: List[TodoItem] = []
        self._next_id: int = 1

    def aggiungi(self, descrizione: str) -> TodoItem:
        item = TodoItem(id=self._next_id, descrizione=descrizione)
        self._items.append(item)
        self._next_id += 1
        return item

    def lista(self) -> List[TodoItem]:
        return list(self._items)

    def completa(self, item_id: int) -> bool:
        for item in self._items:
            if item.id == item_id:
                item.completato = True
                return True
        return False

    def rimuovi(self, item_id: int) -> bool:
        for i, item in enumerate(self._items):
            if item.id == item_id:
                del self._items[i]
                return True
        return False

Il Model non stampa nulla e non legge input: espone solo metodi per gestire i dati.

3.2 La View: interfaccia a riga di comando

La View si occupa di mostrare informazioni all utente e di raccogliere input, ma non decide cosa fare con quei dati. In questo esempio realizziamo una semplice view che lavora su console.

# file: views/console_view.py

from typing import Iterable
from models.todo import TodoItem


class ConsoleView:
    def mostra_menu(self) -> None:
        print("\n--- Gestore TODO (MVC) ---")
        print("1) Elenca attività")
        print("2) Aggiungi attività")
        print("3) Completa attività")
        print("4) Rimuovi attività")
        print("0) Esci")

    def chiedi_scelta(self) -> str:
        return input("Scegli un opzione: ").strip()

    def chiedi_descrizione(self) -> str:
        return input("Descrizione attività: ").strip()

    def chiedi_id(self) -> int:
        raw = input("ID attività: ").strip()
        try:
            return int(raw)
        except ValueError:
            print("ID non valido, uso -1.")
            return -1

    def mostra_lista(self, items: Iterable[TodoItem]) -> None:
        print("\n--- Elenco attività ---")
        if not list(items):
            print("Nessuna attività.")
            return
        for item in items:
            stato = "[x]" if item.completato else "[ ]"
            print(f"{item.id:3d} {stato} {item.descrizione}")

    def mostra_messaggio(self, messaggio: str) -> None:
        print(messaggio)

Anche la View non conosce la logica di business: non sa come i dati vengono memorizzati, si limita a stampare e leggere.

3.3 Il Controller: coordina Model e View

Il Controller riceve input (dalla View), chiama il Model per manipolare i dati e chiede alla View di mostrare i risultati.

# file: controllers/todo_controller.py

from models.todo import TodoModel
from views.console_view import ConsoleView


class TodoController:
    def __init__(self, model: TodoModel, view: ConsoleView) -> None:
        self.model = model
        self.view = view
        self._running = True

    def esegui(self) -> None:
        while self._running:
            self.view.mostra_menu()
            scelta = self.view.chiedi_scelta()
            self._gestisci_scelta(scelta)

    def _gestisci_scelta(self, scelta: str) -> None:
        if scelta == "1":
            self._elenca()
        elif scelta == "2":
            self._aggiungi()
        elif scelta == "3":
            self._completa()
        elif scelta == "4":
            self._rimuovi()
        elif scelta == "0":
            self._running = False
            self.view.mostra_messaggio("Arrivederci")
        else:
            self.view.mostra_messaggio("Scelta non valida.")

    def _elenca(self) -> None:
        items = self.model.lista()
        self.view.mostra_lista(items)

    def _aggiungi(self) -> None:
        descrizione = self.view.chiedi_descrizione()
        if not descrizione:
            self.view.mostra_messaggio("Descrizione vuota, attività non creata.")
            return
        item = self.model.aggiungi(descrizione)
        self.view.mostra_messaggio(f"Attività creata con ID {item.id}.")

    def _completa(self) -> None:
        item_id = self.view.chiedi_id()
        if item_id < 0:
            return
        if self.model.completa(item_id):
            self.view.mostra_messaggio("Attività completata.")
        else:
            self.view.mostra_messaggio("Attività non trovata.")

    def _rimuovi(self) -> None:
        item_id = self.view.chiedi_id()
        if item_id < 0:
            return
        if self.model.rimuovi(item_id):
            self.view.mostra_messaggio("Attività rimossa.")
        else:
            self.view.mostra_messaggio("Attività non trovata.")

Notare come il Controller sia l unico punto in cui vengono prese decisioni sull ordine delle operazioni e sul flusso dell applicazione.

3.4 Il punto di ingresso dell applicazione

Infine, il file main.py si limita a creare le istanze e avviare il Controller.

# file: main.py

from models.todo import TodoModel
from views.console_view import ConsoleView
from controllers.todo_controller import TodoController


def main() -> None:
    model = TodoModel()
    view = ConsoleView()
    controller = TodoController(model, view)
    controller.esegui()


if __name__ == "__main__":
    main()

4. Adattare MVC a diversi tipi di applicazione

Lo stesso schema si applica a molti contesti:

  • Applicazioni web: il Model può essere uno strato di accesso al database; le View sono template HTML; il Controller sono le funzioni che gestiscono le richieste HTTP.
  • GUI desktop: il Model gestisce lo stato dell applicazione; le View sono le finestre e i widget; il Controller sono i gestori degli eventi (click, input).
  • Script complessi: anche negli script a riga di comando è utile separare logica di business e presentazione per evitare file unici difficili da mantenere.

Molti framework Python incorporano varianti di MVC (ad esempio nei web framework spesso si parla di Model Template View o Model View Template), ma l idea di base resta la stessa: separare responsabilità per ottenere un codice più chiaro.

5. Buone pratiche nell uso di MVC in Python

  • Mantieni il Model completamente indipendente dal modo in cui presenti i dati. In questo modo potrai riutilizzarlo con viste diverse (console, web, GUI).
  • Mantieni la View il più possibile priva di logica di business: deve solo mostrare e raccogliere dati.
  • Lascia al Controller il compito di orchestrare le operazioni e di validare la maggior parte degli input.
  • Usa test automatici soprattutto per il Model (più facile da testare perché non dipende dall interfaccia).
  • Se il progetto cresce, valuta di introdurre altri pattern (Repository, Service, ecc.) sopra o accanto al Model.

6. Conclusioni

Il pattern MVC in Python non richiede librerie particolari: è una disciplina di progettazione. Applicarlo anche in piccoli progetti aiuta a scrivere codice più leggibile, testabile e pronto a crescere nel tempo. L esempio del gestore TODO a riga di comando è solo un punto di partenza: puoi estenderlo aggiungendo il salvataggio su file, una interfaccia web o una GUI, mantenendo sempre separate le responsabilità tra Model, View e Controller.

Torna su