Saltar al contenido

Indice / Repaso: archivos y errores / 1.3

CSV con el módulo csv

Un CSV no es «un archivo separado por comas»: es un formato con reglas de entrecomillado que split(",") no conoce. El módulo csv las conoce todas y viene en la biblioteca estándar.

Para qué sirve

Es el formato en el que van a llegar la mayoría de los datos de la materia, y el que más veces se procesa mal. Partir la línea con split(",") funciona hasta la primera fila que trae una coma adentro de un campo - y esa fila siempre existe.

Además, esta lección es donde el dato deja de ser texto plano y pasa a ser fila con columnas nombradas. Ese salto es lo que la unidad 2 va a formalizar y lo que pandas da por hecho.

ELI5Explicado facil

Pensalo como una dirección postal escrita en un renglón. Si el nombre de la calle tiene una coma, hace falta una convención para decir «esta coma es parte del nombre, no una separación». Esa convención son las comillas.

El módulo csv es el que sabe leer y escribir esa convención sin equivocarse. Vos nunca la implementás a mano.

Leer: cada fila es un diccionario

EjemploDictReader: cada fila es un diccionario
import csv

# newline="" no es un adorno: se lo pide la documentacion del modulo csv.
# El modulo maneja los finales de linea por su cuenta, y si open ademas
# los traduce, un campo con un salto adentro se parte al medio.
with open("tienda_clientes.csv", encoding="utf-8", newline="") as f:
    lector = csv.DictReader(f)
    print("columnas:", lector.fieldnames)
    filas = list(lector)

print("filas:", len(filas), "(la cabecera NO cuenta como fila)")
print()
print("la primera fila es un dict:")
print(" ", filas[0])
print()
print("y se accede por nombre de columna, no por posicion:")
print("  filas[0]['nombre'] ->", filas[0]["nombre"])
print("  filas[0]['alta']   ->", filas[0]["alta"])

print()
print("Todo llega como texto. SIEMPRE. Incluso lo que parece numero o fecha:")
print("  type(filas[0]['id']) ->", type(filas[0]["id"]).__name__)

print()
# Comparado con hacerlo a mano: split(",") no sabe nada de comillas.
with open("tienda_clientes.csv", encoding="utf-8") as f:
    lineas = f.read().splitlines()

print("la fila 35 del archivo, cruda:")
print("  ", lineas[35])
print("con split(',') el nombre queda: ", repr(lineas[35].split(",")[1]))
print("con csv el nombre queda:        ", repr(filas[34]["nombre"]))
print("Dos comillas seguidas adentro de un campo entrecomillado significan")
print("una comilla literal. csv lo sabe; split no.")

print()
# csv.reader acepta cualquier iterable de lineas, asi que se puede
# probar una fila suelta sin tocar el disco.
suelta = 'P900,"Teclado mecanico, retroiluminado",Perifericos,12,4.53'
print("un campo con una coma adentro:")
print("  split(',') ->", suelta.split(","))
print("  csv        ->", next(csv.reader([suelta])))
columnas: ['id', 'nombre', 'correo', 'alta']
filas: 40 (la cabecera NO cuenta como fila)

la primera fila es un dict:
  {'id': '1', 'nombre': 'Ximena Herrera', 'correo': 'ximena.herrera@example.com', 'alta': '2026-05-18'}

y se accede por nombre de columna, no por posicion:
  filas[0]['nombre'] -> Ximena Herrera
  filas[0]['alta']   -> 2026-05-18

Todo llega como texto. SIEMPRE. Incluso lo que parece numero o fecha:
  type(filas[0]['id']) -> str

la fila 35 del archivo, cruda:
   35,"José ""Pepe"" Rodríguez",olivia.rios@example.com,2026-03-20
con split(',') el nombre queda:  '"José ""Pepe"" Rodríguez"'
con csv el nombre queda:         'José "Pepe" Rodríguez'
Dos comillas seguidas adentro de un campo entrecomillado significan
una comilla literal. csv lo sabe; split no.

