# Listas, tuplas y diccionarios

URL canónica: https://missyera.com/guias/python-desde-cero/listas-y-diccionarios/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> Una lista guarda varios valores en orden y se puede cambiar. Una tupla es igual pero no se toca. Un diccionario guarda pares de clave y valor, que es como llega casi cualquier dato de una API. Y el error que yo cometí y que comete todo el mundo es creer que asignar una lista a otra variable la copia. No la copia. Las dos etiquetas apuntan a la misma lista.

Capítulo 4 de 17 del libro [Python desde cero](https://missyera.com/guias/python-desde-cero.md).

## Hola! Ahora guardamos muchas cosas juntas

En el capítulo 3 cada variable guardaba un solo valor. Eso alcanza para aprender y no alcanza para trabajar 🙂

En un archivo real tienes tres mil ventas, no una. Así que necesitas estructuras que guarden muchas cosas, y en Python son tres las que vas a usar todo el tiempo.

Antes de la tabla, piensa en el último archivo que abriste: **¿qué guardaba cada fila, una cosa sola o varias con nombre?** Según cómo contestes eso, la estructura que necesitas ya está elegida 🙂

| Estructura | Se escribe | Se puede cambiar | Para qué |
|---|---|---|---|
| **Lista** | `[1, 2, 3]` | Sí | Lo que sea que tenga orden y vaya cambiando |
| **Tupla** | `(1, 2, 3)` | No | Cosas que no deben cambiar: coordenadas, una fila fija |
| **Diccionario** | `{'ciudad': 'Lima'}` | Sí | Datos con nombre. Es como llega cualquier API |

![Árbol para elegir estructura en Python: si cada dato tiene nombre, diccionario de clave y valor, que es como llega cualquier API. Si van en orden, lista cuando va a cambiar después y tupla cuando no.](https://missyera.com/static/diagramas/webp/py-03-estructuras.webp)

*Dos preguntas y la estructura queda decidida. La segunda es la que casi nadie se hace, y es la que evita que algo que no debía cambiar cambie en medio del proceso.*

## Listas: lo que más vas a usar

```
ciudades = ['Lima', 'Arequipa', 'Cusco', 'Piura', 'Trujillo']

print(ciudades[0])
print(ciudades[2])
print(len(ciudades))
```

```text
Lima
Cusco
5
```

Se cuenta desde cero. Ya lo vimos en el capítulo 2 con el `IndexError`, y te lo repito porque es la fuente número uno de errores por uno de diferencia.

Ahora el atajo que te evita hacer cuentas: **los índices negativos**.

```
print(ciudades[-1])     # la ultima, siempre
print(ciudades[-2])     # la penultima
```

```text
Trujillo
Piura
```

Nunca más `ciudades[len(ciudades) - 1]`. Yo escribí esa monstruosidad durante meses antes de que alguien me enseñara el `-1` 😅

### Rebanadas, que es como se corta una lista

Los dos puntos dentro de los corchetes sacan un pedazo:

```
print(ciudades[0:3])     # de la 0 hasta ANTES de la 3
print(ciudades[:3])      # lo mismo, el 0 se sobreentiende
print(ciudades[2:])      # de la 2 hasta el final
print(ciudades[-2:])     # las dos ultimas
print(ciudades[::2])     # de dos en dos
```

```text
['Lima', 'Arequipa', 'Cusco']
['Lima', 'Arequipa', 'Cusco']
['Cusco', 'Piura', 'Trujillo']
['Piura', 'Trujillo']
['Lima', 'Cusco', 'Trujillo']
```

**El límite de la derecha no se incluye.** Eso confunde al principio y tiene una gracia: `ciudades[0:3]` te da exactamente 3 elementos, y `ciudades[:3] + ciudades[3:]` te devuelve la lista entera sin repetir ni perder nada.

Y una que parece un truco de fiesta y sirve de verdad:

```
print(ciudades[::-1])
```

```text
['Trujillo', 'Piura', 'Cusco', 'Arequipa', 'Lima']
```

### Modificar una lista

```
ventas = [480.37, 524.90, 154.83]

ventas.append(2480.84)          # agrega al final
ventas.insert(0, 100.0)         # inserta en una posicion
print(ventas)

ultimo = ventas.pop()           # saca el ultimo Y te lo devuelve
print(ultimo, ventas)

ventas.remove(154.83)           # quita por valor, no por posicion
print(ventas)
```

```text
[100.0, 480.37, 524.9, 154.83, 2480.84]
2480.84 [100.0, 480.37, 524.9, 154.83]
[100.0, 480.37, 524.9]
```

Ojo con `remove`: quita **la primera aparición** del valor, no todas. Y si el valor no está, revienta.

```
ventas.remove(999)
```

```
ValueError: list.remove(x): x not in list
```

### Ordenar, y el error que da la primera vez

Hay dos formas de ordenar y confundirlas te deja con una variable vacía.

```
montos = [480.37, 154.83, 2480.84, 524.90]

ordenados = sorted(montos)          # devuelve una lista nueva
print(ordenados)
print(montos)                       # la original no se toco
```

```text
[154.83, 480.37, 524.9, 2480.84]
[480.37, 154.83, 2480.84, 524.9]
```

```
montos.sort()                       # ordena la original y devuelve None
print(montos)
```

```text
[154.83, 480.37, 524.9, 2480.84]
```

Y aquí el error clásico, que quiero que veas de frente:

```
resultado = montos.sort()
print(resultado)
```

```text
None
```

Sí: `None`. Porque `.sort()` ordena la lista en el sitio y no devuelve nada. Si guardas su resultado, guardas la nada 🙃

La regla para acordarte: **`sorted()` te da una lista nueva, `.sort()` modifica la que ya tienes**. Cuando dudes, usa `sorted()`, que es más difícil de romper.

```
print(sorted(montos, reverse=True))
print(sorted(['Cusco', 'arequipa', 'Lima'], key=str.lower))
```

```text
[2480.84, 524.9, 480.37, 154.83]
['arequipa', 'Cusco', 'Lima']
```

Ese `key=str.lower` es lo que evita que las mayúsculas se ordenen antes que todas las minúsculas, que es lo que pasa por defecto.

## La copia que no se copió

Presta atención a esto, que es el error más traicionero del principio y no falla: te da un resultado equivocado sin decir nada.

```
original = [1, 2, 3]
copia = original          # esto NO es una copia

copia.append(4)
print('copia   :', copia)
print('original:', original)
```

```text
copia   : [1, 2, 3, 4]
original: [1, 2, 3, 4]
```

Se modificaron las dos 😱

Acuérdate del capítulo 3: una variable es una etiqueta, no una caja. Con `copia = original` le pegaste una segunda etiqueta a la **misma** lista. No hay dos listas, hay una con dos nombres.

Se comprueba así:

```
print(copia is original)
```

```text
True
```

Para copiar de verdad hay tres formas, y las tres valen:

```
original = [1, 2, 3]

copia_a = original.copy()
copia_b = list(original)
copia_c = original[:]        # una rebanada completa

copia_a.append(99)
print(original, copia_a)
print(copia_a is original)
```

```text
[1, 2, 3] [1, 2, 3, 99]
False
```

Con esto te ahorro una tarde de "pero si yo no toqué esa variable". A mí me pasó con un dataframe y perdí medio día buscando el fantasma 👻

## Tuplas: las que no se pueden cambiar

Una tupla es una lista que no se puede modificar. Se escribe con paréntesis.

```
coordenada = (-12.0464, -77.0428)      # Lima
print(coordenada[0])

coordenada[0] = 0
```

```
TypeError: 'tuple' object does not support item assignment
```

¿Y para qué querría algo que no puedo cambiar? Por tres razones muy concretas:

- 🔒 Dice "esto no se toca". Un par de coordenadas, un rango de fechas fijo.
- 🗝️ Puede ser clave de un diccionario. Una lista no puede.
- 📦 Es lo que devuelve una función cuando devuelve varias cosas.

Ese último punto lo vas a ver todo el tiempo:

```
def minimo_y_maximo(numeros):
    return min(numeros), max(numeros)

bajo, alto = minimo_y_maximo([480.37, 154.83, 2480.84])
print(bajo, alto)
```

```text
154.83 2480.84
```

Eso de repartir una tupla en varias variables se llama desempaquetado y es puro Python. Lo vamos a usar mucho en el capítulo 6.

## Diccionarios: datos con nombre

Aquí está la estructura que más te va a servir en datos, porque es como llega la información del mundo real.

```
venta = {
    'cliente': 'C0045',
    'ciudad': 'Trujillo',
    'canal': 'Marketplace',
    'monto': 480.37,
    'unidades': 10,
}

print(venta['ciudad'])
print(venta['monto'] * 1.18)
```

```text
Trujillo
566.8366
```

Ya no dependes de recordar que la ciudad estaba en la posición 1. Pides por nombre, que es como piensa una persona.

### get, que evita que reviente

```
print(venta['descuento'])
```

```
KeyError: 'descuento'
```

Con corchetes, si la clave no existe, se cae. Con `.get()` te devuelve `None`, o lo que tú digas:

```
print(venta.get('descuento'))
print(venta.get('descuento', 0))
```

```text
None
0
```

Ese segundo caso es oro puro cuando procesas datos que a veces traen un campo y a veces no, que es siempre 🌸

### Recorrer un diccionario

```
for clave in venta:
    print(clave)
```

```text
cliente
ciudad
canal
monto
unidades
```

```
for clave, valor in venta.items():
    print(f'{clave:10} {valor}')
```

```text
cliente    C0045
ciudad     Trujillo
canal      Marketplace
monto      480.37
unidades   10
```

`.items()` es el que vas a usar el 90% de las veces. Los otros dos son `.keys()` y `.values()`, y se llaman igual de fácil.

## Lo que de verdad te va a llegar: una lista de diccionarios

Junta las dos estructuras y tienes la forma en que viene cualquier respuesta de una API, cualquier JSON, cualquier exportación de un sistema.

```
ventas = [
    {'ciudad': 'Lima', 'canal': 'Web', 'monto': 480.37},
    {'ciudad': 'Cusco', 'canal': 'Tienda', 'monto': 154.83},
    {'ciudad': 'Lima', 'canal': 'WhatsApp', 'monto': 2480.84},
    {'ciudad': 'Piura', 'canal': 'Web', 'monto': 524.90},
]

total = 0
for v in ventas:
    total = total + v['monto']

print(f'Total: S/{total:,.2f}')
print(f'Ticket promedio: S/{total / len(ventas):,.2f}')
```

```text
Total: S/3,640.94
Ticket promedio: S/910.24
```

Y ahora agrupemos por ciudad, que es la operación que más veces vas a hacer en tu vida con datos:

```
por_ciudad = {}

for v in ventas:
    ciudad = v['ciudad']
    por_ciudad[ciudad] = por_ciudad.get(ciudad, 0) + v['monto']

print(por_ciudad)
```

```text
{'Lima': 2961.21, 'Cusco': 154.83, 'Piura': 524.9}
```

Guárdate ese patrón, que es un clásico: `diccionario.get(clave, 0) + algo`. La primera vez que aparece una ciudad, `.get` devuelve 0 y arranca la suma; las siguientes, sigue sumando.

En el capítulo 12 esto mismo se escribe así: `df.groupby('ciudad') ['monto'].sum()`. Una línea. Pero quería que vieras primero lo que hay por debajo, porque el día que `groupby` haga algo raro vas a saber qué está pasando 💜

## Comprensiones de lista

Esta es la forma corta de crear una lista a partir de otra, y es tan de Python que se nota cuando alguien no la usa.

```
montos = [480.37, 154.83, 2480.84, 524.90]

# La forma larga
con_igv = []
for m in montos:
    con_igv.append(round(m * 1.18, 2))

# La forma de Python
con_igv_corto = [round(m * 1.18, 2) for m in montos]

print(con_igv)
print(con_igv == con_igv_corto)
```

```text
[566.84, 182.7, 2927.39, 619.38]
True
```

Se lee de izquierda a derecha casi como una frase: "redondea eme por 1,18, para cada eme en montos".

Y se le puede poner una condición al final:

```
grandes = [m for m in montos if m > 500]
print(grandes)

ciudades = ['Lima', 'cusco', 'AREQUIPA']
print([c.title() for c in ciudades])
```

```text
[2480.84, 524.9]
['Lima', 'Cusco', 'Arequipa']
```

Un consejo de alguien que se pasó de lista: **si tu comprensión no cabe en dos líneas, escríbela como bucle normal**. La gracia es que se lea mejor, no que sea más corta.

## Conjuntos: para preguntar quién está y quién no

Hay una cuarta estructura que casi nunca sale en los tutoriales y que resuelve en una línea preguntas que con listas ocupan diez 🎯

Un conjunto es una bolsa **sin repetidos y sin orden**. Se escribe con llaves, como el diccionario, pero sin dos puntos.

```
enero = ['C0045', 'C0325', 'C0260', 'C0111', 'C0045']
febrero = ['C0325', 'C0777', 'C0045', 'C0888']

clientes_enero = set(enero)
clientes_febrero = set(febrero)

print('compras de enero      :', len(enero))
print('clientes distintos    :', len(clientes_enero))
print('compraron los dos meses:', sorted(clientes_enero & clientes_febrero))
print('solo enero, se fueron  :', sorted(clientes_enero - clientes_febrero))
print('solo febrero, nuevos   :', sorted(clientes_febrero - clientes_enero))
print('distintos en total     :', len(clientes_enero | clientes_febrero))
```

```text
compras de enero      : 5
clientes distintos    : 4
compraron los dos meses: ['C0045', 'C0325']
solo enero, se fueron  : ['C0111', 'C0260']
solo febrero, nuevos   : ['C0777', 'C0888']
distintos en total     : 6
```

Eso es un análisis de retención entero en seis líneas 💜

- 🤝 `&` es **los que están en los dos**: se quedaron.
- ➖ `-` es **los que están en el primero y no en el segundo**: se fueron. Ojo al orden, que aquí sí importa.
- ➕ `|` es **todos, sin repetir**.

Y la otra cosa para la que sirven: preguntar si algo está dentro. En una lista larga, Python va mirando uno por uno; en un conjunto va directo, sin recorrer nada.

```
print('C0045 compro en enero:', 'C0045' in clientes_enero)
print('C0999 compro en enero:', 'C0999' in clientes_enero)
```

```text
C0045 compro en enero: True
C0999 compro en enero: False
```

Con cinco clientes da igual. Con doscientos mil, la lista tarda y el conjunto no. Si vas a preguntar `in` muchas veces sobre lo mismo, conviértelo a conjunto una vez y pregunta ahí 🏃‍♀️

Lo que un conjunto no tiene es orden, y eso es literal:

```
clientes_enero[0]
```

```
TypeError: 'set' object is not subscriptable
```

No hay primero. No hay posición. Si necesitas verlos ordenados, pásalo por `sorted()` como hice arriba, y si necesitas conservar el orden en que llegaron, entonces lo que querías era una lista 📋

## Contar y agrupar sin escribir el bucle a mano

El ejercicio de contar por categoría lo hiciste con un diccionario y un `get`. Funciona perfecto y hay dos herramientas que lo dejan en una línea, las dos en el módulo `collections` 🧰

```
from collections import Counter, defaultdict

movimientos = [
    {'ciudad': 'Lima', 'categoria': 'Abarrotes', 'monto': 480.37},
    {'ciudad': 'Arequipa', 'categoria': 'Bebidas', 'monto': 154.83},
    {'ciudad': 'Lima', 'categoria': 'Abarrotes', 'monto': 2480.84},
    {'ciudad': 'Cusco', 'categoria': 'Limpieza', 'monto': 524.90},
    {'ciudad': 'Lima', 'categoria': 'Bebidas', 'monto': 890.00},
]

cuenta = Counter(v['ciudad'] for v in movimientos)

print(cuenta)
print('la que mas vende:', cuenta.most_common(1))
print('cuantas de Piura:', cuenta['Piura'])
```

```text
Counter({'Lima': 3, 'Arequipa': 1, 'Cusco': 1})
la que mas vende: [('Lima', 3)]
cuantas de Piura: 0
```

Tres regalos ahí 🎁

`Counter` cuenta lo que le pases y ya viene ordenado de mayor a menor. `most_common(3)` te da el podio sin ordenar nada tú. Y una clave que no existe devuelve **cero** en vez de reventar, que es exactamente lo que quieres al contar.

Para agrupar, en vez de contar, está el otro:

```
por_ciudad = defaultdict(list)

for v in movimientos:
    por_ciudad[v['ciudad']].append(v['monto'])

for ciudad, montos in por_ciudad.items():
    print(f'{ciudad:<10} {len(montos)} ventas, S/{sum(montos):>8.2f}')
```

```text
Lima       3 ventas, S/ 3851.21
Arequipa   1 ventas, S/  154.83
Cusco      1 ventas, S/  524.90
```

La gracia está en el `defaultdict(list)`: cuando le pides una clave que no existe, **se la inventa vacía** en vez de fallar. Con un diccionario normal, la primera venta de Lima revienta:

```
sin_default = {}
sin_default['Lima'].append(480.37)
```

```
KeyError: 'Lima'
```

Por eso el patrón de siempre es `if ciudad not in d: d[ciudad] = []` antes de cada `append`. Con `defaultdict` esa línea desaparece y el bucle se lee de un vistazo 🌸

## Ordenar por dos cosas a la vez

Ya ordenaste una lista de números. Con una lista de diccionarios hay que decirle **por qué campo**, y ahí entra `key`:

```
ordenadas = sorted(movimientos, key=lambda v: (v['ciudad'], -v['monto']))

for v in ordenadas:
    print(f"{v['ciudad']:<10} {v['categoria']:<12} {v['monto']:>8.2f}")
```

```text
Arequipa   Bebidas        154.83
Cusco      Limpieza       524.90
Lima       Abarrotes     2480.84
Lima       Bebidas        890.00
Lima       Abarrotes      480.37
```

Ciudad en orden alfabético, y dentro de cada ciudad los montos de mayor a menor 📊

El truco está en esa **tupla** que devuelve el `lambda`. Python compara tuplas de izquierda a derecha: primero mira la ciudad, y solo si empatan pasa al segundo elemento. Puedes poner tres o cuatro criterios, y se comparan en ese orden.

Y el signo menos delante del monto es el truco para invertir **solo ese criterio**. Con `reverse=True` le darías la vuelta a los dos, y las ciudades te saldrían de la Z a la A. El menos solo funciona con números; para invertir un texto hay que ordenar dos veces, primero por el criterio de dentro y después por el de fuera.

## Ejercicios

**1. Primero, último y cuántos**

Con una lista de cinco canales, imprime el primero, el último y cuántos hay, sin contar a mano.

```
canales = ['Web', 'Tienda', 'WhatsApp', 'Marketplace', 'Telefono']

print(canales[0], canales[-1], len(canales))
```

```text
Web Telefono 5
```

**2. Las tres del medio**

De esa misma lista, saca solo los elementos 2, 3 y 4 con una rebanada.

```
print(canales[1:4])
```

```text
['Tienda', 'WhatsApp', 'Marketplace']
```

Recuerda que el límite derecho no entra: `1:4` te da las posiciones 1, 2 y 3.

**3. El top 3 de montos**

De una lista de montos, saca los tres más altos ordenados de mayor a menor, sin modificar la lista original.

```
montos = [480.37, 154.83, 2480.84, 524.90, 95.5, 1200.0]

top = sorted(montos, reverse=True)[:3]
print(top)
print(montos[0])          # la original intacta
```

```text
[2480.84, 1200.0, 524.9]
480.37
```

**4. Demuestra el aliasing**

Crea una lista, hazle una copia falsa y una de verdad, y comprueba con `is` cuál es cuál.

```
pedido = ['Abarrotes', 'Bebidas', 'Snacks']
falsa = pedido
buena = pedido.copy()

falsa.append('Limpieza')
print(pedido)
print(buena)
print(falsa is pedido, buena is pedido)
```

```text
['Abarrotes', 'Bebidas', 'Snacks', 'Limpieza']
['Abarrotes', 'Bebidas', 'Snacks']
True False
```

`is` pregunta si son el mismo objeto; `==` pregunta si tienen el mismo contenido. Dos listas distintas con lo mismo dentro dan `True` con `==` y `False` con `is`.

**5. Un diccionario que no revienta**

Con un diccionario de una venta que no trae la clave `descuento`, calcula el monto final asumiendo 0 si no está.

```
venta = {'cliente': 'C0325', 'monto': 524.90}

descuento = venta.get('descuento', 0)
print(f'S/{venta["monto"] * (1 - descuento):.2f}')
```

```text
S/524.90
```

Fíjate en las comillas dobles dentro del f-string que abre con comillas simples. Si usas las mismas por fuera y por dentro, Python se pierde.

**6. Contar por categoría**

Con una lista de canales repetidos, cuenta cuántas veces aparece cada uno.

```
canales = ['Web', 'Tienda', 'Web', 'WhatsApp', 'Web', 'Tienda']

conteo = {}
for c in canales:
    conteo[c] = conteo.get(c, 0) + 1

print(conteo)
```

```text
{'Web': 3, 'Tienda': 2, 'WhatsApp': 1}
```

Es el mismo patrón de la suma por ciudad, pero sumando 1 en vez del monto. Cuando lo veas escrito así vas a reconocer `value_counts()` de pandas sin que nadie te lo explique.

**7. El máximo con nombre**

De una lista de diccionarios de ventas, encuentra la de mayor monto e imprime su ciudad.

```
ventas = [
    {'ciudad': 'Lima', 'monto': 480.37},
    {'ciudad': 'Cusco', 'monto': 2480.84},
    {'ciudad': 'Piura', 'monto': 524.90},
]

mayor = max(ventas, key=lambda v: v['monto'])
print(mayor['ciudad'], mayor['monto'])
```

```text
Cusco 2480.84
```

Ese `lambda` es una función chiquita sin nombre que dice "para comparar, mira el monto". Lo vemos con calma en el capítulo 6, tranqui.

**8. Comprensión con condición**

De la lista de ventas anterior, arma una lista solo con las ciudades cuyo monto pase los S/500.

```
print([v['ciudad'] for v in ventas if v['monto'] > 500])
```

```text
['Cusco', 'Piura']
```

**9. Quitar duplicados conservando el orden**

De una lista con ciudades repetidas, saca las únicas. Hazlo de dos formas y compara.

```
ciudades = ['Lima', 'Cusco', 'Lima', 'Piura', 'Cusco', 'Lima']

print(sorted(set(ciudades)))

vistas = []
for c in ciudades:
    if c not in vistas:
        vistas.append(c)
print(vistas)
```

```text
['Cusco', 'Lima', 'Piura']
['Lima', 'Cusco', 'Piura']
```

`set()` es rapidísimo y pierde el orden original; el bucle lo conserva. Cuál usar depende de si el orden significa algo, y muchas veces significa.

**10. La tupla que no se deja**

Crea una tupla con un rango de fechas e intenta cambiar el primer valor. Después haz lo mismo con una lista y compara.

```
rango_tupla = ('2026-01-01', '2026-12-31')
rango_lista = ['2026-01-01', '2026-12-31']

rango_lista[0] = '2026-02-01'
print(rango_lista)

rango_tupla[0] = '2026-02-01'
```

```
TypeError: 'tuple' object does not support item assignment
```

La tupla te protege de un cambio accidental. Cuando algo no debe moverse en todo el programa, esa protección vale más que la flexibilidad.

**11. Quién se fue y quién llegó**

Con tres meses de códigos de cliente, di cuántos se mantuvieron, cuántos se perdieron y cuántos entraron nuevos.

```
meses = {
    'enero': {'C0045', 'C0325', 'C0260', 'C0111'},
    'febrero': {'C0325', 'C0045', 'C0777', 'C0888'},
    'marzo': {'C0045', 'C0888', 'C0999'},
}

fieles = meses['enero'] & meses['febrero'] & meses['marzo']
alguna_vez = meses['enero'] | meses['febrero'] | meses['marzo']

print('clientes distintos en el trimestre:', len(alguna_vez))
print('compraron los tres meses          :', sorted(fieles))
print('perdidos de enero a febrero       :',
      sorted(meses['enero'] - meses['febrero']))
print('nuevos en marzo                   :',
      sorted(meses['marzo'] - meses['enero'] - meses['febrero']))
print('retencion enero -> febrero        :',
      round(len(meses['enero'] & meses['febrero']) / len(meses['enero']) * 100), '%')
```

```text
clientes distintos en el trimestre: 7
compraron los tres meses          : ['C0045']
perdidos de enero a febrero       : ['C0111', 'C0260']
nuevos en marzo                   : ['C0999']
retencion enero -> febrero        : 50 %
```

Un cliente de cuatro aguantó los tres meses, y la retención del primer mes al segundo es del 50% 📉

Con datos de verdad esto es el análisis de cohortes entero, y sale de tres conjuntos y cuatro operaciones. Ni una librería.

**12. El podio, sin ordenar nada**

Cuenta las ventas por categoría y saca las dos más frecuentes.

```
categorias = Counter(v['categoria'] for v in movimientos)

print(categorias)
print('el podio     :', categorias.most_common(2))
print('cuantas hay  :', len(categorias), 'categorias distintas')
print('total ventas :', sum(categorias.values()))
print('una que no esta:', categorias['Congelados'])
```

```text
Counter({'Abarrotes': 2, 'Bebidas': 2, 'Limpieza': 1})
el podio     : [('Abarrotes', 2), ('Bebidas', 2)]
cuantas hay  : 3 categorias distintas
total ventas : 5
una que no esta: 0
```

Ese último cero es la razón por la que uso `Counter` y no un diccionario normal 🛡️

Preguntar por una categoría que no vino este mes devuelve cero, que es la respuesta correcta. Un diccionario normal te lanzaría un `KeyError` y te tumbaría el reporte por una categoría que simplemente no vendió.

**13. Agrupar montos por ciudad**

Junta los montos de cada ciudad y saca el total, el promedio y el máximo de cada una.

```
agrupado = defaultdict(list)
for v in movimientos:
    agrupado[v['ciudad']].append(v['monto'])

for ciudad in sorted(agrupado):
    montos = agrupado[ciudad]
    print(f'{ciudad:<10} {len(montos)} ventas  '
          f'total S/{sum(montos):>9.2f}  '
          f'promedio S/{sum(montos) / len(montos):>8.2f}  '
          f'mayor S/{max(montos):>8.2f}')
```

```text
Arequipa   1 ventas  total S/   154.83  promedio S/  154.83  mayor S/  154.83
Cusco      1 ventas  total S/   524.90  promedio S/  524.90  mayor S/  524.90
Lima       3 ventas  total S/  3851.21  promedio S/ 1283.74  mayor S/ 2480.84
```

Ese `sorted(agrupado)` del bucle ordena las claves alfabéticamente, para que la tabla salga siempre igual. Sin él salen en el orden en que aparecieron, que depende del archivo y cambia cada mes 🔀

Y esto es, literalmente, lo que hace `groupby` de pandas. Cuando llegues a ese capítulo vas a saber qué está pasando por debajo.

**14. Un ranking con dos criterios**

Ordena las ventas por ciudad y, dentro de cada ciudad, de mayor a menor monto. Después dale la vuelta al criterio de fuera.

```
print('ciudad A-Z, monto de mayor a menor:')
for v in sorted(movimientos, key=lambda v: (v['ciudad'], -v['monto'])):
    print(f"  {v['ciudad']:<10} {v['monto']:>8.2f}")

print('y ahora ciudad Z-A, monto de menor a mayor:')
for v in sorted(movimientos, key=lambda v: (v['ciudad'], -v['monto']), reverse=True):
    print(f"  {v['ciudad']:<10} {v['monto']:>8.2f}")
```

```text
ciudad A-Z, monto de mayor a menor:
  Arequipa     154.83
  Cusco        524.90
  Lima        2480.84
  Lima         890.00
  Lima         480.37
y ahora ciudad Z-A, monto de menor a mayor:
  Lima         480.37
  Lima         890.00
  Lima        2480.84
  Cusco        524.90
  Arequipa     154.83
```

Mira lo que hizo el `reverse=True`: le dio la vuelta a **los dos** criterios 🔄

Las ciudades pasaron de la Z a la A, pero los montos, que iban de mayor a menor por el signo menos, ahora van de menor a mayor. El `reverse` no distingue: invierte la comparación entera.

Por eso el signo menos es mejor herramienta cuando quieres criterios en direcciones distintas. Es más quisquilloso de escribir y te deja controlar cada uno por separado 🎛️

## Quitar duplicados y quedarte con uno

Esta es la primera trampa de listas con la que se topa todo el mundo, y no avisa de ninguna manera: no revienta, no imprime nada raro, y la lista sale más corta como esperabas 🫥

La trampa

Limpias duplicados recorriendo la lista y quitando lo que sobra. Corre, no da error, y la lista sale más corta.

```
ciudades = ['lima', 'lima', 'cusco']

for c in ciudades:
    if c == 'lima':
        ciudades.remove(c)

print(ciudades)   # ['lima', 'cusco']
```

**Qué está mal**

Sobrevivió una Lima 👻 Al quitar la primera, todo lo de detrás se corre un sitio hacia la izquierda, pero el bucle ya va por la posición 1. Se salta la segunda Lima sin mirarla.

Y esto es lo peligroso: la lista *sí* se acortó, así que parece que funcionó. Con duplicados sueltos acierta, con duplicados pegados falla, y nadie revisa un limpiador de duplicados que devuelve menos filas de las que entraron.

Nunca se modifica una lista mientras se recorre. Se construye una nueva con una comprensión (`[c for c in ciudades if c != 'lima']`), que además se lee mejor y es más rápida.

Comprueba que lo tienes

Haces `b = a` con dos listas, cambias b y a también cambió. ¿Por qué?

- Porque las dos etiquetas apuntan a la misma lista
- Porque las listas se copian solas
- Porque hay que usar tuplas para que no cambien
- Porque b quedó dentro de a

## Lo que te llevas

- 🔢 Se cuenta desde 0, y `[-1]` es siempre el último.
- ✂️ En una rebanada, el límite de la derecha no entra.
- ⚠️ `copia = original` no copia nada: usa `.copy()`.
- 🔀 `sorted()` devuelve una lista nueva; `.sort()` devuelve `None`.
- 🗂️ Una lista de diccionarios es como llega el mundo real, y agrupar con `.get(clave, 0)` es el patrón que después hace `groupby`.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Un diccionario no es una lista con nombres. Es la respuesta a "y esto de quién es".

Y si un diccionario te suena a "una fila con los nombres de las columnas", vas bien: es exactamente lo que devuelve una consulta, y de eso va el [libro de SQL](https://missyera.com/guias/sql-desde-cero/) 🗃️

En el capítulo 5 le enseñamos a Python a decidir y a repetir: `if`, `for` y `while`. Ahí es donde esto deja de ser una calculadora y empieza a ser un programa.

Que tengas un hermoso día! 🌟

## Preguntas frecuentes

### ¿Qué son las listas en Python?

Una secuencia ordenada de cosas que se puede modificar. Se escribe entre corchetes y el primer elemento es el cero.

### ¿Qué son los diccionarios en Python?

Pares de clave y valor. En vez de pedir el elemento tres pides el elemento "ciudad", que es como llegan casi todos los datos de una API.

### ¿Cuál es la diferencia entre lista y tupla?

La tupla no se puede modificar después de creada. Se usa para cosas que no deberían cambiar, como unas coordenadas.

### ¿Cómo copio una lista sin que se copie sola?

Con lista.copy() o con una rebanada completa. Si escribes b = a, las dos apuntan a la misma lista y modificar una modifica la otra, que es el error que más desconcierta al principio.

## Seguir leyendo

- Anterior: [Variables, números y texto](https://missyera.com/guias/python-desde-cero/variables-y-tipos.md)
- Siguiente: [Condicionales y bucles](https://missyera.com/guias/python-desde-cero/control-de-flujo.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/python-desde-cero/listas-y-diccionarios/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
