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.

Enunciado
Crea una función llamada ordenar_por_campos(registros, campos) que:
- Reciba:
registros: una lista de diccionarioscampos: una lista/tupla de nombres de campos (strings) por los que ordenar, en prioridad (primero el campo 1, luego el 2, etc.)
- Si
registrosesNone, devuelva[]. - Si
registrosno es una lista, lanceTypeError. - Si
camposno es una lista o tupla, lanceTypeError. - Si algún elemento de
camposno esstr, lanceTypeError. - Devuelva una nueva lista ordenada por los campos indicados (orden ascendente).
- Para registros que no tengan alguno de los campos, considera el valor como
Noney 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
sorted(registros, key=...)crea una nueva lista ordenada.- Para ordenar por múltiples campos, la
keypuede devolver una tupla. - Para mandar
Noneal final, una técnica común es usar(valor is None, valor).
Solución explicada (paso a paso)
- Validamos entradas:
registrosdebe ser lista oNonecamposdebe ser lista o tupla- cada campo debe ser
str
- 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
Noneal final:(valor is None, valor)
- para cada campo, obtiene el valor con
- Aplicamos
sorted()con esa clave. - Devolvemos la lista ordenada.
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)
import pytestfrom reto_18_ordenar_por_campos import ordenar_por_camposdef 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)
- Orden descendente por campo (ej.
["-edad", "nombre"]) - Ordenar por campos anidados (ej.
"usuario.edad") - Normalizar strings (lowercase, trimming) antes de ordenar
- 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.