un campo con una coma adentro:
  split(',') -> ['P900', '"Teclado mecanico', ' retroiluminado"', 'Perifericos', '12', '4.53']
  csv        -> ['P900', 'Teclado mecanico, retroiluminado', 'Perifericos', '12', '4.53']
Dos cosas que hay que fijar de entrada

newline="" en el open, siempre, tanto al leer como al escribir. Lo pide la documentación del módulo: csv maneja los finales de línea por su cuenta, y si open además los traduce, un campo con un salto adentro se parte al medio.

Todo llega como str. Siempre. '2003' es texto, '57.24' es texto, y una columna vacía es '', no None. Convertir es trabajo tuyo, y es donde aparecen las excepciones de la lección 1.2.

Escribir, y el newline="" otra vez

EjemploDictWriter, y por que newline="" importa de verdad
import csv
import tempfile
from pathlib import Path

carpeta = Path(tempfile.mkdtemp())

filas = [
    {"id": "1", "nombre": "Ximena Herrera", "ciudad": "Salta"},
    {"id": "2", "nombre": 'José "Pepe" Rodríguez', "ciudad": "San Juan"},
    {"id": "3", "nombre": "Ana O'Connor", "ciudad": "Mar del Plata, BA"},
]
COLUMNAS = ["id", "nombre", "ciudad"]

bien = carpeta / "bien.csv"
with open(bien, "w", encoding="utf-8", newline="") as f:
    escritor = csv.DictWriter(f, fieldnames=COLUMNAS)
    escritor.writeheader()
    escritor.writerows(filas)

print("escrito con newline='' :")
print(bien.read_text(encoding="utf-8"))

print("El modulo entrecomillo SOLO lo que lo necesitaba:")
print("  la comilla se duplico, la coma obligo a entrecomillar,")
print("  y el apostrofe no necesito nada.")

# Ahora lo mismo SIN newline="".
mal = carpeta / "mal.csv"
with open(mal, "w", encoding="utf-8") as f:
    escritor = csv.DictWriter(f, fieldnames=COLUMNAS)
    escritor.writeheader()
    escritor.writerows(filas)

print()
print("bytes con newline='' :", len(bien.read_bytes()))
print("bytes sin newline='' :", len(mal.read_bytes()))
print("finales CR CR LF     :", mal.read_bytes().count(b"\r\r\n"))
print()
print("csv escribe CR LF; sin newline='' open traduce ese LF a CR LF otra")
print("vez, y queda CR CR LF. Excel lo muestra con una fila vacia entre")
print("cada dos. Es el bug de CSV mas reportado, y son dos caracteres.")

print()
print("leido de vuelta, el que esta bien vuelve identico:")
with open(bien, encoding="utf-8", newline="") as f:
    vuelta = list(csv.DictReader(f))
print("  ", vuelta == filas, "->", vuelta[1]["nombre"], "|", vuelta[2]["ciudad"])
escrito con newline='' :
id,nombre,ciudad
1,Ximena Herrera,Salta
2,"José ""Pepe"" Rodríguez",San Juan
3,Ana O'Connor,"Mar del Plata, BA"

El modulo entrecomillo SOLO lo que lo necesitaba:
  la comilla se duplico, la coma obligo a entrecomillar,
  y el apostrofe no necesito nada.

bytes con newline='' : 118
bytes sin newline='' : 122
finales CR CR LF     : 4

csv escribe CR LF; sin newline='' open traduce ese LF a CR LF otra
vez, y queda CR CR LF. Excel lo muestra con una fila vacia entre
cada dos. Es el bug de CSV mas reportado, y son dos caracteres.

leido de vuelta, el que esta bien vuelve identico:
   True -> José "Pepe" Rodríguez | Mar del Plata, BA
El CSV con una fila vacía entre cada dos

Es el bug de CSV más reportado del mundo y son dos caracteres. El módulo escribe CR LF al final de cada fila; sin newline="", en Windows open traduce ese LF a CR LF otra vez y el archivo queda con CR CR LF. Excel lo interpreta como una fila en blanco después de cada fila real.

