Antes de sumar un CSV, hay que decidir qué significa cada importe. 0,00 y un campo vacío no significan lo mismo. Tampoco conviene transformar 1.234,56 quitando puntos sin saber qué formato acordó quien exportó el archivo.
En este tutorial vas a separar valores válidos de filas que necesitan revisión, conservar centavos exactos y comprobar la salida con datos ficticios. Solo utiliza la biblioteca estándar de Python: no necesita instalar pandas, abrir una cuenta ni llamar a un modelo de IA.
El problema que vamos a resolver
La guía de reportes de Excel con Python plantea revisar separadores y tipos de datos antes de consolidar archivos. Aquí trabajamos ese paso previo con una práctica pequeña y reproducible: validar el formato sin adivinar datos ausentes.
Si todavía estás practicando funciones y excepciones, empieza por los ejercicios de Python para principiantes. Este ejercicio añade una entrada imperfecta y una salida de revisión.
Contrato de entrada: reglas antes del código
Estas reglas pertenecen exclusivamente al ejercicio. No son una norma general para archivos de dinero ni una política de precios de Cursalo.
- El CSV tiene exactamente las columnas
id;importe, en ese orden, y usa punto y coma como separador. - Los importes representan una única unidad monetaria ficticia. No hay conversión de monedas.
- Se aceptan entre uno y siete dígitos ASCII en la parte entera, coma decimal y exactamente dos dígitos decimales.
- Se eliminan espacios al principio y al final del importe. No se eliminan separadores de miles, signos ni caracteres internos.
- Cero es un importe válido. Vacío significa que falta el dato: va a revisión, no se convierte en cero.
- Negativos, punto decimal, separadores de miles, tres decimales y valores como
NaNvan a revisión. No se redondean ni se corrigen automáticamente. - Solo se suman las filas que pasan las reglas. El total no afirma que el archivo completo esté correcto.
En un proyecto real, acuerda estas reglas con quien produce los datos. Si hay devoluciones negativas o distintas monedas, necesitas otro contrato de entrada.
Dataset ficticio y resultado esperado
Los diez registros siguientes son inventados para estudiar. No pertenecen a clientes, compras ni transacciones reales. Se incluyen en la variable DATOS del programa completo.
| ID | Entrada | Decisión | Centavos válidos |
|---|---|---|---|
| M01 | 12,50 | válida | 1250 |
| M02 | 0,10 | válida | 10 |
| M03 | 0,20 | válida | 20 |
| M04 | vacío | revisar: vacio | — |
| M05 | 1.234,56 | revisar: formato | — |
| M06 | 8,999 | revisar: formato | — |
| M07 | -2,00 | revisar: formato | — |
| M08 | NaN | revisar: formato | — |
| M09 | 0,00 | válida | 0 |
| M10 | un espacio seguido de 7,40 | válida | 740 |
Referencia: cinco filas válidas, cinco a revisar y 2020 centavos, equivalentes a 20.20 en la salida normalizada del programa. La suma comprobable es 1250 + 10 + 20 + 0 + 740 = 2020. El cero de M09 se conserva; el vacío de M04 no se incluye.
Por qué usar csv y Decimal
Según la documentación oficial de csv, la lectura devuelve campos como cadenas y permite declarar el delimitador. DictReader asocia esas cadenas con los nombres de la cabecera. Esa separación nos deja validar el texto antes de convertirlo.
La documentación oficial de Decimal explica la representación decimal y la conversión desde texto. En esta solución construimos Decimal desde una cadena validada, nunca desde un float, y convertimos el resultado a centavos enteros. Así 0,10 + 0,20 se comprueba como 10 + 20 = 30.
No usamos Decimal para rescatar entradas ambiguas. Primero se valida el formato; después se convierte. El límite de siete dígitos mantiene pequeño el dominio del ejercicio.
Programa completo para ejecutar
Guarda el siguiente bloque como validar_importes.py y ejecútalo con python3 validar_importes.py. El dataset está dentro del código: no requiere archivos de entrada, red ni paquetes externos. Se ejecutó localmente el mismo programa publicado y se comparó su salida con la referencia.
import csv
import io
import json
import re
from decimal import Decimal
DATOS = """id;importe
M01;12,50
M02;0,10
M03;0,20
M04;
M05;1.234,56
M06;8,999
M07;-2,00
M08;NaN
M09;0,00
M10; 7,40
"""
def a_centavos(texto):
valor = texto.strip()
if not valor:
raise ValueError("vacio")
if not re.fullmatch(r"[0-9]{1,7},[0-9]{2}", valor):
raise ValueError("formato")
return int(Decimal(valor.replace(",", ".")) * 100)
def revisar_csv(texto):
lector = csv.DictReader(io.StringIO(texto), delimiter=";", strict=True)
if lector.fieldnames != ["id", "importe"]:
raise ValueError("cabecera")
validas, revisar = [], []
for fila in lector:
identificador = fila.get("id", "")
if None in fila or any(v is None for v in fila.values()):
revisar.append({"id": identificador, "motivo": "columnas"})
continue
try:
centavos = a_centavos(fila["importe"])
except ValueError as error:
revisar.append({"id": identificador, "motivo": str(error)})
continue
validas.append({"id": identificador, "centavos": centavos})
total = sum(fila["centavos"] for fila in validas)
return {
"validas": validas,
"revisar": revisar,
"total_centavos": total,
"total_decimal": f"{Decimal(total) / 100:.2f}",
}
if __name__ == "__main__":
print(json.dumps(revisar_csv(DATOS), ensure_ascii=False, indent=2))
Cómo leer la salida
validas conserva el ID y el importe convertido a centavos. revisar conserva el ID y el motivo del rechazo; no incluye una cantidad inventada. total_centavos suma únicamente las filas válidas y total_decimal muestra la misma cantidad con punto y dos decimales.
La cabecera incorrecta detiene el ejercicio con ValueError("cabecera"). Una fila con columnas de más o de menos se registra como columnas. Un CSV sintácticamente mal formado puede provocar csv.Error; no se intenta repararlo automáticamente. Los renglones completamente vacíos se omiten por el lector CSV.
Prompt 1: revisar las reglas sin corregir a ciegas
Revisa este ejercicio ficticio de CSV, no transacciones reales.
Contrato: separador ;, cabecera id;importe. Importe con 1 a 7 dígitos
ASCII, coma y exactamente 2 decimales. Solo se quitan espacios externos.
Cero es válido; vacío va a revisión. No admitir negativos, punto decimal,
miles, NaN ni 3 decimales. No redondear ni inventar valores.
Datos:
id;importe
M01;12,50
M02;0,10
M03;0,20
M04;
M05;1.234,56
M06;8,999
M07;-2,00
M08;NaN
M09;0,00
M10; 7,40
Devuelve por ID: valida/revisar, motivo y centavos si es válida.
Luego suma solo los centavos válidos. Explica por qué M04 y M09
no reciben el mismo tratamiento. No cambies el contrato.
Comprueba la respuesta contra la tabla anterior: M04 va a revisión, M09 aporta cero y el total válido es 2020. Una salida que transforme 1.234,56 en 123456 centavos incumple este contrato, aunque esa conversión pudiera ser adecuada en otro archivo.
Prompt 2: proponer pruebas, no resultados imaginados
Estoy aprendiendo Python con una función a_centavos(texto).
Acepta solo 1 a 7 dígitos ASCII, coma y 2 decimales; recorta espacios
externos y devuelve centavos enteros. Vacío lanza ValueError("vacio");
otro formato lanza ValueError("formato"). No se usa float ni redondeo.
Propón pruebas con assert para cero, 0,10 + 0,20, espacios externos,
el máximo 9999999,99, vacío, 1.234,56, 8,999, -2,00 y NaN.
Indica resultados esperados según el contrato y separa código de prueba
de afirmaciones de ejecución. No digas que ejecutaste las pruebas.
Una IA puede ayudar a explicar o proponer casos. En este artículo no se atribuye la solución a un modelo ni se presenta una prueba de rendimiento de modelos.
Pruebas cortas que puedes ejecutar
Añade estas comprobaciones después de las funciones, antes del bloque if __name__ == "__main__", o ejecútalas en un archivo que importe las funciones.
assert a_centavos("0,00") == 0
assert a_centavos("0,10") + a_centavos("0,20") == 30
assert a_centavos(" 7,40 ") == 740
assert a_centavos("9999999,99") == 999999999
resultado = revisar_csv(DATOS)
assert len(resultado["validas"]) == 5
assert len(resultado["revisar"]) == 5
assert resultado["total_centavos"] == 2020
assert resultado["total_decimal"] == "20.20"
assert revisar_csv("id;importe\n")["total_centavos"] == 0
for entrada in ("1.234,56", "8,999", "-2,00", "NaN"):
try:
a_centavos(entrada)
except ValueError as error:
assert str(error) == "formato"
else:
raise AssertionError(f"Entrada aceptada indebidamente: {entrada}")
La verificación local también cubrió cabecera incorrecta, columnas adicionales o ausentes, espacios vacíos, punto decimal, Infinity, más de siete dígitos enteros y dígitos fuera de ASCII. El dataset se ejecutó dos veces con el mismo resultado. Estas pruebas cubren el contrato publicado, no todas las variantes posibles de archivos CSV.
Una variación para comprobar que entendiste
Cambia solo M04 de vacío a 3,00. Ahora deben quedar seis filas válidas, cuatro a revisar y 2320 centavos, con salida decimal 23.20. M05, M06, M07 y M08 siguen a revisión: modificar una fila no cambia las reglas del resto.
Después vuelve a dejar M04 vacío y sustituye M09 por 0,01: el total válido pasa a 2021 centavos. Si tu versión vuelve a producir 2020, está confundiendo cero con la presencia de un importe.
Límites y siguiente paso de aprendizaje
El programa no valida la identidad del registro, no detecta IDs repetidos, no convierte monedas y no carga ni modifica bases de datos. Dos filas válidas con el mismo ID se sumarían dos veces: decidir cómo tratar esa duplicación pertenece a otro ejercicio. Tampoco procesa importes negativos o formatos regionales distintos sin cambiar explícitamente el contrato.
Para practicar funciones, tipos y manejo de errores con una formación existente, consulta Introducción a Python. Revisa su temario y las condiciones vigentes en la página del curso. Este tutorial complementa esa práctica; no cambia el acceso ni promete una certificación o ingresos.
Cuando puedas explicar por qué cada registro se acepta o se rechaza, vuelve a la guía de reportes de Excel y añade una etapa de revisión antes de calcular indicadores. Conserva siempre la entrada original y documenta qué filas quedaron fuera.
Fuentes oficiales consultadas el 30 de septiembre de 2026: módulo csv y módulo decimal de Python.