Hablando de Django
Publicado el 23 May 2025
Me gustaría empezar diciendo que Django es solo Python, mientras más cómodos nos sintamos usando Python mejor nos moveremos en Django.
Realmente no se necesita un conocimiento profundo de Python, pero hay un par de conceptos que creo que es importante mencionar
Módulos
En Python un módulo es un archivo con extensión .py que contiene código de Python.
Paquetes
Es una carpeta que contiene varios módulos, contiene un archivo __init__.py lo cual ayuda al intérprete a identificar la carpeta como un módulo de Python.
Ciclo de solicitud/respuesta
Django (entre otros frameworks web) tiene algo llamado ciclo de solicitud/respuesta, en inglés request/response cicle. El siguiente diagrama

En la reunión vimos los principales archivos que posibilitan este flujo en Django y como usarlos. El resto de este artículo está dedicado a explicar ese flujo.
Creando el entorno virtual
Es conveniente tener un entorno virtual. Durante la reunión usamos venv que usualmente viene junto con la instalación de Python. Dependiendo de nuestro sistema operativo utilizaremos uno de los siguientes grupos de comandos:
Windows
Usando la interfaz gráfica de Windows creamos una carpeta para almacenar nuestro proyecto, en este caso la llamaremos miproyecto
Abrimos el CMD y navegamos hasta esa carpeta, suponiendo que creamos las carpeta en el disco local C ejecutariamos los siguiente
cd C:\miproyecto
Creamos el entorno virtual dandole un nombre al entorno
python -m venv mientorno
Activamos el entorno virtual
mientorno\Scripts\activate
Linux
# crea una carpeta en el root del usuario mkdir ~/miproyecto # entra en esa carpeta cd ~/miproyecto # crea un entorno usando venv python3 -m venv mientorno # activa el entorno source venv/bin/activate
Instalando e Inicializando Django
Para instalar Django ejecuta el siguiente comando
pip install django
Podemos verificar que hemos instalado Django con el siguiente comando
pip list
Deberíamos ver la siguiente salida en la terminal
Package Version ----------------- ------- asgiref 3.8.1 Django 5.2.1 pip 22.0.2 setuptools 59.6.0 sqlparse 0.5.3 typing_extensions 4.13.2
Inicializamos nuestro proyecto con el siguiente comando
django-admin startproject miproyecto .
El punto al final del comando indica que no queremos que cree una carpeta envolviendo nuestro proyecto. Recordando que ya estamos dentro de una carpeta que envuelve nuestro proyecto.
Revisando los archivos
En este punto deberíamos tener la siguiente estructura de archivos
miproyecto ├── manage.py └── miproyecto ├── asgi.py ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py
Entre estos el más importante es settings.py aquí van todas las configuraciones de nuestro proyecto, entre ellas por el momento nos importan 2 en particular, los paquetes instalados y las rutas registradas.
# . . . algunas configuraciones antes # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] # . . . más configuracinoes en el medio ROOT_URLCONF = 'miproyecto.urls' # . . . más configuraciones despues
Django usa el módulo indicado por ROOT_URLCONF para saber a qué rutas responder. Es como la puerta de entrada de nuestro servidor.
# miproyecto/urls.py from django.contrib import admin from django.urls import path urlpatterns = [ path('admin/', admin.site.urls), ]
Las rutas se definen dentro de urlpatterns usando la función path
Por defecto nuestro proyecto tiene solo una ruta registrada que dirige hacia el admin de Django.
Creando Nuestras Rutas
La función path recibe por lo menos 2 parametros:
- un path (url) válido
- una función que devuelva una instancia de HttpResponse
Veamos como hacerlo
# miproyecto/urls.py from django.contrib import admin from django.urls import path from django.http import HttpResponse def mivista(request): return HttpResponse("hola mundo") urlpatterns = [ path('admin/', admin.site.urls), path('inicio', mivista) ]
Guardamos los cambios e iniciamos el servidor de desarrollo de Django desde la terminal con el siguiente comando:
python manage.py runserver
Ahora visitamos la ruta que acabamos de registrar http://127.0.0.1:8000/inicio

