SolveConPython

Python Reto #18 — Ordenar una lista de diccionarios por múltiples campos

Nivel: Intermedio
Tema: Ordenación, sorted(), key=, tuplas como clave, manejo de datos faltantes, validación de entradas, tests con pytest
Objetivo: Ordenar datos “tipo tabla” (lista de diccionarios) por más de un criterio, un patrón típico en reporting, ETL y preparación de datos.

Reto #18 Ordenar una lista de diccionarios por múltiples campos
Reto #18 Ordenar una lista de diccionarios por múltiples campos

Enunciado

Crea una función llamada ordenar_por_campos(registros, campos) que:

  1. Reciba:
    • registros: una lista de diccionarios
    • campos: una lista/tupla de nombres de campos (strings) por los que ordenar, en prioridad (primero el campo 1, luego el 2, etc.)
  2. Si registros es None, devuelva [].
  3. Si registros no es una lista, lance TypeError.
  4. Si campos no es una lista o tupla, lance TypeError.
  5. Si algún elemento de campos no es str, lance TypeError.
  6. Devuelva una nueva lista ordenada por los campos indicados (orden ascendente).
  7. Para registros que no tengan alguno de los campos, considera el valor como None y ordénalos al final.

Nota: Orden ascendente. Para este reto, no necesitas soportar orden descendente.

Ejemplos

registros = [
{"nombre": "Ana", "edad": 30, "ciudad": "Madrid"},
{"nombre": "Luis", "edad": 25, "ciudad": "Barcelona"},
{"nombre": "Ana", "edad": 22, "ciudad": "Valencia"},
]

ordenar_por_campos(registros, ["nombre", "edad"])
# [
# {"nombre": "Ana", "edad": 22, "ciudad": "Valencia"},
# {"nombre": "Ana", "edad": 30, "ciudad": "Madrid"},
# {"nombre": "Luis", "edad": 25, "ciudad": "Barcelona"},
# ]

Con campos faltantes:

registros = [
{"nombre": "Ana", "edad": 30},
{"nombre": "Luis"}, # edad falta
{"nombre": "Ana", "edad": 22},
]

ordenar_por_campos(registros, ["nombre", "edad"])
# "Luis" sin edad debe ir al final dentro de su grupo

Pistas

  1. sorted(registros, key=...) crea una nueva lista ordenada.
  2. Para ordenar por múltiples campos, la key puede devolver una tupla.
  3. Para mandar None al final, una técnica común es usar (valor is None, valor).

Solución explicada (paso a paso)

  1. Validamos entradas:
    • registros debe ser lista o None
    • campos debe ser lista o tupla
    • cada campo debe ser str
  2. Definimos una función clave_ordenacion(registro) que construya una tupla con tantos criterios como campos:
    • para cada campo, obtiene el valor con registro.get(campo, None)
    • transforma ese valor en un criterio que empuje None al final: (valor is None, valor)
  3. Aplicamos sorted() con esa clave.
  4. Devolvemos la lista ordenada.

Python
def ordenar_por_campos(registros: list | None, campos) -> list:
"""
Ordena una lista de diccionarios por múltiples campos (ascendente).
Registros sin campo -> None, y None va al final.
Reglas:
- registros None -> []
- registros debe ser list
- campos debe ser list o tuple
- cada campo debe ser str
"""
if registros is None:
return []
if not isinstance(registros, list):
raise TypeError("El parámetro 'registros' debe ser una lista o None.")
if not isinstance(campos, (list, tuple)):
raise TypeError("El parámetro 'campos' debe ser una lista o tupla.")
for c in campos:
if not isinstance(c, str):
raise TypeError("Cada elemento de 'campos' debe ser una cadena (str).")
def clave_ordenacion(registro: dict):
# Para cada campo: (True/False si es None, valor)
# Esto hace que None quede al final.
clave = []
for c in campos:
valor = registro.get(c, None) if isinstance(registro, dict) else None
clave.append((valor is None, valor))
return tuple(clave)
return sorted(registros, key=clave_ordenacion)

Python
import pytest
from reto_18_ordenar_por_campos import ordenar_por_campos
def test_ordenar_por_dos_campos():
registros = [
{"nombre": "Ana", "edad": 30, "ciudad": "Madrid"},
{"nombre": "Luis", "edad": 25, "ciudad": "Barcelona"},
{"nombre": "Ana", "edad": 22, "ciudad": "Valencia"},
]
resultado = ordenar_por_campos(registros, ["nombre", "edad"])
assert resultado == [
{"nombre": "Ana", "edad": 22, "ciudad": "Valencia"},
{"nombre": "Ana", "edad": 30, "ciudad": "Madrid"},
{"nombre": "Luis", "edad": 25, "ciudad": "Barcelona"},
]
def test_none_en_campos_va_al_final():
registros = [
{"nombre": "Ana", "edad": 30},
{"nombre": "Luis"}, # edad falta
{"nombre": "Ana", "edad": 22},
{"nombre": "Luis", "edad": 20},
]
resultado = ordenar_por_campos(registros, ["nombre", "edad"])
assert resultado == [
{"nombre": "Ana", "edad": 22},
{"nombre": "Ana", "edad": 30},
{"nombre": "Luis", "edad": 20},
{"nombre": "Luis"},
]
def test_registros_none_devuelve_lista_vacia():
assert ordenar_por_campos(None, ["nombre"]) == []
def test_tipo_incorrecto_registros():
with pytest.raises(TypeError):
ordenar_por_campos("no_es_lista", ["nombre"])
def test_tipo_incorrecto_campos():
with pytest.raises(TypeError):
ordenar_por_campos([], "nombre")
def test_campos_con_elemento_no_str():
with pytest.raises(TypeError):
ordenar_por_campos([], ["nombre", 123])

Ejecuta:

  • pytest -q

Variantes para subir de nivel (opcional)

  1. Orden descendente por campo (ej. ["-edad", "nombre"])
  2. Ordenar por campos anidados (ej. "usuario.edad")
  3. Normalizar strings (lowercase, trimming) antes de ordenar
  4. Estabilidad y “tie-breakers” (añadir un campo “id” final)

Lo que aprendiste

  • Ordenar por múltiples criterios usando tuplas como clave
  • Manejar valores faltantes (None) de forma consistente
  • Validar entradas en utilidades de datos
  • Escribir tests para ordenación determinista

Accede al código completo y a los tests en GitHub para ejecutar y modificar la solución localmente.

Continúa con Reto #19 — Generador de contraseñas (con reglas) para practicar validación, aleatoriedad controlada y tests más estratégicos.