Saltar al contenido
csv.py · devschool

Archivos CSV en Python

Lección 22 de 31 · 11 min de lectura · Actualizado el

En esta lección
  1. Qué es un CSV
  2. Preparar los datos de ejemplo
  3. Leer con csv.reader
  4. DictReader: filas como diccionarios
  5. Procesar los datos
  6. Escribir CSV
  7. CSV de Excel en español
  8. CSV en memoria con io.StringIO
  9. Datos sucios
  10. Cuando el CSV se queda pequeño: pandas
  11. Errores frecuentes
  12. Resumen

El CSV es el formato más sencillo para mover tablas de datos de un programa a otro: lo exportan Excel, Google Sheets, las tiendas online, los bancos y casi cualquier aplicación de gestión. En ficheros y JSON viste cómo leer y escribir un CSV básico. En esta lección vas a trabajar con CSV de verdad: entender sus reglas, procesar los datos (sumar, filtrar, agrupar), guardar resultados y abrir sin problemas los CSV que genera Excel en español.

Qué es un CSV

CSV significa comma-separated values, valores separados por comas. Es un archivo de texto plano en el que:

  • Cada línea es una fila de la tabla.
  • Las columnas se separan con un carácter llamado separador o delimitador, normalmente la coma.
  • La primera fila suele ser la cabecera, con los nombres de las columnas.
fecha,tienda,producto,unidades,precio
2026-03-02,Santander,"Cuerda 9,8 mm",2,119.90
2026-03-02,Torrelavega,Pies de gato,1,89.00

Si un valor contiene el separador (como la coma de “9,8 mm”), se rodea de comillas dobles. Y si contiene comillas, se escriben dobles: "Modelo ""Pro""" significa Modelo "Pro". Por eso nunca debes leer un CSV con linea.split(","): rompería la cuerda en dos columnas. El módulo csv conoce estas reglas y las aplica por ti.

El punto y coma de Excel en español

En España la coma se usa como separador decimal (119,90). Para no liarse, Excel en español guarda los CSV con punto y coma como separador:

fecha;tienda;producto;unidades;precio
2026-03-02;Santander;Cuerda 9,8 mm;2;119,90

Más adelante verás cómo leer y escribir este formato.

Preparar los datos de ejemplo

Vamos a trabajar con las ventas de una cadena de tiendas de montaña. Para que el ejemplo funcione en cualquier sitio (también aquí en la web, que tiene su propio sistema de archivos virtual), primero creamos el archivo desde Python:

datos = """fecha,tienda,producto,categoria,unidades,precio
2026-03-02,Santander,"Cuerda 9,8 mm",escalada,2,119.90
2026-03-02,Torrelavega,Pies de gato,escalada,1,89.00
2026-03-03,Santander,Mochila 30 L,senderismo,3,54.50
2026-03-03,Laredo,Bastones,senderismo,4,32.00
2026-03-04,Torrelavega,Arnés,escalada,2,65.00
2026-03-04,Santander,Botas,senderismo,1,140.00
2026-03-05,Laredo,Pies de gato,escalada,2,89.00
"""

with open("ventas.csv", "w", encoding="utf-8", newline="") as f:
    f.write(datos)

Leer con csv.reader

csv.reader recibe un archivo abierto y devuelve las filas una a una, cada una como una lista de cadenas:

import csv

with open("ventas.csv", encoding="utf-8", newline="") as f:
    lector = csv.reader(f)
    cabecera = next(lector)        # la primera fila
    print(cabecera)
    for fila in lector:
        print(fila)
        break                      # solo la primera fila de datos
# ['fecha', 'tienda', 'producto', 'categoria', 'unidades', 'precio']
# ['2026-03-02', 'Santander', 'Cuerda 9,8 mm', 'escalada', '2', '119.90']

