El módulo 3 terminó señalando una grieta: el catálogo de Papyrus vive repartido en tres listas paralelas (catalogo, precios, stocks) que Ana debe mantener sincronizadas a mano. Antes de resolver esa grieta con diccionarios (lección 04-03), necesitas dominar de verdad la estructura que has estado usando desde el módulo 1 casi sin mirarla: la lista. Hasta ahora has recorrido listas con for, enumerate() y zip(), y has usado append() de pasada; en esta lección las abrimos por dentro: indexación, slicing, todos sus métodos importantes y —lo más delicado— qué significa que sean mutables.

Contenido

  1. Crear listas
  2. Indexación positiva y negativa
  3. Slicing a fondo
  4. Métodos de listas: añadir, quitar y buscar
  5. Ordenar: sort() vs sorted() y reverse()
  6. Mutabilidad: alias vs copia
  7. Listas anidadas
  8. Pertenencia: in y not in

Crear listas

Una lista es una secuencia ordenada y mutable de elementos, que pueden ser de cualquier tipo (incluso mezclados, aunque no suele ser buena idea).

# Las tres listas paralelas de Papyrus, tal y como quedaron en el módulo 3
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]

vacia = []                    # lista vacía, lista para ir llenándola
tambien_vacia = list()        # equivalente
desde_range = list(range(5))  # [0, 1, 2, 3, 4]: list() convierte iterables
letras = list("Ana")          # ['A', 'n', 'a']: una cadena también es iterable
  • [] crea una lista literal; list(iterable) construye una lista a partir de cualquier cosa recorrible.
  • Recuerda del módulo 2 que también existen las comprensiones: [p * 1.04 for p in precios] crea una lista nueva calculada. Aquí nos centramos en manipular listas ya existentes.

Indexación positiva y negativa

Cada elemento tiene una posición (índice) que empieza en 0. Los índices negativos cuentan desde el final: -1 es el último elemento.

catalogo = ["La Odisea", "Hamlet", "El Quijote"]

print(catalogo[0])    # La Odisea   (primero)
print(catalogo[2])    # El Quijote  (tercero)
print(catalogo[-1])   # El Quijote  (último, sin saber cuántos hay)
print(catalogo[-2])   # Hamlet      (penúltimo)
Elemento Índice positivo Índice negativo
"La Odisea" 0 -3
"Hamlet" 1 -2
"El Quijote" 2 -1

Acceder a un índice que no existe (catalogo[10]) provoca un IndexError que detiene el programa. En el módulo 7 aprenderás a capturar ese error; de momento, comprueba antes con len().

Slicing a fondo

El slicing (rebanado) extrae una sublista nueva con la sintaxis lista[inicio:fin:paso]. Regla de oro: el inicio se incluye, el fin no.

titulos = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]

print(titulos[0:2])   # ['La Odisea', 'Hamlet']         índices 0 y 1
print(titulos[1:])    # ['Hamlet', 'El Quijote', 'Fausto']  desde 1 hasta el final
print(titulos[:2])    # ['La Odisea', 'Hamlet']         desde el principio hasta 1
print(titulos[:])     # copia completa (¡lo veremos en mutabilidad!)
print(titulos[::2])   # ['La Odisea', 'El Quijote']     de dos en dos
print(titulos[::-1])  # lista invertida, sin tocar la original
print(titulos[-2:])   # ['El Quijote', 'Fausto']        los dos últimos

Puntos que conviene interiorizar:

  • Omitir inicio significa "desde el principio"; omitir fin, "hasta el final".
  • Un slice nunca da error por pasarse: titulos[1:100] devuelve lo que haya, sin IndexError.
  • paso negativo recorre hacia atrás; [::-1] es el idiom clásico para invertir.
  • El resultado es siempre una lista nueva: modificarla no afecta a la original.

Métodos de listas: añadir, quitar y buscar