El ejemplo lo muestra contando bytes: 118 contra 122, y cuatro CR CR LF donde no debería haber ninguno.

Al escribir, csv entrecomilla solo lo que hace falta: la coma dentro de Mar del Plata, BA obliga a entrecomillar, la comilla de José "Pepe" Rodríguez se duplica, y el apóstrofe de Ana O'Connor no necesita nada. Ese es exactamente el criterio que habría que reimplementar a mano, y no vale la pena.

Las filas que no tienen la forma esperada

Ejemploreader, DictReader y las filas que no tienen la forma esperada
import csv

CABECERA = "isbn,titulo,autor,anio,precio"

# csv.reader y csv.DictReader aceptan cualquier iterable de lineas, no
# solo un archivo. Sirve para probar un caso raro sin tocar el disco.
raras = [
    CABECERA,
    "978-1000-001,Algoritmos vol. 1,Ximena Molina,2003,57.24",
    "978-1000-002,Grafos,Ana Diaz,2010",                       # falta precio
    "978-1000-003,Redes,Luis Paz,2011,40.00,EXTRA,OTRO",       # sobran dos
]

print("con csv.reader: listas, y cada una con el largo que vino")
for fila in csv.reader(raras):
    print("  ", len(fila), fila)

print()
print("con csv.DictReader: dicts, y la forma se completa o se junta")
for fila in csv.DictReader(raras):
    print("  ", fila)

print()
print("Lo que falta queda en None y lo que sobra en la clave None.")
print("Ninguna de las dos cosas levanta excepcion: hay que buscarlas.")

print()
# restval y restkey le ponen nombre a esas dos situaciones.
lector = csv.DictReader(raras, restval="", restkey="sobrante")
for fila in lector:
    faltan = [c for c, v in fila.items() if v == ""]
    sobra = fila.get("sobrante")
    if faltan or sobra:
        print("   fila irregular:", fila["isbn"],
              "| vacias:", faltan, "| de mas:", sobra)

print()
print("En el archivo real las 50 filas tienen las 5 columnas...")
with open("libros_catalogo.csv", encoding="utf-8", newline="") as f:
    filas = list(csv.DictReader(f, restkey="sobrante", restval=None))
irregulares = [x for x in filas if "sobrante" in x or None in x.values()]
print("   filas:", len(filas), "| irregulares:", len(irregulares))
print("...pero eso es sobre la FORMA. El contenido es otra historia:")

NULOS = {"", "-", "N/A", "NaN", "null", "NA"}
vacias = sum(1 for x in filas for v in x.values() if v.strip() in NULOS)
print("   celdas con un token de ausente:", vacias)
print("   isbn distintos:", len({x['isbn'] for x in filas}), "de", len(filas))
con csv.reader: listas, y cada una con el largo que vino
   5 ['isbn', 'titulo', 'autor', 'anio', 'precio']
   5 ['978-1000-001', 'Algoritmos vol. 1', 'Ximena Molina', '2003', '57.24']
   4 ['978-1000-002', 'Grafos', 'Ana Diaz', '2010']
   7 ['978-1000-003', 'Redes', 'Luis Paz', '2011', '40.00', 'EXTRA', 'OTRO']

con csv.DictReader: dicts, y la forma se completa o se junta
   {'isbn': '978-1000-001', 'titulo': 'Algoritmos vol. 1', 'autor': 'Ximena Molina', 'anio': '2003', 'precio': '57.24'}
   {'isbn': '978-1000-002', 'titulo': 'Grafos', 'autor': 'Ana Diaz', 'anio': '2010', 'precio': None}
   {'isbn': '978-1000-003', 'titulo': 'Redes', 'autor': 'Luis Paz', 'anio': '2011', 'precio': '40.00', None: ['EXTRA', 'OTRO']}

Lo que falta queda en None y lo que sobra en la clave None.
Ninguna de las dos cosas levanta excepcion: hay que buscarlas.

   fila irregular: 978-1000-002 | vacias: ['precio'] | de mas: None
   fila irregular: 978-1000-003 | vacias: [] | de mas: ['EXTRA', 'OTRO']