Fíjate en tres cosas:

  1. newline="": deja que el módulo csv gestione los saltos de línea. Sin él, un valor entre comillas que contenga un salto de línea se lee mal, y al escribir en Windows aparecen filas en blanco entre medias.
  2. encoding="utf-8": para que las tildes y la ñ (“Arnés”) se lean bien en cualquier sistema.
  3. Todo llega como texto: '2' y '119.90' son cadenas. Para operar hay que convertirlas con int() y float().

next(lector) saca la cabecera antes del bucle, para que el for solo recorra datos.

DictReader: filas como diccionarios

Acceder por posición (fila[4]) es frágil: si alguien añade una columna, todos los índices cambian. csv.DictReader usa la cabecera como claves y te da cada fila como un diccionario:

with open("ventas.csv", encoding="utf-8", newline="") as f:
    ventas = list(csv.DictReader(f))

print(len(ventas))                # 7
print(ventas[1]["producto"])      # Pies de gato
print(ventas[1])
# {'fecha': '2026-03-02', 'tienda': 'Torrelavega', 'producto': 'Pies de gato', 'categoria': 'escalada', 'unidades': '1', 'precio': '89.00'}

Convertimos list(...) dentro del with porque, al cerrarse el archivo, el lector ya no puede leer más. Para archivos pequeños y medianos, cargar todo en una lista es lo más cómodo. Para archivos enormes (millones de filas), es mejor procesar fila a fila dentro del for sin guardarlas todas.

Convertir los tipos una sola vez

Lo más limpio es convertir los valores numéricos nada más leer, en lugar de llenar el programa de float():

for v in ventas:
    v["unidades"] = int(v["unidades"])
    v["precio"] = float(v["precio"])

print(ventas[0]["unidades"] * ventas[0]["precio"])   # 239.8

Procesar los datos

Con la lista de diccionarios, todo lo que sabes de listas, diccionarios y comprensiones sirve para analizar los datos.

Sumar

facturacion = sum(v["unidades"] * v["precio"] for v in ventas)
unidades = sum(v["unidades"] for v in ventas)
print(f"Total: {facturacion:.2f} €, unidades: {unidades}")
# Total: 1068.30 €, unidades: 15

Filtrar

escalada = [v for v in ventas if v["categoria"] == "escalada"]
print(len(escalada))   # 4

caras = [v["producto"] for v in ventas if v["precio"] >= 100]
print(caras)           # ['Cuerda 9,8 mm', 'Botas']

Agrupar

¿Cuánto ha facturado cada tienda? Es el clásico “agrupar y sumar”, que se resuelve con un diccionario acumulador. Con defaultdict(float) no hace falta comprobar si la clave ya existe (lo viste en la biblioteca estándar):

from collections import defaultdict

por_tienda = defaultdict(float)
for v in ventas:
    por_tienda[v["tienda"]] += v["unidades"] * v["precio"]

for tienda, total in sorted(por_tienda.items(), key=lambda par: par[1], reverse=True):
    print(f"{tienda:<12} {total:>8.2f} €")
# Santander      543.30 €
# Laredo         306.00 €
# Torrelavega    219.00 €

sorted(..., key=..., reverse=True) ordena de mayor a menor facturación, y el formato :<12 y :>8.2f alinea las columnas.

El producto más vendido

from collections import Counter

vendidos = Counter()
for v in ventas:
    vendidos[v["producto"]] += v["unidades"]
print(vendidos.most_common(1))   # [('Bastones', 4)]

Escribir CSV

csv.writer: a partir de listas

writerow escribe una fila y writerows, varias de golpe. El módulo pone las comillas donde haga falta:

resumen = [["tienda", "facturacion"]]
resumen += [[tienda, round(total, 2)] for tienda, total in por_tienda.items()]

with open("resumen_tiendas.csv", "w", encoding="utf-8", newline="") as f:
    escritor = csv.writer(f)
    escritor.writerows(resumen)

with open("resumen_tiendas.csv", encoding="utf-8") as f:
    print(f.read(), end="")
# tienda,facturacion
# Santander,543.3
# Torrelavega,219.0
# Laredo,306.0