Los métodos se llaman con la sintaxis lista.metodo(...). La mayoría modifican la lista en el sitio y devuelven None — fuente clásica de despistes.

Método Qué hace Devuelve
append(x) Añade x al final None
insert(i, x) Inserta x en la posición i, desplazando el resto None
extend(otra) Añade todos los elementos de otro iterable al final None
remove(x) Elimina la primera aparición de x (error si no está) None
pop(i) Elimina y devuelve el elemento en i (por defecto, el último) el elemento
index(x) Posición de la primera aparición de x (error si no está) int
count(x) Cuántas veces aparece x int

Veámoslos en acción con la llegada de "Fausto" a Papyrus (de momento con stock 0: está agotado en la distribuidora):

catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]

# Alta de "Fausto": hay que tocar LAS TRES listas, y en el mismo orden
catalogo.append("Fausto")
precios.append(21.00)
stocks.append(0)

print(catalogo.index("Hamlet"))   # 1 → sirve para localizar su precio: precios[1]
print(catalogo.count("Fausto"))   # 1

# append vs extend: la diferencia importa
novedades = ["Ilíada", "Macbeth"]
copia = catalogo[:]
copia.append(novedades)   # ['La Odisea', ..., 'Fausto', ['Ilíada', 'Macbeth']]  ¡lista dentro de lista!
copia = catalogo[:]
copia.extend(novedades)   # ['La Odisea', ..., 'Fausto', 'Ilíada', 'Macbeth']    elementos sueltos

Fíjate en el coste de mantenimiento: dar de alta un libro exige tres append() coordinados, y un remove() olvidado en una de las listas desincroniza precios y stocks para siempre. Este dolor es deliberado: es el gancho que resolveremos en 04-03.

La cola de pedidos

Las listas también sirven como cola: los pedidos entran por el final (append) y se atienden por el principio (pop(0)):

cola_pedidos = []
cola_pedidos.append("Luis: El Quijote")
cola_pedidos.append("Marta: Hamlet")
cola_pedidos.append("Luis: Fausto")

while cola_pedidos:                      # truthiness: lista vacía es False
    pedido = cola_pedidos.pop(0)         # saca y devuelve el primero
    print(f"Atendiendo -> {pedido}")

Funciona, pero pop(0) obliga a Python a desplazar todos los elementos restantes una posición. Para colas largas existe una estructura pensada exactamente para esto, collections.deque, que verás en 04-06.

Ordenar: sort() vs sorted() y reverse()

Esta distinción resume la filosofía de los métodos de lista:

lista.sort() sorted(lista)
¿Qué modifica? La lista original, en el sitio Nada: crea una lista nueva
¿Qué devuelve? None La lista ordenada
¿Sobre qué funciona? Solo listas Cualquier iterable (tuplas, cadenas...)
Uso típico Cuando ya no necesitas el orden original Cuando quieres conservar el original
precios = [12.50, 9.95, 15.90, 21.00]

baratos_primero = sorted(precios)        # [9.95, 12.5, 15.9, 21.0]; precios intacto
caros_primero = sorted(precios, reverse=True)

precios.sort()                           # ahora sí: precios queda ordenado
resultado = precios.sort()               # ¡cuidado! resultado es None

catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]
catalogo.reverse()                       # invierte en el sitio (devuelve None)
por_longitud = sorted(catalogo, key=len) # el key= de lambdas (03-03) también vive aquí

Ojo con las listas paralelas: si Ana hace catalogo.sort(), los títulos se reordenan pero precios y stocks no, y el desastre está servido. Con zip() (03-02) se puede ordenar todo junto, pero es otro malabar más que los diccionarios harán innecesario.

Mutabilidad: alias vs copia

Aquí está la consecuencia más importante (y traicionera) de que las listas sean mutables. Asignar una lista a otra variable no la copia: crea un alias, un segundo nombre para la misma lista.