En el archivo real las 50 filas tienen las 5 columnas...
   filas: 50 | irregulares: 0
...pero eso es sobre la FORMA. El contenido es otra historia:
   celdas con un token de ausente: 4
   isbn distintos: 49 de 50
reader contra DictReader
csv.readercsv.DictReader
Devuelvelist de strdict columna → valor
La cabeceraEs una fila más: la consumís con next(r)La consume sola, queda en .fieldnames
Si faltan camposLa lista viene más cortaLos que faltan quedan en None (o en restval)
Si sobran camposLa lista viene más largaSe juntan en la clave None (o en restkey)
CuándoArchivos sin cabecera, o cuando importa la posiciónCasi siempre

Ninguna de las dos irregularidades levanta excepción. Si el archivo puede venir mal formado, hay que buscarlas: restkey y restval les ponen un nombre para poder preguntar por ellas.

El ciclo completo

EjemploEl ciclo completo: leer, validar, escribir dos archivos
import csv
import tempfile
from pathlib import Path

NULOS = {"", "-", "N/A", "NaN", "null", "NA"}
SALIDA = ["isbn", "titulo", "autor", "anio", "precio"]
RECHAZOS = ["isbn", "columna", "motivo", "valor"]


def revisar(fila, vistos):
    """Devuelve (columna, motivo, valor) del primer defecto, o None."""
    if fila["isbn"] in vistos:
        return ("isbn", "duplicado", fila["isbn"])
    for columna in ("titulo", "autor"):
        if fila[columna].strip() in NULOS:
            return (columna, "ausente", fila[columna])
    if fila["anio"].strip() in NULOS:
        return ("anio", "ausente", fila["anio"])
    try:
        anio = int(fila["anio"])
    except ValueError:
        return ("anio", "no_numerico", fila["anio"])
    if not 1400 <= anio <= 2026:
        return ("anio", "fuera_de_rango", fila["anio"])
    if fila["precio"].strip() in NULOS:
        return ("precio", "ausente", fila["precio"])
    return None


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

buenas, malas, vistos = [], [], set()
for fila in filas:
    defecto = revisar(fila, vistos)
    if defecto is None:
        vistos.add(fila["isbn"])
        buenas.append(fila)
    else:
        columna, motivo, valor = defecto
        malas.append({"isbn": fila["isbn"], "columna": columna,
                      "motivo": motivo, "valor": valor})

carpeta = Path(tempfile.mkdtemp())
for nombre, columnas, datos in (("libros_validos.csv", SALIDA, buenas),
                                ("libros_rechazados.csv", RECHAZOS, malas)):
    with open(carpeta / nombre, "w", encoding="utf-8", newline="") as f:
        escritor = csv.DictWriter(f, fieldnames=columnas, extrasaction="ignore")
        escritor.writeheader()
        escritor.writerows(datos)

print("entrada :", len(filas), "filas")
print("validos :", len(buenas))
print("rechazos:", len(malas))
print("suma    :", len(buenas) + len(malas), "== entrada:",
      len(buenas) + len(malas) == len(filas))

print()
print(Path(carpeta / "libros_rechazados.csv").read_text(encoding="utf-8"))

print("Ese assert de la suma es la invariante mas util de todas: si no da,")
print("hay una fila que no fue ni aceptada ni rechazada, y se perdio callada.")
entrada : 50 filas
validos : 44
rechazos: 6
suma    : 50 == entrada: True

isbn,columna,motivo,valor
978-2045-006,precio,ausente,
978-3226-020,anio,fuera_de_rango,2199
978-8634-027,anio,ausente,
978-1000-001,isbn,duplicado,978-1000-001
978-6834-041,autor,ausente,
978-4222-048,precio,ausente,NaN

Ese assert de la suma es la invariante mas util de todas: si no da,
hay una fila que no fue ni aceptada ni rechazada, y se perdio callada.