Puedes escribir números directamente: writer los convierte a texto.

csv.DictWriter: a partir de diccionarios

Cuando tus datos ya son diccionarios, DictWriter es más claro. Necesita la lista de columnas en fieldnames, y writeheader() escribe la cabecera:

columnas = ["producto", "tienda", "importe"]

with open("ventas_escalada.csv", "w", encoding="utf-8", newline="") as f:
    escritor = csv.DictWriter(f, fieldnames=columnas, extrasaction="ignore")
    escritor.writeheader()
    for v in escalada:
        v["importe"] = round(v["unidades"] * v["precio"], 2)
        escritor.writerow(v)

with open("ventas_escalada.csv", encoding="utf-8") as f:
    print(f.read(), end="")
# producto,tienda,importe
# "Cuerda 9,8 mm",Santander,239.8
# Pies de gato,Torrelavega,89.0
# Arnés,Torrelavega,130.0
# Pies de gato,Laredo,178.0

Los diccionarios de escalada tienen más claves de las que queremos guardar (fecha, categoria…). Por defecto DictWriter lanzaría ValueError al encontrarlas; con extrasaction="ignore" simplemente las descarta. Y si a una fila le falta alguna columna, la deja vacía.

Observa también que la cuerda ha salido entre comillas, porque contiene una coma.

CSV de Excel en español

Cuando abras un CSV exportado desde Excel en español te encontrarás tres diferencias:

DetalleCSV “estándar”Excel en español
Separador,;
Decimales119.90119,90
CodificaciónUTF-8UTF-8 con BOM (o Windows-1252 en versiones antiguas)

El BOM (byte order mark) es una marca invisible que algunos programas ponen al principio de un archivo UTF-8. Si lo lees con encoding="utf-8", el BOM se queda pegado al nombre de la primera columna ('fecha') y fila["fecha"] da KeyError. La codificación utf-8-sig lo elimina al leer y lo añade al escribir.

Escribir un CSV que Excel abra bien

Si un compañero va a abrir tu archivo con doble clic en Excel, usa ;, decimales con coma y utf-8-sig. Así las tildes se verán bien y cada dato irá a su columna:

with open("para_excel.csv", "w", encoding="utf-8-sig", newline="") as f:
    escritor = csv.writer(f, delimiter=";")
    escritor.writerow(["tienda", "facturación"])
    for tienda, total in por_tienda.items():
        escritor.writerow([tienda, f"{total:.2f}".replace(".", ",")])

with open("para_excel.csv", encoding="utf-8-sig") as f:
    print(f.read(), end="")
# tienda;facturación
# Santander;543,30
# Torrelavega;219,00
# Laredo;306,00

Leer un CSV de Excel

Para leerlo, indica el mismo separador y la misma codificación, y convierte los decimales cambiando la coma por un punto antes de usar float():

with open("para_excel.csv", encoding="utf-8-sig", newline="") as f:
    for fila in csv.DictReader(f, delimiter=";"):
        total = float(fila["facturación"].replace(",", "."))
        print(fila["tienda"], total)
# Santander 543.3
# Torrelavega 219.0
# Laredo 306.0

Consejo: si no sabes qué separador usa un archivo, abre las primeras líneas con un editor de texto. Si te llega un CSV con caracteres raros como é en lugar de é, prueba a abrirlo con encoding="cp1252", la codificación clásica de Windows.

CSV en memoria con io.StringIO

A veces el CSV no está en un archivo, sino en una cadena: la respuesta de una API, un texto pegado por el usuario… io.StringIO crea un “archivo” en memoria que el módulo csv puede leer igual que uno real:

import io

texto = "nombre;nota\nLucía;8,5\nIker;6\n"
for fila in csv.DictReader(io.StringIO(texto), delimiter=";"):
    print(fila["nombre"], float(fila["nota"].replace(",", ".")))
# Lucía 8.5
# Iker 6.0

