Con Flask montaste cada pieza de la web de Papyrus a mano: elegiste dónde guardar el catálogo (el JSON de M6), escribiste la validación del POST campo a campo, y si mañana quisieras un panel para que Marta edite libros sin tocar código, tendrías que construirlo. Django es la respuesta de la otra escuela: un framework con baterías incluidas que trae de serie persistencia, panel de administración, formularios validados, gestión de usuarios y más. En esta lección arrancarás un proyecto Django desde cero, entenderás su arquitectura MTV y su estructura de ficheros (que impone, no sugiere), redefinirás el Libro de M5 como un modelo con persistencia regalada y terminarás editando el catálogo desde el admin — sin haber escrito ni una línea de HTML. El mismo Papyrus, el otro camino.
Contenido
- Filosofía: baterías incluidas (cuándo Django, cuándo Flask)
- Instalación y
startproject: nacepapyrus_web - Proyecto vs app:
startapp catalogo - La estructura de ficheros, fichero a fichero
- El patrón MTV (y su primo MVC)
- Primera vista:
HttpResponseyurls.py - El modelo
Libro: de dataclass amodels.Model - Migraciones:
makemigrationsymigrate - El admin de Django: el regalo estrella
runservery el mapa completo
Filosofía: baterías incluidas (cuándo Django, cuándo Flask)
Flask te da un núcleo mínimo y libertad total; Django te da un electrodoméstico completo y sus instrucciones. Ninguno es "mejor": resuelven contextos distintos.
| Criterio | Flask | Django |
|---|---|---|
| Persistencia | La eliges tú (nuestro JSON de M6) | ORM incluido: modelos que se guardan solos |
| Panel de administración | Lo construyes tú | Incluido y automático |
| Formularios validados | A mano (el POST de 10-03) | ModelForm incluido (10-05) |
| Usuarios y sesiones | Extensiones de terceros | Incluidos |
| Estructura de proyecto | La decides tú | La impone Django (y eso es bueno en equipo) |
| Ideal para | APIs, proyectos pequeños, aprender pieza a pieza | Aplicaciones completas con partes estándar |
| Riesgo típico | Reinventar ruedas | Cargar con piezas que no usas |
Regla práctica: si tu proyecto es sobre todo una API o algo pequeño y peculiar, Flask; si es "una aplicación web clásica" — catálogo, altas, usuarios, administración —, Django te ahorra semanas. Papyrus, que es exactamente eso, es un caso de libro (nunca mejor dicho).
Instalación y startproject: nace papyrus_web
En un entorno virtual nuevo (M1: un venv por proyecto):
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install django
django-admin startproject papyrus_web
cd papyrus_webpip install django instaló también el comando django-admin, el generador de proyectos. startproject papyrus_web crea una carpeta con el esqueleto completo — primera diferencia cultural con Flask, donde empezaste con un app.py vacío: Django te da la estructura hecha porque su filosofía es que todos los proyectos Django se parezcan.
Proyecto vs app: startapp catalogo
Django distingue dos conceptos que conviene fijar pronto:
- El proyecto (
papyrus_web) es el conjunto: configuración global, URLs raíz, el sitio entero. - Una app es un componente funcional con sus modelos, vistas y plantillas: el catálogo, algún día las reservas, quizá los socios. Un proyecto contiene varias apps; una app bien hecha podría reutilizarse en otro proyecto.
Creamos la primera:
Y la registramos en el proyecto, en papyrus_web/settings.py, para que Django la tenga en cuenta:
INSTALLED_APPS = [
"django.contrib.admin", # el panel de administración
"django.contrib.auth", # usuarios y permisos
# ... otras baterías incluidas ...
"catalogo", # ← nuestra app
]Ese INSTALLED_APPS es literalmente la lista de baterías conectadas: fíjate en cuántas cosas vienen encendidas de fábrica.
La estructura de ficheros, fichero a fichero
Tras los dos comandos, el árbol es este (los ficheros importantes, comentados):
papyrus_web/ ← carpeta raíz del proyecto
├── manage.py ← tu navaja suiza: runserver, migrate, startapp...
├── papyrus_web/ ← el "corazón" del proyecto (configuración)
│ ├── settings.py ← configuración global
│ ├── urls.py ← tabla de URLs raíz
│ └── wsgi.py / asgi.py ← puntos de entrada para servidores (producción)
└── catalogo/ ← la app
├── models.py ← los datos: aquí renacerá Libro
├── views.py ← las vistas (como las de Flask)
├── admin.py ← qué se administra desde el panel
├── migrations/ ← historial de cambios de los modelos
└── tests.py ← Django también cree en los tests (M9 sonríe)| Fichero | Responsabilidad | Equivalente Flask |
|---|---|---|
manage.py |
Ejecutar comandos del proyecto | el CLI flask |
settings.py |
Configuración: apps, base de datos, idioma, zona horaria | lo poco que configurabas en app.py |
urls.py |
Mapear URLs → vistas | los @app.route (¡aquí sin decorador!) |
views.py |
Recibir petición, devolver respuesta | tus funciones vista |
models.py |
Definir los datos y su persistencia | tu modelos.py + almacen.py + JSON |
admin.py |
Configurar el panel de administración | no existe: lo harías tú |
Dos ajustes de cortesía en settings.py antes de seguir: LANGUAGE_CODE = "es" y TIME_ZONE = "Europe/Madrid" — el admin te hablará en español.
El patrón MTV (y su primo MVC)
Django organiza el código según el patrón MTV: Model–Template–View. Es la separación de responsabilidades que venimos practicando desde M3, con nombres propios:
flowchart LR
P["Petición<br/>GET /libros/"] --> U["urls.py<br/>¿qué vista atiende esto?"]
U --> V["View (views.py)<br/>lógica: qué datos hacen falta"]
V --> M["Model (models.py)<br/>los datos: Libro y su persistencia"]
M --> V
V --> T["Template<br/>HTML con huecos"]
T --> R["Respuesta HTML"]
- Model: los datos y las reglas para acceder a ellos. En Papyrus-Flask eran la dataclass más
almacen.pymás el JSON; en Django será una sola clase. - Template: la presentación — como las plantillas Jinja2 de 10-02, con una sintaxis casi idéntica.
- View: la lógica que conecta ambos — recibe la petición, consulta modelos, elige plantilla.
Si has oído hablar de MVC (Model-View-Controller), el patrón clásico: es lo mismo con las etiquetas movidas. La "View" de Django hace de Controller del MVC, y el Template hace de "View" del MVC. Confuso de nombrar, idéntico de espíritu: datos, lógica y presentación, cada uno en su sitio.
Primera vista: HttpResponse y urls.py
El "hola mundo" Django, para ver el mecanismo desnudo. En catalogo/views.py:
from django.http import HttpResponse
def inicio(request):
return HttpResponse("<h1>Papyrus, librería de barrio</h1>")Dos diferencias con Flask saltan a la vista: la función recibe siempre request como primer parámetro (en Flask era un objeto global que importabas), y no hay decorador de ruta. En Django, las URLs viven separadas de las vistas, en papyrus_web/urls.py:
from django.contrib import admin
from django.urls import path
from catalogo import views
urlpatterns = [
path("admin/", admin.site.urls), # el panel, ya enchufado de serie
path("", views.inicio), # "" = la raíz del sitio
]path(ruta, vista) hace lo que @app.route hacía: apuntar en la tabla. La diferencia es filosófica: Django prefiere todas las URLs juntas en un índice en vez de repartidas por decoradores — en proyectos grandes ese índice único se agradece. Arranca y compruébalo:
http://127.0.0.1:8000 (Django usa el puerto 8000; Flask, el 5000) muestra tu saludo. runserver incluye recarga automática, como el modo debug de Flask — y con la misma advertencia: es un servidor de desarrollo, no de producción.
El modelo Libro: de dataclass a models.Model
Aquí llega el momento estrella de la lección. Recuerda la dataclass de M5:
Su reencarnación Django, en catalogo/models.py:
from django.db import models
class Libro(models.Model):
titulo = models.CharField(max_length=200)
precio = models.DecimalField(max_digits=6, decimal_places=2)
stock = models.IntegerField(default=0)
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def precio_final(self, socio=False):
precio = float(self.precio) * (1 + self.IVA_LIBROS)
if socio:
precio *= 1 - self.DESCUENTO_SOCIO
return round(precio, 2)
def hay_stock(self):
return self.stock > 0
def __str__(self):
return self.tituloCompara miembro a miembro:
| Dataclass (M5) | Modelo Django | Qué añade Django |
|---|---|---|
titulo: str |
CharField(max_length=200) |
Longitud máxima, validación |
precio: float |
DecimalField(max_digits=6, decimal_places=2) |
Decimales exactos para dinero (¡mejora!) |
stock: int = 0 |
IntegerField(default=0) |
Tipo y valor por defecto persistidos |
| — | hereda de models.Model |
Guardarse, cargarse, buscarse: gratis |
Los mismos tres datos de siempre, los mismos métodos precio_final() y hay_stock() (Hamlet seguirá costando 9.83 para socios — los invariantes de M9 viajan con nosotros). La novedad es lo que no ves: por heredar de models.Model (herencia de M5 en acción), cada Libro sabe guardarse con libro.save() y consultarse con Libro.objects... (lo exprimiremos en 10-05). En Flask escribiste cargar_catalogo y guardar_catalogo sobre JSON; esto es la persistencia que Django te regala. Por debajo usa una base de datos SQLite — un fichero db.sqlite3 en tu proyecto, sin instalar ni configurar nada. La teoría de bases de datos SQL queda fuera de este curso: hoy nos basta con que el regalo funciona. Un detalle profesional de propina: DecimalField evita los redondeos raros del float con dinero — Django hasta mejora nuestra dataclass.
Migraciones: makemigrations y migrate
Definir el modelo no crea nada todavía: hay que decirle a la base de datos qué forma tienen los datos. Ese diálogo son las migraciones, en dos pasos:
python manage.py makemigrations catalogo
# Migrations for 'catalogo':
# catalogo/migrations/0001_initial.py
# + Create model Libro
python manage.py migrate
# Applying catalogo.0001_initial... OK (y las de admin, auth...)Conceptualmente:
makemigrationscompara tusmodels.pycon el último estado conocido y escribe la receta del cambio (un fichero Python enmigrations/): "crear la tabla de Libro con estas columnas".migrateaplica las recetas pendientes a la base de datos real.
Es control de versiones para la forma de tus datos: cada cambio futuro del modelo (añadir un campo autor, por ejemplo) generará su migración 0002_..., y cualquier compañero reproduce tu base de datos ejecutando migrate. Verás que la primera vez se aplican también migraciones de admin y auth: son las tablas de las baterías incluidas preparándose.
El admin de Django: el regalo estrella
Y ahora, la demostración de fuerza. Tres líneas en catalogo/admin.py:
Un superusuario para entrar (te pedirá usuario y contraseña por terminal):
Arranca runserver, entra en http://127.0.0.1:8000/admin/, identifícate y contempla: un panel de administración completo — listado de libros, formulario de alta con validación por tipos, edición, borrado con confirmación, buscador — sin haber escrito una sola plantilla ni una sola vista. Da de alta el catálogo canónico: La Odisea (12.50, 4), Hamlet (9.95, 6), El Quijote (15.90, 8) y Fausto (21.00, 10). Marta ya puede gestionar el inventario desde el navegador; en Flask, ese panel te habría costado varias tardes. El __str__ que definimos es lo que el admin muestra en los listados — sin él verías «Libro object (1)».
runserver y el mapa completo
El ciclo de trabajo Django que repetirás mil veces, resumido:
python manage.py runserver # desarrollar y probar
# ... editas models.py ...
python manage.py makemigrations # anotar el cambio de datos
python manage.py migrate # aplicarloErrores Comunes y Consejos
- Olvidar registrar la app en
INSTALLED_APPS: el síntoma clásico esmakemigrationsrespondiendo "No changes detected". Django solo mira las apps registradas. - Editar
models.pyy no migrar: al acceder verásOperationalError: no such table. Modelo cambiado =makemigrations+migrate, siempre, en ese orden. - Confundir
django-adminconmanage.py:django-adminsolo parastartproject; después, todo conpython manage.py ..., que carga la configuración de tu proyecto. - Usar
FloatFieldpara dinero: la tentación viene de la dataclass.DecimalFieldexiste precisamente para importes; acostúmbrate desde el primer modelo. - Borrar
db.sqlite3omigrations/"para arreglar" un error: en aprendizaje puedes permitírtelo, pero es dinamita: pierdes datos e historial. Mejor entender el error — casi siempre es una migración sin aplicar. - Buscar el
if __name__ == "__main__": en Django no arrancas tú la aplicación; la arrancamanage.py. Suelta el control: el framework llama a tu código, no al revés (eso es, literalmente, lo que define a un framework).
Ejercicios
Ejercicio 1: el campo autor
El catálogo canónico siempre tuvo autores (Homero, Shakespeare, Cervantes, Goethe) y el modelo no los guarda. Añade autor = models.CharField(max_length=100, default="") al modelo, genera y aplica la migración, y completa los autores desde el admin. Ajusta __str__ para mostrar "Hamlet — Shakespeare".
Ejercicio 2: una página de socios
Añade la vista socios (con HttpResponse y HTML mínimo listando a Luis, Marta y Pau con sus carnés LUIS-001, MARTA-002 y PAU-003) y conéctala a la URL /socios/. Compruébala en el navegador.
Ejercicio 3: precios de socio en la consola de Django
python manage.py shell abre un intérprete con tu proyecto cargado. Úsalo para recorrer todos los libros e imprimir titulo: precio_final(socio=True). Verifica contra los valores canónicos: 12.35, 9.83, 15.71 y 20.75. (Adelanto inevitable: para "todos los libros" necesitarás Libro.objects.all() — la puerta de 10-05.)
Soluciones
Ejercicio 1:
class Libro(models.Model):
titulo = models.CharField(max_length=200)
autor = models.CharField(max_length=100, default="")
# ... resto igual ...
def __str__(self):
return f"{self.titulo} — {self.autor}"El default="" es necesario: la base de datos ya contiene libros y Django necesita saber qué poner en la columna nueva de las filas existentes (si no lo das, makemigrations te lo preguntará interactivamente).
Ejercicio 2:
# catalogo/views.py
def socios(request):
return HttpResponse(
"<h1>Socios de Papyrus</h1>"
"<ul><li>Luis — LUIS-001</li>"
"<li>Marta — MARTA-002</li>"
"<li>Pau — PAU-003</li></ul>"
)# papyrus_web/urls.py
urlpatterns = [
path("admin/", admin.site.urls),
path("", views.inicio),
path("socios/", views.socios),
]HTML en cadenas otra vez — igual de incómodo que en Flask. Las plantillas Django que lo arreglan llegan en 10-05.
Ejercicio 3:
>>> from catalogo.models import Libro
>>> for libro in Libro.objects.all():
... print(f"{libro.titulo}: {libro.precio_final(socio=True)}")
La Odisea: 12.35
Hamlet: 9.83
El Quijote: 15.71
Fausto: 20.75Los cuatro números canónicos, ahora saliendo de una base de datos en lugar de un JSON. Libro.objects.all() es tu primer QuerySet: la sintaxis completa de consultas te espera en la siguiente lección.
Conclusión
Django ha mostrado sus cartas: una estructura impuesta pero clara (settings.py configura, urls.py enruta, views.py responde, models.py define los datos), el patrón MTV separando datos, lógica y presentación, y sobre todo las baterías — el modelo Libro que heredando de models.Model gana la persistencia que en Flask programaste a mano sobre JSON, las migraciones que versionan la forma de tus datos, y ese admin que convirtió a Marta en gestora del inventario con tres líneas de código. La dataclass de M5 y el modelo Django son el mismo libro con distinta cubierta: mismos datos, mismos precio_final() y hay_stock(), mismos 12.35, 9.83, 15.71 y 20.75. Pero de momento los visitantes solo ven un HttpResponse con HTML incrustado: nada de catálogo navegable, ni búsquedas, ni altas públicas. En la próxima lección completaremos la app: consultas con QuerySets, plantillas con herencia (te sonarán muchísimo), formularios con ModelForm que validan solos, y la web de Papyrus terminada por segunda vez — misma funcionalidad, dos caminos, y tú sabiendo recorrer ambos.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