Esta es la forma que van a tener casi todos los ejercicios de la unidad: leer con DictReader, decidir fila por fila, y escribir dos archivos - lo válido y lo rechazado con su motivo. Fijate en tres detalles que no son adorno:

Dónde se rompe

(sin error) el CSV se abre en Excel con una fila vacía entre cada dos

Causa: falta newline="" en el open de escritura.

Arreglo: open(ruta, "w", encoding="utf-8", newline="").

ValueError: dict contains fields not in fieldnames: 'sobrante'

Causa: el diccionario que le pasás al DictWriter tiene más claves que las declaradas.

Arreglo: extrasaction="ignore", o armar el diccionario de salida con solo las columnas que van.

TypeError: '>' not supported between instances of 'str' and 'int'

Causa: comparaste con un número algo que vino del CSV, y del CSV todo viene como texto.

Arreglo: convertir primero, con su try/except ValueError. "9" > "10" además es True: comparar textos ordena alfabéticamente.

KeyError: 'nombre'

Causa: la cabecera trae un espacio (' nombre') o un BOM pegado a la primera columna ('isbn'), típico de un CSV exportado de Excel.

Arreglo: mirar lector.fieldnames. Para el BOM, encoding="utf-8-sig".

_csv.Error: field larger than field limit

Causa: una comilla sin cerrar hace que el módulo se coma el resto del archivo como un solo campo.

Arreglo: mirar el archivo crudo cerca de esa línea. El defecto está en los datos, no en tu código.

Recetario

El módulo csv
ExpresiónQué haceCuándo
open(r, encoding="utf-8", newline="")La apertura correctaLeer y escribir CSV, siempre
csv.DictReader(f)Filas como dictCasi siempre
lector.fieldnamesLos nombres de columna leídosPrimer diagnóstico ante un KeyError
csv.reader(f)Filas como listSin cabecera, o si importa la posición
next(lector)Consume una filaSaltear la cabecera con reader
DictReader(f, restval="", restkey="extra")Nombra lo que falta y lo que sobraArchivos que pueden venir mal formados
csv.DictWriter(f, fieldnames=COLS)Escritor por nombreAl escribir
escritor.writeheader()Escribe la cabeceraAntes de las filas; se olvida seguido
escritor.writerows(filas)Escribe todasEn vez de un for
extrasaction="ignore"Tolera claves de másCuando la fila trae datos de trabajo
delimiter=";"Otro separadorCSV «europeo» (lección 7.1)
encoding="utf-8-sig"Descarta el BOM al leerCSV exportado de Excel

Ponete a prueba

1. Leyendo con csv.DictReader, ¿de qué tipo es fila["anio"] si en el archivo dice 2003?Nivel 1 · Recordar
2. ¿Para qué se pasa newline="" al open de un CSV?Nivel 1 · Recordar
3. El archivo trae la línea 3,"Mar del Plata, BA",2026. ¿Qué devuelve csv.reader y qué split(",")?Nivel 2 · Predecir la salida
4. Una fila del archivo tiene 4 campos y la cabecera declara 5. ¿Qué hace DictReader?Nivel 2 · Predecir la salida
5. Querés escribir 40 filas que son dicts con las claves isbn, titulo, motivo_interno, pero el archivo debe tener solo isbn y titulo. Las cuatro corren: ¿cuál conviene?Nivel 3 · Elegir entre dos casi correctas
6. Un CSV exportado de Excel se lee bien, pero fila["isbn"] da KeyError y fila["titulo"] funciona. ¿Qué pasó?Nivel 3 · Elegir entre dos casi correctas
7. Este filtro sobre inventario_productos.csv busca los productos con más de 50 unidades. Corre sin error, devuelve 18 filas, y sin embargo deja afuera los 7 productos con más stock (103, 108, 112, 113, 114 y 117 unidades). ¿Por qué?
for fila in csv.DictReader(f):
    if fila["cantidad"] > "50":
        con_stock.append(fila)
Nivel 4 · Encontrar el defecto

Prácticas de esta subunidad