Tada!!! 🎉 ✨🎊
¿Viste? Es magia... bueno, en realidad es solo Python.
¿Qué acaba de suceder? Creamos la ruta "inicio" y le asociamos la función mivista.
Volvamos a nuestro flujograma inicial para repasar como hemos implementado el ciclo de solicitud y respuesta para la ruta "inicio"

Ok, simplificamos un montón el proceso, pero esto es solo para mostrar el concepto. Ahora que sabemos lo mínimo que necesitamos para generar una respuesta, usemos algunas de las herramientas que vienen con Django.
Creando nuestra primera app
En Django se le llama app a los paquetes que descargamos o a los que nosotros mismos desarrollamos. Desarrollaremos una página que muestre información de algunos platos de comida.
Para comenzar a desarrollar nuestra app usaremos el comando startapp seguido del nombre de la app.
python manage.py startapp comida
Ahora deberíamos tener la siguiente estructura de archivos
miproyecto ├── comida │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── db.sqlite3 ├── manage.py └── miproyecto ├── asgi.py ├── __init__.py ├── __pycache__ ├── settings.py ├── urls.py └── wsgi.py
Django solo conoce las apps que están registradas en INSTALLED_APPS . Para registrar nuestra app agregaremos el nombre a la lista.
# Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'comida', # la agregamos aquí ]
Hay varias cosas que Django buscará en nuestra app cada vez que se inicie el servidor de desarrollo, de eso hablaremos dentro de poco, por ahora lo importante es decir que Django ya sabe que nuestra app existe.
Dentro de nuestra app recién creada hay un archivo destinado a colocar las funciones asociadas a las rutas, démosle un vistazo.
# archivo comida/views.py from django.shortcuts import render # Create your views here.
Como nos lo indica el nombre de este módulo a las funciones asociadas a una ruta se les denomina vista.
La función render que vemos importada al comienzo también devuelve una instancia de la clase HttpResponse lo que significa que la podemos utilizar para enviar una respuesta. render recibe los siguientes argumentos:
- objeto request
- una cadena de texto con el nombre de una plantilla
- un diccionario de Python que se pasa como variables a la plantilla
# archivo comida/views.py from django.shortcuts import render def lista_comidas(request): contexto = { "comida": "empanada" } return render(request, "comida/index.html", contexto)
Ok, nuestra vista está lista, pero falta crear la plantilla "comida/index.html".
Django busca plantillas dentro de todas las apps registradas y espera encontrarlas en una carpeta llamada templates.
Cuando llamamos a render Django buscará en TODAS las apps registradas y nos devolverá la primera plantilla que tenga ese nombre, por esa razón debemos crear una carpeta intermedia con el nombre de la app (hay otro enfoque que veremos en otra oportunidad)
Al crear nuestra plantilla tendremos la siguiente estructura de archivos
├── comida │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── templates # NUEVO django buscará esta carpeta al usar render │ │ └── comida # NUEVO esta es la carpeta intermedia │ │ └── index.html # NUEVO nuestra plantilla │ ├── tests.py │ └── views.py ├── db.sqlite3 ├── manage.py └── miproyecto ├── asgi.py ├── __init__.py ├── __pycache__ ├── settings.py ├── urls.py └── wsgi.py
Colocamos el siguiente contenido en la plantilla
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Comidas</title> </head> <body> <h1>Lista de Comidas</h1> <p>Los mejores platos de Latinoamérica</p> <ul> <li>{{comida}}</li> </ul> </body> </html>
Nuestra plantilla parece un archivo HTML común, pero el motor de plantillas de Django tiene la capacidad de inyectar variables, ejecutar bucles, leer condicionales y muchas más cosas. En este caso solo imprimirá la variable que le estamos pasando desde la vista.
Estamos casi listos, ahora tenemos que asociar nuestra vista a una ruta.
Crearemos un módulo llamado urls dedicado a guardar todas las rutas de nuestra app recién creada.
├── comida │ ├── admin.py │ ├── apps.py │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── comida │ │ └── index.html │ ├── tests.py │ ├── urls.py # NUEVO rutas de la app │ └── views.py ├── db.sqlite3 ├── manage.py └── miproyecto ├── asgi.py ├── __init__.py ├── __pycache__ ├── settings.py ├── urls.py └── wsgi.py
En este módulo colocaremos lo siguiente
# comidas/urls.py from django.urls import path from . import views urlpatterns = [ path('comidas', views.lista_comidas) ]
La única diferencia de este módulo de rutas con el que vimos al principio es que la vista está definida en un módulo diferente.
En la segunda línea podemos ver una importación relativa, que dice algo como "desde este mismo paquete importar el módulo views".
Aquí importamos todo el módulo, por lo que podemos usar la vista que definimos así views.lista_comidas
Este enfoque es útil para desacoplar un poco la lógica de nuestra app de comidas, pero no hay que olvidar que la "puerta de nuestro servidor" sigue siendo el módulo miproyecto/urls.py así que hay que conectar nuestra app con él.
Comentaré el código anterior y agregaré nuevo código al archivo miproyecto/urls.py
# miproyecto/urls.py from django.contrib import admin from django.urls import path, include # NUEVO importación # from django.http import HttpResponse # def mivista(request): # return HttpResponse("hola mundo") urlpatterns = [ path('admin/', admin.site.urls), path('', include("comida.urls")), # path('inicio', mivista) ]
Hay varias cosas aquí, lo más importante es que la función include nos permite incluir todo el módulo de rutas de nuestra app de comidas y asociarlo con una ruta. En este caso con una cadena de texto vacía, lo que significa que no usamos un prefijo para nuestras rutas.
Guardamos todos los cambios y visitamos la ruta http://127.0.0.1:8000/comidas recuerda revisar que el servidor de desarrollo esté rodando.