También sirve al revés: escribir un CSV en un StringIO y obtener el texto con getvalue(), por ejemplo para enviarlo como descarga desde una web.

Datos sucios

Los CSV reales casi nunca están perfectos: celdas vacías, texto donde debería haber un número, filas incompletas. Si conviertes sin cuidado, el programa se detiene en la primera fila mala. Lo habitual es capturar el error con lo que viste en excepciones, avisar y seguir:

sucio = """producto,unidades
Cuerda,2
Arnés,
Casco,tres
Bastones,4
"""

total = 0
for numero, fila in enumerate(csv.DictReader(io.StringIO(sucio)), start=2):
    try:
        total += int(fila["unidades"])
    except ValueError:
        print(f"Línea {numero}: unidades no válidas ({fila['unidades']!r})")
print("Unidades válidas:", total)
# Línea 3: unidades no válidas ('')
# Línea 4: unidades no válidas ('tres')
# Unidades válidas: 6

Empezamos a contar en 2 porque la línea 1 es la cabecera; así el aviso coincide con la línea que verías en el editor.

Cuando el CSV se queda pequeño: pandas

Para análisis de datos serios (miles de filas, gráficas, cruzar tablas), la herramienta estándar es pandas, una librería externa que se instala con pip (pip install pandas). Lee un CSV de Excel en una línea y agrupa en otra:

import pandas as pd

df = pd.read_csv("para_excel.csv", sep=";", decimal=",", encoding="utf-8-sig")
print(df["facturación"].sum())   # 1068.3

Aprende primero el módulo csv: entenderás qué hace pandas por debajo y lo tendrás disponible siempre, sin instalar nada.

Errores frecuentes

  • Separar con split(","): rompe los valores que llevan comas entre comillas. Usa siempre el módulo csv.
  • Olvidar newline="": filas en blanco entre medias al escribir en Windows.
  • Operar con texto: "2" * "119.90" da TypeError; convierte con int() y float().
  • KeyError en la primera columna: es el BOM de Excel; abre con encoding="utf-8-sig".
  • Todo en una sola columna: el separador no es el que crees; prueba con delimiter=";".
  • float("119,90"): da ValueError; cambia antes la coma por un punto.
  • Usar "w" sobre el archivo original: lo vacías antes de leerlo. Guarda los resultados en otro archivo.

Resumen

TareaCódigo
Abrir para leeropen(ruta, encoding="utf-8", newline="")
Filas como listascsv.reader(f) y next(lector) para la cabecera
Filas como diccionarioscsv.DictReader(f)
Escribir listascsv.writer(f).writerows(filas)
Escribir diccionarioscsv.DictWriter(f, fieldnames=[...]) + writeheader()
Ignorar claves sobrantesDictWriter(..., extrasaction="ignore")
Separador punto y comadelimiter=";"
CSV de Excelencoding="utf-8-sig" y decimales con replace(",", ".")
CSV desde una cadenacsv.reader(io.StringIO(texto))
Agrupar y sumardefaultdict(float) o Counter

Los CSV son perfectos para intercambiar datos, pero cuando necesitas buscar, actualizar y relacionar registros, lo que necesitas es una base de datos. En la siguiente lección la crearás sin instalar nada con SQLite.

Pon a prueba lo que has aprendido

[Python] Para la línea lupa,"cuerda 9,8 mm",2 ¿cuántas columnas obtienen linea.split(",") y next(csv.reader([linea])), respectivamente?
linea = 'lupa,"cuerda 9,8 mm",2'

[Python] Con csv.DictReader, fila["unidades"] vale "2". ¿Qué devuelve fila["unidades"] + fila["unidades"]?

[Python] Al leer un CSV exportado por Excel con encoding="utf-8", fila["fecha"] lanza KeyError aunque la cabecera parece correcta. ¿Cuál es la causa y la solución más probable?

[Python] Un DictWriter se crea con fieldnames=["a"] y se llama a writerow({"a": 1, "b": 2}). ¿Qué ocurre?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.