stocks = [4, 6, 8]
inventario = stocks          # ALIAS: las dos variables apuntan a LA MISMA lista

inventario.append(0)
print(stocks)                # [4, 6, 8, 0]  ← ¡stocks también "cambió"!
print(stocks is inventario)  # True: son el mismo objeto
graph LR
    stocks --> L["[4, 6, 8, 0]"]
    inventario --> L

Para tener una lista independiente hay que copiar explícitamente:

copia1 = stocks.copy()   # método copy()
copia2 = stocks[:]       # slice completo: idiom equivalente
copia3 = list(stocks)    # constructor: también copia

copia1.append(99)
print(stocks)            # sin cambios: la copia es otra lista

Esto explica también la trampa de los argumentos mutables por defecto que viste en 03-02: si una función recibe una lista y la modifica, el que llama nota el cambio, porque ambos comparten el mismo objeto.

Un matiz para dejar apuntado: copy() y [:] hacen una copia superficial (shallow). Si la lista contiene otras listas dentro, se copia la lista exterior pero las interiores siguen compartidas. Para copias profundas existe copy.deepcopy(); con saber que existe es suficiente por ahora.

Listas anidadas

Una lista puede contener listas. Es una forma primitiva de agrupar la ficha de cada libro:

fichas = [
    ["La Odisea", 12.50, 4],
    ["Hamlet", 9.95, 6],
    ["El Quijote", 15.90, 8],
]

print(fichas[1])        # ['Hamlet', 9.95, 6]  → la ficha completa
print(fichas[1][1])     # 9.95                 → primer índice: fila; segundo: columna
fichas[2][2] -= 1       # Luis compra un Quijote: el stock baja a 7

for titulo, precio, stock in fichas:   # desempaquetado en el for, como con zip()
    print(f"{titulo:<12} {precio:>6.2f} EUR  ({stock} uds.)")

Ya es un avance: cada libro viaja junto y un solo append(["Fausto", 21.00, 0]) da de alta todo. Pero ficha[1] y ficha[2] son índices mudos: nada dice que 1 sea el precio. La siguiente lección (tuplas) y sobre todo la 04-03 (diccionarios) pulirán esta idea.

Pertenencia: in y not in

Los operadores in / not in comprueban si un valor está en la lista, sin bucles a la vista:

catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]

if "Fausto" in catalogo:
    print("Lo tenemos fichado (aunque quizá sin stock).")

if "Drácula" not in catalogo:
    print("Habría que pedirlo a la distribuidora.")

Es una comparación exacta: "fausto" in catalogo da False por las mayúsculas. Por eso buscar_libro() de papyrus_utils.py compara en minúsculas; en 04-05 iremos aún más lejos normalizando texto.

Errores Comunes y Consejos

  • Guardar el resultado de sort(), append() o reverse(): lista = lista.sort() deja lista valiendo None. Estos métodos modifican en el sitio y devuelven None; no reasignes.
  • Confundir alias con copia: b = a no copia. Si luego mutas b y a "cambia solo", ese es el síntoma. Usa a.copy() o a[:].
  • remove() e index() con valores inexistentes lanzan ValueError. Protege con if x in lista: antes de llamar (la gestión elegante de errores llega en el módulo 7).
  • Modificar una lista mientras la recorres con for produce saltos de elementos. Recorre una copia (for x in lista[:]) o construye una lista nueva con una comprensión.
  • append(otra_lista) cuando querías extend(otra_lista): el primero mete la lista entera como un único elemento anidado.
  • Consejo: cuando dudes entre mutar o crear, pregúntate si alguien más usa esa lista. Si la recibiste como argumento, crear una nueva (sorted, slicing, comprensión) es casi siempre más seguro.

