Listas y tuplas en Python — teoría con las notas de una clase, slices y esquemas de recorrido (y la lista que no se dejaba subir la nota)
Tenía una lista con las notas de mi clase y quise regalarles medio punto a todos. Escribí un for que recorría la lista y le sumaba 0.5 a cada nota. Ejecuté el programa, no salió ningún error, imprimí la lista y esto fue lo que vi:
notas = [6.5, 4.0, 9.0, 7.5]
def subir_medio_punto(lista):
for nota in lista:
nota += 0.5
subir_medio_punto(notas)
print(notas)
[6.5, 4.0, 9.0, 7.5]
«¿Cómo que no ha cambiado ni una nota? Si he sumado… ¿dónde se han ido esos 0.5?» Estuve un buen rato revisando la suma, el +=, la función. Todo estaba bien. El fallo era otro: recorrer una lista no es lo mismo que modificarla, y la diferencia depende de cómo escribas el for.
Hoy ponemos orden en las secuencias: qué son las listas, las tuplas y los strings, cómo se accede a sus elementos y a trozos de ellas (los slices), cómo se recorren y cuáles son los esquemas que vas a usar una y otra vez (recorrido, selección, búsqueda, máximo, acumulador y contador). Todo con un único ejemplo: las notas de una clase. Si vienes de bucles en Python y de módulos y paquetes, aquí empiezas a sacarle partido de verdad a los for. Y cuando termines, la práctica de listas y tuplas te espera con tres programas completos.
Tabla de Contenidos
Qué es una secuencia
Una secuencia es una colección de elementos puestos en fila: hay un primero, y cada uno (menos el último) tiene un siguiente. Los meses del año son una secuencia: enero va primero, después febrero, y así hasta diciembre. Como están en fila, cada elemento tiene un número de orden (su posición).
Python tiene tres tipos de secuencia que usarás constantemente:
| Tipo | Se escribe con | Ejemplo | ¿Se puede modificar? |
|---|---|---|---|
str (string) | comillas | "PYTHON" | No (inmutable) |
tuple (tupla) | paréntesis | ("Lucía", 2024) | No (inmutable) |
list (lista) | corchetes | [7.5, 4.0, 9.0] | Sí (mutable) |
Los tres son contenedores secuenciales: guardan varios elementos en orden y comparten casi todas las operaciones que vamos a ver. La gran diferencia es que la lista admite cambios y los otros dos no. En este artículo nos centramos en acceder, recorrer y calcular; cómo modificar una lista en profundidad lo dejamos para el siguiente.
Crear listas y tuplas
notas = [7.5, 4.0, 9.0, 6.0, 8.5] # lista: entre corchetes
alumno = ("Lucía", 2024, "X928476") # tupla: entre paréntesis
palabra = "PYTHON" # string: entre comillas
vacia = []
un_solo = (10,) # la coma es la que crea la tupla
print(type(notas), type(alumno), type(palabra))
print(len(notas), len(alumno), len(palabra))
print(type((10)), type((10,)))
print(vacia, un_solo)
<class 'list'> <class 'tuple'> <class 'str'> 5 3 6 <class 'int'> <class 'tuple'> [] (10,)
Entre líneas:
- Una lista va entre corchetes y puede mezclar tipos; una tupla va entre paréntesis. Se suele usar la lista para una colección de cosas del mismo tipo (las notas) y la tupla para agrupar datos distintos de una sola cosa (un alumno: nombre, año de ingreso y código).
len()devuelve cuántos elementos tiene cualquier secuencia. En el string cuenta caracteres.- La línea que más se olvida es la de
un_solo:(10,)es una tupla con un elemento, y(10)es simplemente el número 10 entre paréntesis. Lo que crea la tupla es la coma, no los paréntesis.
Escribir ("Python") pensando que es una tupla de un elemento. Es un string con paréntesis alrededor. Para que sea una tupla necesita la coma: ("Python",).
Y como las tuplas son inmutables, no puedes cambiar un elemento una vez creada:
Traceback (most recent call last):
File "error_tupla.py", line 2, in <module>
alumno[0] = "Ana"
~~~~~~^^^
TypeError: 'tuple' object does not support item assignment
Acceder a un elemento: los índices
Cada elemento se alcanza con su índice, entre corchetes. En Python se cuenta desde cero: la primera posición es la 0. Y hay un truco muy cómodo: los índices negativos cuentan desde el final, donde -1 es el último.
| Posición | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Elemento | 10 | 20 | 30 | 40 | 50 |
| Índice negativo | -5 | -4 | -3 | -2 | -1 |
notas = [10, 20, 30, 40, 50] print(notas[0], notas[4]) # primero y último, contando desde el principio print(notas[-1], notas[-5]) # último y primero, contando desde el final print(notas[len(notas) - 1]) # equivale a notas[-1]
10 50 50 10 50
El último elemento de una secuencia de n elementos es el n - 1 (o, más cómodo, el -1). Si te pasas del final, Python protesta:
Traceback (most recent call last):
File "error_indice.py", line 2, in <module>
print(notas[5])
~~~~~^^^
IndexError: list index out of range
Usar notas[len(notas)] para el último elemento. Una lista de 5 elementos llega hasta el índice 4; el 5 ya no existe y salta un IndexError.
Slices: trozos de una secuencia
Un slice es una copia de un trozo de la secuencia. Se escribe secuencia[inicio:fin:salto], y cualquiera de las tres partes se puede omitir. Lo importante, y lo que más se confunde: el inicio se incluye y el fin no.
notas = [10, 20, 30, 40, 50]
print("notas[1:3] ", notas[1:3])
print("notas[-3:-1]", notas[-3:-1])
print("notas[:3] ", notas[:3])
print("notas[3:] ", notas[3:])
print("notas[:-1] ", notas[:-1])
print("notas[1:-1] ", notas[1:-1])
print("notas[::2] ", notas[::2])
print("notas[::-1] ", notas[::-1])
print("notas[::-2] ", notas[::-2])
print("notas[:] ", notas[:])
notas[1:3] [20, 30] notas[-3:-1] [30, 40] notas[:3] [10, 20, 30] notas[3:] [40, 50] notas[:-1] [10, 20, 30, 40] notas[1:-1] [20, 30, 40] notas[::2] [10, 30, 50] notas[::-1] [50, 40, 30, 20, 10] notas[::-2] [50, 30, 10] notas[:] [10, 20, 30, 40, 50]
Cómo leerlos:
notas[1:3]empieza en el índice 1 y llega hasta justo antes del 3: da las posiciones 1 y 2.- Si omites el inicio, empieza por el principio (
notas[:3]); si omites el fin, llega hasta el final (notas[3:]). - Los negativos funcionan igual que en un índice suelto:
notas[:-1]es «todo menos el último». - El tercer número es el salto.
[::2]salta de dos en dos, y un salto negativo recorre al revés:[::-1]es la secuencia entera dada la vuelta. notas[:]copia la lista entera.
Con strings y tuplas funciona exactamente igual:
palabra = "abcdefghij" print(palabra[2:5]) print(palabra[:3]) print(palabra[-3:]) print(palabra[::2]) print(palabra[::-1])
cde abc hij acegi jihgfedcba
Para no equivocarte con el fin, piensa que secuencia[a:b] tiene b - a elementos. Así notas[1:3] tiene 2.
Querer «las dos últimas» y escribir notas[-2:-1]. Como el fin no se incluye, eso te da solo uno. Para llegar hasta el final se deja el fin vacío: notas[-2:].
Unir, repetir y convertir secuencias
Con + unes dos secuencias del mismo tipo y con * repites una secuencia un número de veces:
primera = [10, 20]
segunda = [30, 40]
print(primera + segunda) # concatenar: junta dos secuencias del mismo tipo
print((1, 2) + (3,))
print("abc" + "def")
print([0] * 5) # repetir
print((1,) * 3)
print("ab" * 3)
print([1, 2, 3] * 2)
[10, 20, 30, 40] (1, 2, 3) abcdef [0, 0, 0, 0, 0] (1, 1, 1) ababab [1, 2, 3, 1, 2, 3]
[0] * 5 es la forma rápida de crear una lista de cinco ceros. Pero no puedes mezclar tipos con +:
Traceback (most recent call last):
File "error_mezcla.py", line 1, in <module>
print([1, 2] + (3, 4))
~~~~~~~^~~~~~~~
TypeError: can only concatenate list (not "tuple") to list
Para pasar de un tipo a otro se usa el nombre del tipo como función:
cifras = "123" print(tuple(cifras)) print(list(cifras)) print(list((1, 2, 3))) print(str((1, 2, 3))) print(len(str((1, 2, 3))))
('1', '2', '3')
['1', '2', '3']
[1, 2, 3]
(1, 2, 3)
9
Fíjate en los dos últimos casos: al convertir un string en tupla o lista, cada carácter se vuelve un elemento. Y al convertir una tupla en string no obtienes los elementos unidos, sino la tupla escrita como texto, con sus paréntesis, comas y espacios (por eso mide 9 caracteres).
Empaquetar y desempaquetar
Meter varios valores en una tupla es empaquetar. Sacarlos a variables sueltas es desempaquetar:
alumno = ("Lucía", 2024, "X928476") # empaquetar: varios valores en una tupla
nombre, curso, codigo = alumno # desempaquetar: una variable por elemento
print(nombre, curso, codigo)
par = nombre, curso # los paréntesis son opcionales
print(par)
a, b = 1, 2
a, b = b, a # intercambiar dos variables
print(a, b)
Lucía 2024 X928476
('Lucía', 2024)
2 1
El intercambio a, b = b, a es una de las ventajas más agradables de Python: en otros lenguajes necesitarías una variable auxiliar. Hay una regla: tiene que haber tantas variables a la izquierda como elementos a la derecha.
Traceback (most recent call last):
File "error_desempaquetar.py", line 2, in <module>
nombre, curso = alumno
^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)
Desempaqueta siempre que recorras una lista de tuplas. for nombre, nota in alumnos: se lee muchísimo mejor que for alumno in alumnos: seguido de alumno[0] y alumno[1].
Recorrer una secuencia
Cuando quieres hacer lo mismo con cada elemento, usas el esquema de recorrido:
para cada item en la secuencia: hacer algo con item
En Python, eso es un for. Y funciona con cualquier secuencia:
def mostrar(secuencia):
for item in secuencia:
print(item, end=", ")
print()
mostrar([10, 12, 23, 35])
mostrar((1, 2, 3))
mostrar("abc")
10, 12, 23, 35, 1, 2, 3, a, b, c,
(end=", " hace que print termine con una coma y un espacio en lugar de un salto de línea, y el print() final cierra la línea.)
Recorrer un rango
A veces no tienes una secuencia guardada, sino una serie de números consecutivos. Para eso está range(), que admite hasta tres parámetros: range(inicio, fin, salto). Igual que en los slices, el fin no se incluye:
print(list(range(5))) print(list(range(2, 7))) print(list(range(0, 10, 2))) print(list(range(5, 0, -1))) print(list(range(5, 0)))
[0, 1, 2, 3, 4] [2, 3, 4, 5, 6] [0, 2, 4, 6, 8] [5, 4, 3, 2, 1] []
La última línea muestra que, si el inicio y el fin están «cruzados» y no pones un salto negativo, el rango queda vacío y el bucle no ejecuta ni una vuelta.
Por elemento o por índice: el misterio del principio
Ahora tenemos todo para resolver mi problema. Hay dos formas de recorrer una lista, y no hacen lo mismo:
def subir_mal(lista, puntos):
for nota in lista:
nota += puntos
def subir_bien(lista, puntos):
for pos in range(len(lista)):
lista[pos] += puntos
notas = [6.5, 4.0, 9.0, 7.5]
subir_mal(notas, 0.5)
print("Con for por elemento:", notas)
subir_bien(notas, 0.5)
print("Con for por índice: ", notas)
Con for por elemento: [6.5, 4.0, 9.0, 7.5] Con for por índice: [7.0, 4.5, 9.5, 8.0]
Por qué una funciona y la otra no:
- En
for nota in lista, la variablenotaes una copia de la referencia a cada número. Cuando hacesnota += puntos, Python crea un número nuevo y hace quenotaapunte a él. La lista ni se entera. - En
for pos in range(len(lista))recorres los índices, ylista[pos] += puntossí cambia lo que hay guardado en esa posición de la lista.
Modificar la variable del for esperando que cambie la lista. Si necesitas cambiar los elementos, recorre por índice. Si solo necesitas leerlos, recorre por elemento: es más corto y más claro.
Recorrer hasta una condición
El for está pensado para secuencias de las que sabes dónde terminan. Si el final solo se descubre al encontrarlo (una marca especial), usa while. De hecho, todo lo que hace un for se puede escribir con un while:
def mostrar_for(secuencia):
for item in secuencia:
print(item, end=", ")
print()
def mostrar_while(secuencia):
pos = 0
while pos < len(secuencia):
print(secuencia[pos], end=", ")
pos += 1
print()
mostrar_for([10, 12, 23])
mostrar_while([10, 12, 23])
10, 12, 23, 10, 12, 23,
datos = [7, 5, 9, -1, 8] # el -1 marca el final: lo que viene después no cuenta
def sumar_hasta_marca(secuencia):
pos = 0
total = 0
while secuencia[pos] >= 0:
total += secuencia[pos]
pos += 1
return total
print(sumar_hasta_marca(datos))
21
En la segunda función el -1 actúa como marca de final: se suma todo lo que hay antes, y el 8 que viene después ni se mira. El esquema general es: preparar el primer elemento, y mientras no se llegue al final, tratarlo y avanzar al siguiente.
Recorrido con tratamiento selectivo
Muchas veces no quieres tratar todos los elementos, sino solo los que cumplen una condición. El esquema es el mismo recorrido, con una pregunta antes de tratar cada elemento:
para cada item en la secuencia: si item cumple la condición: hacer algo con item
def mostrar_aprobadas(notas):
for nota in notas:
if nota >= 5:
print(nota, end=", ")
print()
mostrar_aprobadas([7.5, 4.0, 9.0, 3.5, 6.0, 8.5, 5.0])
7.5, 9.0, 6.0, 8.5, 5.0,
Este esquema es la base de los demás: casi todo lo que viene ahora es un recorrido (a veces selectivo) con una variable auxiliar que va recordando algo.
Búsqueda: ¿hay alguno que cumpla…?
En una búsqueda quieres saber si hay algún elemento que cumpla una condición. Es un recorrido selectivo con una diferencia importante: en cuanto lo encuentras, paras (con un return) y no hace falta seguir hasta el final.
def hay_suspenso(notas):
for nota in notas:
if nota < 5:
return True # lo encontré: no hace falta seguir
return False # recorrí todo y no apareció
print(hay_suspenso([7.5, 4.0, 9.0]))
print(hay_suspenso([7.5, 6.0, 9.0]))
True False
Entre líneas:
- El primer
return Trueestá dentro delif: sale de la función en cuanto aparece el primer suspenso. - El
return Falseestá fuera del bucle, después de él: solo se llega ahí si elforterminó sin encontrar nada.
Y aquí viene el error clásico:
Poner el return False en un else dentro del bucle. Entonces la función decide con el primer elemento y nunca mira el resto.
def hay_suspenso(notas):
for nota in notas:
if nota < 5:
return True
else:
return False
print(hay_suspenso([7.5, 4.0, 9.0]))
False
La lista tenía un 4.0 en la segunda posición y la función dice False. Sin error, sin aviso: simplemente miró el 7.5, vio que no era suspenso, y el else hizo return False en la primera vuelta.
Devolver el valor o la posición
Si lo que te interesa no es «sí o no», sino cuál es el elemento o dónde está, cambia lo que devuelves. Para la posición hay una función muy útil, enumerate(), que va dando parejas (índice, elemento):
def primer_suspenso(notas):
for nota in notas:
if nota < 5:
return nota
return None
def posicion_suspenso(notas):
for pos, nota in enumerate(notas):
if nota < 5:
return pos
return -1
notas = [7.5, 6.0, 4.0, 9.0, 3.5]
print(primer_suspenso(notas))
print(posicion_suspenso(notas))
print(posicion_suspenso([7.5, 6.0]))
4.0 2 -1
Devuelvo -1 cuando no hay ninguno porque nunca puede ser una posición válida. Evita devolver False para «no encontrado»: en Python False == 0, y el 0 sí es una posición.
Operador in y funciones ya hechas
Python trae las búsquedas más habituales ya escritas:
lenguajes = ["C", "Java", "Python"]
print("Python" in lenguajes)
print("Pascal" in lenguajes)
print(lenguajes.index("Java"))
texto = "abracadabra"
print("cad" in texto)
print(texto.find("cad"))
print(texto.find("xyz"))
True False 1 True 4 -1
valor in secuenciarespondeTrueoFalse. Con strings busca también trozos de texto, no solo caracteres sueltos..index()te da la posición de la primera aparición;.find()(solo en strings) hace lo mismo pero devuelve-1si no está.
Por dentro, todas esas operaciones hacen un recorrido como el que acabamos de escribir, así que cuestan lo mismo. Y ojo, que .index() protesta si el valor no está:
Traceback (most recent call last):
File "error_index.py", line 2, in <module>
print(lenguajes.index("Pascal"))
^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: 'Pascal' is not in list
Antes de usar .index(), comprueba con in que el valor está en la lista.
Máximo y mínimo
Aquí sí hay que recorrerlo todo, porque no sabes cuál es el mayor hasta haberlos visto todos. Se parte de un candidato (el primer elemento) y, cada vez que aparece uno mejor, se cambia el candidato:
def maximo(notas):
candidato = notas[0]
for nota in notas:
if nota > candidato:
candidato = nota
return candidato
def minimo(notas):
candidato = notas[0]
for nota in notas:
if nota < candidato:
candidato = nota
return candidato
notas = [7.5, 4.0, 9.0, 3.5, 6.0]
print(maximo(notas))
print(minimo(notas))
print(max(notas), min(notas))
9.0 3.5 9.0 3.5
Para el mínimo solo cambia el sentido de la comparación. Y ya existen max() y min(), pero conviene saber construirlos: te servirán cuando el criterio no sea el valor directo.
Inicializar el candidato con un valor inventado, como 0. Si todas las notas son mayores que ese 0, el «mínimo» será siempre 0, aunque nadie lo haya sacado:
0
Por eso se empieza con el primer elemento: es un valor que sí está en la secuencia. Eso tiene una consecuencia: con una lista vacía no hay primer elemento.
Traceback (most recent call last):
File "error_vacia.py", line 9, in <module>
print(maximo([]))
^^^^^^^^^^
File "error_vacia.py", line 2, in maximo
candidato = notas[0]
~~~~~^^^
IndexError: list index out of range
Acumulador y contador
El esquema acumulador va construyendo un resultado a partir de todos los elementos. Se empieza con el elemento neutro de la operación (el 0 para sumar, el 1 para multiplicar, "" para unir strings, [] para unir listas) y se va acumulando:
inicializar el acumulador con el elemento neutro; para cada item: acumular item
Si le añades la pregunta del recorrido selectivo, tienes el acumulador selectivo. Y si en vez de sumar el valor de cada elemento sumas siempre 1, tienes el contador:
def suma(notas):
acumulador = 0
for nota in notas:
acumulador += nota
return acumulador
def suma_aprobadas(notas):
acumulador = 0
for nota in notas:
if nota >= 5:
acumulador += nota
return acumulador
def contar_aprobadas(notas):
contador = 0
for nota in notas:
if nota >= 5:
contador += 1
return contador
notas = [7.5, 4.0, 9.0, 3.5, 6.0, 8.5, 5.0]
print("Suma:", suma(notas))
print("Suma de aprobadas:", suma_aprobadas(notas))
print("Aprobadas:", contar_aprobadas(notas))
print("Media:", round(suma(notas) / len(notas), 2))
print(sum(notas), len(notas), max(notas), min(notas))
Suma: 43.5 Suma de aprobadas: 36.0 Aprobadas: 5 Media: 6.21 43.5 7 9.0 3.5
La diferencia entre un acumulador y un contador es solo esta: el acumulador suma algo que depende de cada elemento (nota), y el contador suma siempre lo mismo (1). La última línea muestra los atajos de Python (sum, len, max, min), que aplican estos mismos esquemas por dentro.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta tu programa línea a línea y dibuja las variables y los objetos en memoria. Elige Python 3 y pega este programa, que recoge el misterio del principio con solo dos notas para que se vea bien:
def subir_mal(lista, puntos):
for nota in lista:
nota += puntos
def subir_bien(lista, puntos):
for pos in range(len(lista)):
lista[pos] += puntos
notas = [6.5, 4.0]
subir_mal(notas, 0.5)
print("Por elemento:", notas)
subir_bien(notas, 0.5)
print("Por índice:", notas)
Salida esperada:
Por elemento: [6.5, 4.0] Por índice: [7.0, 4.5]
Qué observar, paso a paso:
- Antes de llamar a nada, la variable global
notasapunta a una caja de lista con dos números (6.5y4.0), y cada posición de la lista tiene su flecha al número. - Al entrar en
subir_mal, aparece su marco conlistaapuntando a la misma caja quenotas. En la primera vuelta delfor, la variablenotaapunta al6.5. - Con
nota += puntos,notasalta a un número nuevo (7.0). Mira la lista: las flechas de sus posiciones no se han movido. Por eso, al salir, la lista sigue igual. - En
subir_bien, la clave eslista[pos] += puntos: aquí la flecha de la posición de la lista sí salta al número nuevo. Al terminar,notasya tiene7.0y4.5. - Fíjate en que las variables
notasylistacomparten la misma caja: es lo que permite que una función modifique la lista de quien la llama. De eso hablaremos en el siguiente artículo.
Resumen
| Concepto | Lo esencial |
|---|---|
| Secuencias | str, tuple y list: elementos en orden, accesibles por posición |
| Lista / tupla | [ ] se puede modificar; ( ) no. (10,) es la tupla de un elemento |
| Índices | Desde 0; los negativos cuentan desde el final (-1 es el último) |
| Slice | s[inicio:fin:salto]: el inicio se incluye, el fin no; [::-1] invierte |
+ y * | Unir secuencias del mismo tipo y repetirlas |
| Desempaquetar | a, b = tupla: tantas variables como elementos |
for por elemento | Para leer. No modifica la lista |
for por índice | range(len(s)) y s[pos]: para modificar o para saber la posición |
| Recorrido selectivo | Un recorrido con un if antes de tratar el elemento |
| Búsqueda | Recorrido que para con return al encontrar; el return False va fuera del bucle |
| Máximo / mínimo | Candidato inicial = primer elemento |
| Acumulador / contador | Se parte del elemento neutro; el contador suma siempre 1 |
Tres ideas para llevarte:
- Los índices empiezan en 0 y el fin de un slice no se incluye. Con eso y los negativos, accedes a cualquier trozo.
- Si quieres cambiar una lista, recorre por índice. La variable de un
forpor elemento es independiente de la lista. - Todos los esquemas son el mismo recorrido con pequeños añadidos: un
if(selectivo), unreturn(búsqueda), un candidato (máximo) o una variable que va acumulando (acumulador y contador).
En la práctica de listas y tuplas montarás estadísticas de una clase, un buscador de alumnos y unas medias móviles. Después, los ejercicios te dirán si ha quedado claro. Y en el siguiente tema entramos en lo que hace especial a las listas: mutabilidad, copias, list comprehension y matrices.
Siguiente paso: prácticaListas y tuplas en Python — práctica con estadísticas de clase, un buscador de alumnos y medias móviles (y la nota mínima que era un cero que nadie había sacado)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Un comentario