Bravo!! 👏👏👏
Sin duda este tomo muchos más pasos que nuestro primer ejemplo, pero hemos desacoplado la app de comidas del resto del proyecto, y esto nos ayudará a mantener las cosas organizadas, veamos nuevamente nuestro flujograma

Creando Modelos
El corazón de Django está en los modelos. Los modelos nos permiten interactuar con la base de datos sin tener que escribir SQL.
Cada app tiene un modulo dedicadoa los modelos llamado models.py e inicialmente se ve así:
# comida/models.py from django.db import models # Create your models here.
Ahora crearemos una clase que nos permita almacenar información de comidas en la base de datos.
# comida/models.py from django.db import models class Comida(models.Model): nombre = models.CharField(max_length=120) descripcion = models.TextField() calificacion = models.IntegerField() def __str__(self): return self.nombre
Aquí creamos una clase que extiende de model.Model, Django usará estos atributos para crear una tabla en la base de datos, si te interesa puedes consultar la documentación de todos los campos disponibles en Django. Finalmente, definimos el método __str__ que establece que texto se mostrará cuando imprimamos una instancia de esta clase (este método es algo propio de las clases en Python).
Migraciones
Las migraciones son el mecanismo que nos permite traducir cambios en el código de nuestros modelos (Python) en cambios a nuestra base de datos (SQL). Este mecanismo se da en un proceso de 2 pasos:
- Crear una migración
- Ejecutar una migración
Ejecuta el siguiente comando
python manage.py makemigrations
Deberías ver una salida en la terminal como esta
Migrations for 'comida': comida/migrations/0001_initial.py + Create model Comida
En la carpeta de la app de comidas deberías tener el siguiente archivo
comida ├── admin.py ├── apps.py ├── __init__.py ├── migrations │ ├── 0001_initial.py # NUEVO │ ├── __init__.py │ └── __pycache__ ├── models.py . . .
Démosle una revisada rápida
# Generated by Django 5.2.1 on 2025-05-23 14:38 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Comida', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('nombre', models.CharField(max_length=120)), ('descripcion', models.TextField()), ('calificacion', models.IntegerField()), ], ), ]
Este archivo es el que Django utilizará para realizar los cambios en la base de datos, normalmente no necesitaremos hacer cambios en el de forma directa a menos que algo salga mal durante el proceso de crear la migración.
En este punto nuestra base de datos está completamente vacía, hay un archivo llamado db.sqlite3 en el directorio base de nuestro proyecto pero este no contiene nada.
Ahora aplicaremos las migraciones con el siguiente comando:
python manage.py migrate
Deberíamos ver la siguiente salida en la terminal
Operations to perform: Apply all migrations: admin, auth, comida, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying comida.0001_initial... OK Applying sessions.0001_initial... OK
Lo que acaba de suceder es que Django hay recorrido todas las apps registradas buscando migraciones pendientes por ejecutar, casi todo lo que vemos aquí son migraciones que vienen creadas dentro de la instalación de Django, la migración de nuestro modelo está en la penúltima línea.
Ahora que hemos creado una tabla para almacenar datos de nuestras comidas, es hora de llenar esa tabla.
Shell de Django
Django tiene un Shell que nos da acceso al entorno de nuestro proyecto mientras ejecutamos código de Python. Para entrar al Shell usamos el siguiente comando:
python manage.py shell
Iniciamos importando nuestra clase Comida
from comida.models import Comida
Usamos la clase para consultar el contenido de la tabla de comidas (sabemos que está vacía).
Comida.objects.all() ### salida:<QuerySet []>
Esta especie de atributo llamado objects es un Manager. Los managers tienen muchos métodos útiles para interactuar con la tabla a la que la clase está relacionada. El método all que invocamos trae todos los datos de la tabla.
El tipo de la salida es un QuerySet, se parece mucho a una lista en Python, como esperábamos, esta QuerySet está vacía.
A continuación crearemos varias instancias de la clase Comida pasándole los atributos como argumentos:
empanadas = Comida(nombre="Empanadas", descripcion="Platillo típico venezolano hecho a base de masa de maíz precocido (similar a la de las arepas), que se rellena con diversos ingredientes —como carne mechada, pollo, queso, cazón (pescado), o caraotas (frijoles negros)— y luego se fríe hasta obtener una textura crujiente por fuera y suave por dentro.", calificacion=5) ceviche = Comida(nombre="Ceviche", descripcion="Plato fresco y sabroso preparado con pescado crudo (generalmente corvina o lenguado) marinado en jugo de limón, acompañado de cebolla roja, ají limo, cilantro y sal. Se sirve tradicionalmente con camote (batata dulce), maíz cocido (choclo) y, a veces, cancha (maíz tostado)", calificacion=5) bandeja_paisa = Comida(nombre="Bandeja Paisa", descripcion="La bandeja paisa es uno de los platos más tradicionales y abundantes de la cocina colombiana, especialmente de la región de Antioquia. Se caracteriza por su generosa variedad de ingredientes servidos juntos en una sola bandeja: arroz blanco, carne molida o en bistec, chicharrón crujiente, huevo frito, plátano maduro, frijoles rojos, arepa, aguacate y morcilla.", calificacion=5)
Podemos revisar los atributos de estas instancias
empanadas.nombre # salida: 'Empanadas' bandeja_paisa.calificacion # salida: 5 ceviche # salida: <Comida: Ceviche> # la salida de imprimir la instancia "ceviche" es "Ceviche" porque el método __str__ que definimos devuelve el atributo nombre de la instancia
Muy bien, entonces si consultamos nuestra tabla nuevamente tendremos lo siguiente
Comida.objects.all() # salida:<QuerySet []> # Sí, no hay nada en esta tabla
La razón de porque la tabla continúa vacía es porque las instancias se crean "en memoria" y para "enviarlas" a la base de datos es necesario ejecutar el método save de la siguiente forma:
empanadas.save() bandeja_paisa.save() ceviche.save() Comida.objects.all() # salida: <QuerySet [<Comida: Empanadas>, <Comida: Bandeja Paisa>, <Comida: Ceviche>]> # Ahora sí hemos guardado las comidas en nuestra base de datos
En ocasiones querremos crear un nuevo registro en una tabla sin tener que primero crear una instancia en memoria, esto se puede hacer usando el método create del manager, de la siguiente forma:
Comida.objects.create(nombre="Asado", calificacion=5, descripcion="El asado es mucho más que una comida: es una tradición y un ritual social en Argentina. Consiste en carne de res (como costillas, vacío, chorizo, morcilla y otros cortes) cocinada lentamente a las brasas en una parrilla. Se suele acompañar con chimichurri (una salsa a base de ajo, perejil, vinagre y especias), ensaladas frescas y pan.") # salida <Comida: Asado> # devuelve como salida la instancia (después de guardar el registro en la base de datos)
Para salir del Shell escribimos exit()
Bien, ahora que hemos poblado nuestra tabla podemos recuperar esos datos en la vista y así mostrarlos en la respuesta.
Haciendo Consultas desde la Vista
Editemos comida/views.py de la siguiente forma
from django.shortcuts import render from .models import Comida # importamos nuestra clase def lista_comidas(request): contexto = { "comidas": Comida.objects.all() # repetimos nuestra consulta asociandola a la clave "comidas" (nótese que la clave ahora está en plural) } return render(request, "comida/index.html", contexto)
Ahora actualicemos la plantilla para poder imprimir nuestra QuerySet
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Comidas</title> <style> * { margin:0; box-sizing: border-box; } body { margin-inline: auto; max-width: 56rem; text-align: center; padding: 1rem; font-family: sans-serif; } header { margin-bottom: 1.25rem; } .subtitulo { font-size: 1.25rem; margin-top: 0.5rem; } .comidas { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 1rem; } .comida { flex: 1 1 calc(50% - 0.75rem); display: flex; flex-direction:column; gap: 1rem; min-width: min(100%, 20rem); padding: 1rem; border-radius: 0.5rem; background-color: #8ae1fc; } .comida-nombre { font-size: 1.5rem; } .comida-descripcion { opacity: 0.8; } .comida-calificacion { flex: 1 0; display: grid; place-items: end center; } </style> </head> <body> <header> <h1>Lista de Comidas</h1> <p class="subtitulo"><em>Los mejores platos de latinoamérica</em></p> </header> <main> <ul class="comidas"> {% for comida in comidas %} <li class="comida"> <p class="comida-nombre">{{ comida }}</p> <p class="comida-descripcion">{{ comida.descripcion }}</p> <p class="comida-calificacion">Calificación: {{ comida.calificacion }}</p> </li> {% endfor %} </ul> </main> </body> </html>
Estamos iterando nuestro QuerySet usando una sintaxis similar a como se iteran las colecciones en Python, luego dentro del bucle estamos imprimiendo los atributos de la instancia que se está iterando, al final debemos indicar en donde se cierra el bucle.
Agregue algunos estilos dentro de una tag style para mejorar la apariencia. En otra oportunidad podríamos ver como se cargan los estilos (archivos estáticos) en Django.
Finalmente iniciamos el servidor de desarrollo (si no está rodando) y visitamos nuestra ruta http://127.0.0.1:8000/comidas

¡Lo hemos logrado! 🚀🚀🚀
Echémosle un vistazo a nuestro diagrama de flujo de solicitud y respuesta para la ruta comidas

Ha sido un gusto acompañarte en este proceso. Si tienes alguna duda, consulta o algo que quieras compartir conmigo puedes usar el formulario de contacto que está continuación, te deseo la mejor de las suertes y mucho éxito.