Ejercicios

  1. Alta y baja sincronizadas. Partiendo de las tres listas paralelas de Papyrus, escribe una función alta_libro(catalogo, precios, stocks, titulo, precio, stock) que añada el libro a las tres listas, y una baja_libro(catalogo, precios, stocks, titulo) que lo elimine de las tres usando index() y pop(). Prueba a dar de alta "Fausto" (21.00, 0) y de baja "Hamlet".
  2. Rebanadas del catálogo. Con catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"], obtén usando solo slicing: (a) los dos primeros títulos, (b) los dos últimos, (c) el catálogo invertido, (d) una copia independiente que puedas ordenar alfabéticamente sin tocar el original.
  3. Cola con prioridad de socio. Simula la cola de pedidos como lista de cadenas "nombre: título". Los pedidos de Luis (socio) deben colarse: escríbelos con insert(0, ...) en vez de append(). Atiende la cola con while y pop(0) mostrando el orden final.

Soluciones

# Ejercicio 1
def alta_libro(catalogo, precios, stocks, titulo, precio, stock):
    """Añade un libro a las tres listas paralelas (mutándolas)."""
    catalogo.append(titulo)
    precios.append(precio)
    stocks.append(stock)

def baja_libro(catalogo, precios, stocks, titulo):
    """Elimina un libro de las tres listas; devuelve su precio y stock."""
    if titulo not in catalogo:
        return None                    # centinela, como en 03-02
    i = catalogo.index(titulo)         # misma fila en las tres listas
    catalogo.pop(i)
    return (catalogo, precios.pop(i), stocks.pop(i))[1:]  # (precio, stock)

catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]
alta_libro(catalogo, precios, stocks, "Fausto", 21.00, 0)
print(baja_libro(catalogo, precios, stocks, "Hamlet"))   # (9.95, 6)
print(catalogo)   # ['La Odisea', 'El Quijote', 'Fausto']

Fíjate en que las funciones mutan las listas recibidas: quien llama ve los cambios porque comparte los objetos. Y en que cada operación toca tres estructuras — el argumento definitivo para 04-03.

# Ejercicio 2
catalogo = ["La Odisea", "Hamlet", "El Quijote", "Fausto"]
print(catalogo[:2])        # (a) ['La Odisea', 'Hamlet']
print(catalogo[-2:])       # (b) ['El Quijote', 'Fausto']
print(catalogo[::-1])      # (c) invertido
copia = catalogo[:]        # (d) copia independiente
copia.sort()
print(copia)               # ordenada
print(catalogo)            # el original, intacto
# Ejercicio 3
cola = []
def encolar(cola, cliente, titulo, socio=False):
    pedido = f"{cliente}: {titulo}"
    if socio:
        cola.insert(0, pedido)   # los socios se cuelan al principio
    else:
        cola.append(pedido)

encolar(cola, "Marta", "Hamlet")
encolar(cola, "Luis", "El Quijote", socio=True)
encolar(cola, "Pau", "La Odisea")

while cola:
    print("Atendiendo ->", cola.pop(0))
# Luis: El Quijote / Marta: Hamlet / Pau: La Odisea

Consejo: insert(0, ...) y pop(0) desplazan toda la lista; para Papyrus es irrelevante, pero recuérdalo cuando veas deque en 04-06.

Conclusión

Las listas son la navaja suiza de Python: sabes crearlas, indexarlas (también desde el final), rebanarlas con [inicio:fin:paso], y manejar sus métodos distinguiendo los que mutan en el sitio (append, sort, remove...) de las funciones que crean listas nuevas (sorted, slicing). Sobre todo, ahora entiendes la mutabilidad: b = a crea un alias, no una copia, y ese detalle explica comportamientos que hasta hoy parecían magia negra. También has visto sus límites en Papyrus: las listas paralelas exigen disciplina de sincronización y las listas anidadas usan índices mudos. El siguiente paso es su hermana inmutable, la tupla: la estructura perfecta para registros que no deben cambiar — y la que por fin salda la promesa del retorno múltiple de funciones que dejamos pendiente en 03-02.

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