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

  1. Filosofía: baterías incluidas (cuándo Django, cuándo Flask)
  2. Instalación y startproject: nace papyrus_web
  3. Proyecto vs app: startapp catalogo
  4. La estructura de ficheros, fichero a fichero
  5. El patrón MTV (y su primo MVC)
  6. Primera vista: HttpResponse y urls.py
  7. El modelo Libro: de dataclass a models.Model
  8. Migraciones: makemigrations y migrate
  9. El admin de Django: el regalo estrella
  10. runserver y 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_web

pip 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:

python manage.py startapp catalogo

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.py má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:

python manage.py runserver

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:

@dataclass
class Libro:
    titulo: str
    precio: float
    stock: int = 0

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.titulo

Compara 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:

  • makemigrations compara tus models.py con el último estado conocido y escribe la receta del cambio (un fichero Python en migrations/): "crear la tabla de Libro con estas columnas".
  • migrate aplica 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:

from django.contrib import admin
from .models import Libro

admin.site.register(Libro)

Un superusuario para entrar (te pedirá usuario y contraseña por terminal):

python manage.py createsuperuser

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            # aplicarlo

Errores Comunes y Consejos

  • Olvidar registrar la app en INSTALLED_APPS: el síntoma clásico es makemigrations respondiendo "No changes detected". Django solo mira las apps registradas.
  • Editar models.py y no migrar: al acceder verás OperationalError: no such table. Modelo cambiado = makemigrations + migrate, siempre, en ese orden.
  • Confundir django-admin con manage.py: django-admin solo para startproject; después, todo con python manage.py ..., que carga la configuración de tu proyecto.
  • Usar FloatField para dinero: la tentación viene de la dataclass. DecimalField existe precisamente para importes; acostúmbrate desde el primer modelo.
  • Borrar db.sqlite3 o migrations/ "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 arranca manage.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}"
python manage.py makemigrations catalogo   # crea 0002_libro_autor.py
python manage.py migrate

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.75

Los 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados