Si llevas un tiempo programando en Python, en algún momento habrás visto una línea de código que crea una lista entera en una sola expresión y te habrás preguntado cómo funciona. Ese mecanismo se llama comprensión de listas (list comprehension) y es una de las características más elegantes y características del lenguaje.
No es solo una forma más corta de escribir un bucle. Cuando la usas bien, el código es más legible, más rápido y más expresivo. En esta guía aprenderás desde la sintaxis básica hasta los casos de uso avanzados, con ejemplos reales para cada situación.
El problema que resuelve: el bucle clásico
Antes de ver la solución, hay que entender el problema. Supón que tienes una lista de números y quieres crear una nueva lista con el cuadrado de cada uno. La forma clásica con un bucle for es esta:
numeros = [1, 2, 3, 4, 5]
cuadrados = []
for n in numeros:
cuadrados.append(n ** 2)
print(cuadrados) # [1, 4, 9, 16, 25]
Funciona perfectamente. Pero requiere tres líneas para algo conceptualmente simple: dame una lista con el cuadrado de cada número. La comprensión de listas expresa exactamente esa idea en una sola línea.
La sintaxis básica
La estructura de una comprensión de listas sigue siempre este patrón:
[expresion for elemento in iterable]
Se lee de izquierda a derecha: dame la expresión, para cada elemento, del iterable. El ejemplo anterior queda así:
numeros = [1, 2, 3, 4, 5]
cuadrados = [n ** 2 for n in numeros]
print(cuadrados) # [1, 4, 9, 16, 25]
Una línea, sin variable auxiliar, sin append(). La expresión puede ser cualquier cosa: una operación matemática, una llamada a función, una transformación de cadenas.
palabras = ["python", "es", "genial"]
mayusculas = [p.upper() for p in palabras]
print(mayusculas) # ['PYTHON', 'ES', 'GENIAL']
longitudes = [len(p) for p in palabras]
print(longitudes) # [6, 2, 6]
nombres = [" Ana ", " Carlos ", " María "]
limpios = [n.strip() for n in nombres]
print(limpios) # ['Ana', 'Carlos', 'María']
Añadir una condición: el filtro con if
Puedes añadir un if al final para incluir solo los elementos que cumplan una condición. La estructura completa es:
[expresion for elemento in iterable if condicion]
Solo los elementos donde la condición sea verdadera pasan a la nueva lista:
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pares = [n for n in numeros if n % 2 == 0]
print(pares) # [2, 4, 6, 8, 10]
impares = [n for n in numeros if n % 2 != 0]
print(impares) # [1, 3, 5, 7, 9]
mayores = [n for n in numeros if n > 5]
print(mayores) # [6, 7, 8, 9, 10]
# Combinar transformación y filtro
cuadrados_pares = [n ** 2 for n in numeros if n % 2 == 0]
print(cuadrados_pares) # [4, 16, 36, 64, 100]
Un caso muy habitual: filtrar cadenas vacías o valores nulos de una lista:
datos = ["Ana", "", "Carlos", None, "María", "", "Luis"]
validos = [d for d in datos if d] # filtra cualquier valor falsy
print(validos) # ['Ana', 'Carlos', 'María', 'Luis']
El if-else dentro de la expresión
Atención: hay una diferencia importante entre poner el if al final (filtro) y ponerlo dentro de la expresión (transformación condicional). Cuando usas if-else dentro de la expresión, todos los elementos pasan a la nueva lista, pero se transforman de forma diferente según la condición.
# if al FINAL → filtra (elimina elementos)
[expresion for elemento in iterable if condicion]
# if-else en la EXPRESIÓN → transforma (todos los elementos quedan)
[valor_si_true if condicion else valor_si_false for elemento in iterable]
numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Etiquetar cada número como par o impar
etiquetas = ["par" if n % 2 == 0 else "impar" for n in numeros]
print(etiquetas)
# ['impar', 'par', 'impar', 'par', 'impar', 'par', 'impar', 'par', 'impar', 'par']
# Reemplazar negativos por cero
valores = [-3, 5, -1, 8, -7, 2, 4]
sin_negativos = [v if v >= 0 else 0 for v in valores]
print(sin_negativos) # [0, 5, 0, 8, 0, 2, 4]
# Normalizar texto: mayúscula si tiene más de 3 letras, minúscula si no
palabras = ["hola", "a", "Python", "es", "increíble"]
resultado = [p.upper() if len(p) > 3 else p.lower() for p in palabras]
print(resultado) # ['HOLA', 'a', 'PYTHON', 'es', 'INCREÍBLE']
Comprensiones anidadas: iterar sobre estructuras en dos dimensiones
Puedes usar más de un for en una comprensión de listas para iterar sobre estructuras anidadas como matrices o listas de listas. El orden de los for en la comprensión es el mismo que en los bucles anidados equivalentes.
# Aplanar una matriz (lista de listas) en una sola lista
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
plana = [n for fila in matriz for n in fila]
print(plana) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Equivalente con bucles anidados:
# plana = []
# for fila in matriz:
# for n in fila:
# plana.append(n)
# Combinaciones de dos listas (producto cartesiano)
colores = ["rojo", "verde", "azul"]
tallas = ["S", "M", "L"]
variantes = [(c, t) for c in colores for t in tallas]
print(variantes)
# [('rojo','S'), ('rojo','M'), ('rojo','L'), ('verde','S'), ...]
# Transponer una matriz
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transpuesta = [[fila[i] for fila in matriz] for i in range(3)]
print(transpuesta) # [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
Con las comprensiones anidadas, la legibilidad puede disminuir si abusas de ellas. Una buena regla: si necesitas más de dos for o la línea resulta difícil de leer en voz alta, usa bucles normales.
Comprensión de conjuntos y diccionarios
La misma sintaxis funciona para crear conjuntos (set) y diccionarios (dict), simplemente cambiando los corchetes por llaves.
Comprensión de conjuntos
# Eliminar duplicados y transformar al mismo tiempo
palabras = ["python", "java", "python", "javascript", "java", "go"]
unicos = {p.upper() for p in palabras}
print(unicos) # {'PYTHON', 'JAVA', 'JAVASCRIPT', 'GO'} (sin duplicados, sin orden)
# Longitudes únicas de palabras
longitudes_unicas = {len(p) for p in palabras}
print(longitudes_unicas) # {2, 4, 6, 10}
Comprensión de diccionarios
# Invertir claves y valores
original = {"a": 1, "b": 2, "c": 3}
invertido = {v: k for k, v in original.items()}
print(invertido) # {1: 'a', 2: 'b', 3: 'c'}
# Crear un diccionario a partir de dos listas
nombres = ["Ana", "Carlos", "María"]
edades = [28, 34, 25]
personas = {n: e for n, e in zip(nombres, edades)}
print(personas) # {'Ana': 28, 'Carlos': 34, 'María': 25}
# Filtrar entradas de un diccionario
precios = {"manzana": 1.2, "banana": 0.8, "cereza": 4.5, "kiwi": 2.1}
caros = {fruta: precio for fruta, precio in precios.items() if precio > 1.5}
print(caros) # {'cereza': 4.5, 'kiwi': 2.1}
# Transformar valores
con_iva = {fruta: round(precio * 1.21, 2) for fruta, precio in precios.items()}
print(con_iva) # {'manzana': 1.45, 'banana': 0.97, 'cereza': 5.45, 'kiwi': 2.54}
Expresiones generadoras: la variante eficiente en memoria
Si cambias los corchetes por paréntesis obtienes una expresión generadora. La sintaxis es idéntica, pero en lugar de crear toda la lista en memoria de una vez, genera los elementos uno a uno bajo demanda. Es la opción correcta cuando no necesitas la lista completa, solo procesar sus elementos.
# List comprehension: crea toda la lista en memoria
cuadrados_lista = [n ** 2 for n in range(1_000_000)] # ocupa ~8 MB
# Expresión generadora: genera cada valor solo cuando se necesita
cuadrados_gen = (n ** 2 for n in range(1_000_000)) # ocupa ~120 bytes
# Uso típico: pasar directamente a funciones que aceptan iterables
total = sum(n ** 2 for n in range(1000))
maximo = max(len(p) for p in ["python", "es", "increíble"])
hay_par = any(n % 2 == 0 for n in [1, 3, 5, 6, 7])
print(total) # 332833500
print(maximo) # 9
print(hay_par) # True
Fíjate en que al pasar una expresión generadora como único argumento a una función, los paréntesis del generador y los de la función se pueden compartir. sum(n ** 2 for n in range(1000)) es más limpio que sum((n ** 2 for n in range(1000))).
Casos de uso reales
Estos son los patrones más frecuentes que encontrarás en proyectos reales.
Procesar respuestas de una API
# Respuesta típica de una API REST
usuarios = [
{"id": 1, "nombre": "Ana García", "activo": True, "rol": "admin"},
{"id": 2, "nombre": "Carlos López", "activo": False, "rol": "usuario"},
{"id": 3, "nombre": "María Torres", "activo": True, "rol": "usuario"},
{"id": 4, "nombre": "Luis Martín", "activo": True, "rol": "admin"},
]
# Extraer solo los nombres
nombres = [u["nombre"] for u in usuarios]
# Filtrar los activos
activos = [u for u in usuarios if u["activo"]]
# Obtener los IDs de los administradores activos
admin_ids = [u["id"] for u in usuarios if u["activo"] and u["rol"] == "admin"]
print(admin_ids) # [1, 4]
# Crear un diccionario id → nombre para búsquedas rápidas
indice = {u["id"]: u["nombre"] for u in usuarios}
print(indice[3]) # María Torres
Limpiar y normalizar datos
registros = [
" Ana García ",
"CARLOS LÓPEZ",
"",
"maría torres",
" ",
"Luis Martín"
]
limpios = [
r.strip().title()
for r in registros
if r.strip() # elimina cadenas vacías o solo espacios
]
print(limpios) # ['Ana García', 'Carlos López', 'María Torres', 'Luis Martín']
Trabajar con archivos y rutas
from pathlib import Path
carpeta = Path("documentos")
# Listar solo archivos Python
archivos_py = [f.name for f in carpeta.iterdir() if f.suffix == ".py"]
# Obtener rutas completas de todos los CSVs en subdirectorios
csvs = [str(f) for f in carpeta.rglob("*.csv")]
# Leer todas las primeras líneas de archivos .txt
primeras_lineas = [
open(f, encoding="utf-8").readline().strip()
for f in carpeta.glob("*.txt")
if f.stat().st_size > 0
]
Operaciones con matrices
# Multiplicar todos los elementos de una matriz por un escalar
matriz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
por_dos = [[n * 2 for n in fila] for n in matriz for n in fila]
# Forma más clara
por_dos = [[celda * 2 for celda in fila] for fila in matriz]
print(por_dos) # [[2, 4, 6], [8, 10, 12], [14, 16, 18]]
# Extraer la diagonal principal
diagonal = [matriz[i][i] for i in range(len(matriz))]
print(diagonal) # [1, 5, 9]
# Filtrar filas cuya suma supere un umbral
filas_grandes = [fila for fila in matriz if sum(fila) > 10]
print(filas_grandes) # [[4, 5, 6], [7, 8, 9]]
Cuándo NO usar comprensión de listas
La comprensión de listas es una herramienta, no una obligación. Hay situaciones donde un bucle for normal es la mejor opción.
Cuando la lógica es compleja. Si necesitas varios pasos, variables intermedias o condiciones anidadas complicadas, un bucle es más fácil de leer y depurar:
# Difícil de leer: demasiada lógica comprimida
resultado = [procesar(x) for x in datos if validar(x) and x not in vistos and not es_duplicado(x, historial)]
# Mejor así
resultado = []
for x in datos:
if not validar(x):
continue
if x in vistos or es_duplicado(x, historial):
continue
resultado.append(procesar(x))
Cuando tienes efectos secundarios. Si dentro del bucle necesitas modificar variables externas, imprimir mensajes de progreso, actualizar una base de datos o hacer varias operaciones relacionadas, usa un for normal. Las comprensiones de listas están diseñadas para producir un valor, no para ejecutar acciones.
# Mal uso: efectos secundarios dentro de una comprensión
[print(x) for x in lista] # funciona, pero es un antipatrón
# Correcto
for x in lista:
print(x)
Cuando el resultado no es una lista. Si solo necesitas procesar los elementos sin guardarlos, una expresión generadora con sum(), any(), all() o max() es más eficiente que crear una lista completa para usarla una sola vez.
Rendimiento: comprensión vs bucle vs map()
Las comprensiones de listas son generalmente más rápidas que los bucles for equivalentes porque están optimizadas internamente en CPython. La diferencia es más notable en listas grandes.
import timeit
datos = list(range(100_000))
# Bucle for con append
tiempo_for = timeit.timeit(
"[n * 2 for n in range(100_000)]", # comprensión
number=100
)
# map() con lambda
tiempo_map = timeit.timeit(
"list(map(lambda n: n * 2, range(100_000)))",
number=100
)
print(f"Comprensión: {tiempo_for:.3f}s")
print(f"map(): {tiempo_map:.3f}s")
# La comprensión suele ser ligeramente más rápida o igual que map() con lambda.
# map() con una función nativa (sin lambda) puede ser más rápido:
tiempo_map2 = timeit.timeit(
"list(map(str, range(100_000)))",
number=100
)
# En este caso map(str, ...) gana porque str es una función de C puro.
En la práctica, la diferencia de rendimiento rara vez importa para listas normales. Elige la forma que sea más legible. Optimiza solo cuando hayas medido que el rendimiento es un problema real.
Resumen
La comprensión de listas es uno de los rasgos más distintivos de Python. Dominarla marca la diferencia entre escribir código que simplemente funciona y escribir código que comunica claramente su intención.
- La sintaxis básica es
[expresion for elemento in iterable]. - Añade
if condicional final para filtrar elementos. - Usa
valor_true if condicion else valor_falseen la expresión para transformaciones condicionales donde todos los elementos se conservan. - Puedes anidar varios
forpara trabajar con estructuras multidimensionales, aunque más de dos suele perjudicar la legibilidad. - Usa
{}en lugar de[]para crear conjuntos o diccionarios con la misma sintaxis. - Usa paréntesis
()para obtener una expresión generadora que ahorra memoria cuando no necesitas la lista completa. - No abuses: si la lógica es compleja o hay efectos secundarios, un bucle
fornormal es la opción correcta.
No hay comentarios todavía. Sé el primero en compartir tu opinión.