CSV y JSON en Python

📅 Actualizado en marzo 2026 📊 Nivel: Intermedio ⏱️ 16 min de lectura

CSV y JSON son los dos formatos que mueven datos entre sistemas. Una API que devuelve resultados, una exportación de Excel, la configuración de una aplicación, los logs de un servidor estructurado, un dataset de Kaggle... todo eso es CSV o JSON. Dominar los módulos csv y json de la biblioteca estándar de Python no es opcional: es una habilidad de día uno en cualquier trabajo real con datos.

Medallón de Julio César, general y dictador romano
Divide et impera
«Divide y vencerás»
Julio César · General y dictador romano · 100 a.C. – 44 a.C.
La esencia de CSV y JSON es exactamente esta: tomar información compleja y heterogénea y dividirla en unidades pequeñas, estructuradas e intercambiables. Una tabla de diez mil clientes se convierte en diez mil filas idénticas. Un árbol de configuración con docenas de parámetros se convierte en un objeto JSON con claves predecibles. El programa que procesa los datos no necesita entender el todo; solo necesita saber cómo leer una unidad. Dividir para conquistar.

El idioma del intercambio de datos

Antes de abrir el editor, vale la pena entender por qué existen estos dos formatos y cuándo se usa cada uno. No son competidores directos; resuelven problemas distintos.

CSV (Comma-Separated Values) es la forma más vieja y universal de serializar datos tabulares. Una hoja de cálculo, un listado de registros, los resultados de una consulta SQL: todo eso cabe perfectamente en un CSV. La estructura es siempre la misma: primera línea con nombres de columnas, resto de líneas con datos. Plano, predecible, compatible con cualquier herramienta del planeta.

JSON (JavaScript Object Notation) nació para el intercambio de datos en la web y ganó la guerra contra XML en la segunda mitad de los años 2000. Es el formato nativo de las APIs REST. Soporta jerarquías, arrays, tipos de datos básicos y anidamiento sin límite. No es tan eficiente como CSV para datos puramente tabulares, pero es infinitamente más flexible.

Piensa en la diferencia así: un fichero de tarjetas de índice de biblioteca es un CSV. Cada tarjeta tiene exactamente los mismos campos —autor, título, año, signatura— en el mismo orden y con el mismo formato. Las muñecas matrioska rusas son un JSON: cada objeto puede contener otro objeto dentro, que a su vez puede contener otro, con la profundidad que necesites.

Fichero de tarjetas de índice con pestañas organizadas alfabéticamente en un cajón
Un fichero de tarjetas de índice: cada tarjeta tiene exactamente los mismos campos en el mismo orden. Eso es CSV: datos tabulares donde todas las filas comparten la misma estructura. csv.DictReader te da acceso a cada tarjeta (fila) con sus campos nombrados. Fuente: Pexels (licencia libre).

Los dos módulos que vas a usar —csv y json— forman parte de la biblioteca estándar de Python. No necesitas instalar nada.

Leer CSV con csv.reader

El módulo csv existe porque leer CSV a mano es más complicado de lo que parece. ¿Un campo que contiene comas? Se encierra entre comillas. ¿Un campo que contiene comillas? Se escapan. ¿Saltos de línea dentro de un campo? También son válidos. Intentar parsear CSV con split(',') funciona el 80% del tiempo y falla exactamente cuando más lo necesitas. El módulo csv lo hace bien siempre.

La función más básica es csv.reader:

import csv

with open('usuarios.csv', 'r', encoding='utf-8') as f:
    reader = csv.reader(f)
    cabecera = next(reader)          # consume la primera fila (nombres de columna)
    for fila in reader:
        print(fila)                  # cada fila es una lista de strings
        # ['Ana', 'Madrid', '32']
        nombre = fila[0]             # acceso por índice (frágil)
        edad   = int(fila[2])        # todo llega como string: convertir manualmente

Hay dos cosas que debes saber desde el principio sobre csv.reader:

  • Todo llega como string. Los números, las fechas, los booleanos: todo es texto. Si quieres un entero, haz int(fila[2]). Si quieres una fecha, parsea con datetime.strptime().
  • El acceso por índice es frágil. Si alguien reordena las columnas del CSV, tu código se rompe silenciosamente. La solución es DictReader, que veremos a continuación.

El problema del delimitador

El nombre "CSV" da a entender que el separador es siempre una coma, pero en la práctica no es así. Excel en España y otros países con configuración regional de coma decimal genera ficheros con punto y coma como separador. Si recibes un CSV de una empresa española y ves que los datos se mezclan en una sola columna, ese es el motivo:

# CSV con punto y coma (típico de Excel en España)
# nombre;ciudad;edad
# Ana;Madrid;32

reader = csv.reader(f, delimiter=';')

# CSV con tabuladores (formato TSV)
reader = csv.reader(f, delimiter='\t')
💡 Detectar el separador automáticamente: csv.Sniffer().sniff(f.read(1024)) analiza los primeros bytes del fichero e intenta deducir el delimitador y el estilo de comillas. Útil cuando recibes ficheros de origen desconocido.

DictReader: cada fila como diccionario

csv.DictReader es la forma recomendada de trabajar con CSV en Python moderno. Transforma cada fila en un diccionario donde las claves son los nombres de columna tomados de la primera fila del fichero. El código resultante es más legible, más robusto y se rompe menos cuando el CSV cambia:

import csv

with open('usuarios.csv', 'r', encoding='utf-8', newline='') as f:
    reader = csv.DictReader(f)
    for fila in reader:
        # fila es un dict: {'nombre': 'Ana', 'ciudad': 'Madrid', 'edad': '32'}
        nombre = fila['nombre']
        edad   = int(fila['edad'])    # convertir al tipo correcto
        print(f'{nombre} ({edad} años) vive en {fila["ciudad"]}')

Si el CSV no tiene cabecera (a veces pasa con exports automáticos), puedes especificar los nombres tú mismo:

reader = csv.DictReader(f, fieldnames=['nombre', 'ciudad', 'edad'])

Cargar todo en una lista

El patrón más habitual en scripts de procesamiento de datos es cargar el CSV completo en memoria como lista de dicts:

with open('usuarios.csv', 'r', encoding='utf-8', newline='') as f:
    registros = list(csv.DictReader(f))
# registros es ahora una lista de dicts, lista para filtrar, ordenar, transformar
registros_madrid = [r for r in registros if r['ciudad'] == 'Madrid']

Esto solo tiene sentido cuando el fichero cabe en memoria. Para CSVs de gigabytes, procesa fila a fila sin convertir a lista.

Diagrama del flujo de datos entre CSV, Python y JSON usando DictReader, DictWriter, json.load y json.dump
Flujo de datos entre los tres formatos: el módulo csv traduce entre archivos de texto plano con columnas y listas de diccionarios Python; el módulo json traduce entre objetos JSON y estructuras nativas Python. Las flechas verdes indican lectura y las ámbar escritura. Infografía: Ciberaula.

Escribir CSV: writer y DictWriter

Para escribir CSV hay dos opciones paralelas a las de lectura: csv.writer (escribe listas) y csv.DictWriter (escribe dicts). La segunda es la recomendada porque hace el código más explícito y evita que las columnas se desordenen.

csv.writer — escribir listas

import csv

datos = [
    ['Ana', 'Madrid', 32],
    ['Luis', 'Barcelona', 28],
    ['Sara', 'Sevilla', 41],
]

with open('salida.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.writer(f)
    writer.writerow(['nombre', 'ciudad', 'edad'])   # cabecera
    writer.writerows(datos)                          # todas las filas de una vez
⚠️ El parámetro newline='' es obligatorio al escribir. Sin él, Python añade su propio \r\n encima del que ya gestiona el módulo csv, resultando en una línea en blanco extra entre cada fila del fichero. Es un bug silencioso que solo se manifiesta en Windows y que puede arruinar la importación del CSV resultante.

csv.DictWriter — la forma recomendada

import csv

registros = [
    {'nombre': 'Ana',   'ciudad': 'Madrid',    'edad': 32},
    {'nombre': 'Luis',  'ciudad': 'Barcelona', 'edad': 28},
    {'nombre': 'Sara',  'ciudad': 'Sevilla',   'edad': 41},
]

campos = ['nombre', 'ciudad', 'edad']

with open('salida.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=campos)
    writer.writeheader()       # escribe la línea de cabecera
    writer.writerows(registros)

DictWriter también acepta el parámetro extrasaction='ignore', útil cuando tus dicts tienen más claves que columnas quieres en el CSV:

writer = csv.DictWriter(f, fieldnames=['nombre', 'ciudad'], extrasaction='ignore')
# Ahora puedes pasarle dicts que incluyan 'edad' sin que lance KeyError

JSON en Python: loads y dumps

El módulo json tiene cuatro funciones principales que conviene recordar con un truco: las que terminan en s trabajan con strings, las que no terminan en s trabajan con ficheros.

  • json.loads(cadena) → parsea un string JSON y devuelve un objeto Python
  • json.dumps(objeto) → serializa un objeto Python a string JSON
  • json.load(fichero) → lee un fichero abierto y devuelve un objeto Python
  • json.dump(objeto, fichero) → serializa y escribe directamente en un fichero

La conversión de tipos entre JSON y Python es directa:

JSONPythonEjemplo
objectdict{"clave": "valor"}
arraylist[1, 2, 3]
stringstr"texto"
number (int)int42
number (real)float3.14
true / falseTrue / Falsetrue
nullNonenull

En la práctica, con json.loads y json.dumps:

import json

# String JSON → objeto Python
texto_json = '{"nombre": "Ana", "edad": 32, "activo": true, "tags": ["py", "web"]}'
datos = json.loads(texto_json)
print(datos['nombre'])    # → Ana
print(type(datos['activo']))  # → <class 'bool'>  (true JSON = True Python)

# Objeto Python → string JSON
persona = {'nombre': 'Luis', 'ciudad': 'Barcelona', 'edad': 28}
cadena = json.dumps(persona)
print(cadena)
# → '{"nombre": "Luis", "ciudad": "Barcelona", "edad": 28}'

Opciones de json.dumps

Por defecto, json.dumps produce JSON compacto en una sola línea, con los caracteres no ASCII escapados. Para trabajo cotidiano, casi siempre querrás cambiar eso:

import json

config = {
    'host': 'localhost',
    'puerto': 5432,
    'base_datos': 'producción',
    'opciones': {'timeout': 30, 'ssl': True}
}

# Para almacenar o mostrar: con sangría y caracteres reales (ñ, tildes...)
bonito = json.dumps(config, indent=2, ensure_ascii=False)
print(bonito)
# {
#   "host": "localhost",
#   "puerto": 5432,
#   "base_datos": "producción",     ← acento preservado
#   "opciones": {
#     "timeout": 30,
#     "ssl": true
#   }
# }

# Para comparaciones o git: claves ordenadas
ordenado = json.dumps(config, indent=2, ensure_ascii=False, sort_keys=True)
Muñecas matrioska rusas de colores apiladas, mostrando el principio de objetos anidados
Las muñecas matrioska: cada muñeca se abre para revelar otra dentro, que a su vez contiene otra. JSON funciona exactamente igual: un objeto puede contener arrays de objetos, que a su vez tienen sus propios objetos anidados, con la profundidad que necesites. Lo que en un CSV sería imposible (columnas dentro de columnas), en JSON es trivial. Fuente: Pexels (licencia libre).

Leer y escribir ficheros JSON

En el 90% de los casos trabajarás con ficheros, no con strings en memoria. Para eso están json.load y json.dump:

import json

# ── ESCRIBIR ───────────────────────────────────────────────────
config = {
    'version': '2.1',
    'base_datos': {
        'host': 'localhost',
        'puerto': 5432,
        'nombre': 'mi_app'
    },
    'debug': False,
    'admins': ['ana@ejemplo.com', 'luis@ejemplo.com']
}

with open('config.json', 'w', encoding='utf-8') as f:
    json.dump(config, f, indent=2, ensure_ascii=False)


# ── LEER ───────────────────────────────────────────────────────
with open('config.json', 'r', encoding='utf-8') as f:
    config_cargado = json.load(f)

print(config_cargado['base_datos']['host'])   # → localhost
print(config_cargado['admins'][0])            # → ana@ejemplo.com

La combinación json.dump(datos, f, indent=2, ensure_ascii=False) es la que deberías usar casi siempre al escribir ficheros. Sin ensure_ascii=False, los caracteres como la ñ o las tildes se guardan como secuencias de escape Unicode (\u00f1, \u00e9), que son válidas pero hacen el fichero ilegible para humanos.

Manejo de errores

import json

try:
    with open('config.json', 'r', encoding='utf-8') as f:
        config = json.load(f)
except FileNotFoundError:
    print('No existe config.json. Usando configuración por defecto.')
    config = {}
except json.JSONDecodeError as e:
    print(f'El fichero config.json está mal formado: {e}')
    config = {}

El JSONDecodeError (subclase de ValueError) ocurre cuando el contenido del fichero no es JSON válido. Los casos más frecuentes son: comas al final de la última propiedad de un objeto, comillas simples en lugar de dobles, o comentarios (el estándar JSON no admite comentarios).

Ficha de referencia rápida de los módulos csv y json: DictReader, DictWriter, json.load, json.dump con ejemplos de código
Referencia rápida: las cuatro operaciones fundamentales de los módulos csv y json, con sus patrones de uso y parámetros clave. Ficha de referencia: Ciberaula.

Serialización personalizada

El módulo json solo sabe serializar los tipos nativos de JSON: dict, list, str, int, float, bool y None. Cualquier otro tipo de Python —datetime, set, Decimal, objetos personalizados— lanza TypeError al intentar serializarlo.

El caso más frecuente con diferencia es el de las fechas:

from datetime import datetime
import json

evento = {
    'titulo': 'Conferencia Python',
    'fecha': datetime(2026, 6, 15, 10, 0)
}

json.dumps(evento)
# TypeError: Object of type datetime is not JSON serializable

Solución 1: convertir antes de serializar

La más simple: convierte los tipos no serializables antes de llamar a json.dumps:

evento_serializable = {
    'titulo': evento['titulo'],
    'fecha': evento['fecha'].isoformat()   # '2026-06-15T10:00:00'
}
json.dumps(evento_serializable)   # funciona

Solución 2: parámetro default=

Si tienes estructuras complejas con muchos tipos no serializables, puedes pasar una función default= que se llama cuando json encuentra un tipo que no sabe manejar:

from datetime import datetime, date
import decimal

def json_serial(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    if isinstance(obj, decimal.Decimal):
        return float(obj)
    raise TypeError(f'Tipo no serializable: {type(obj).__name__}')

datos = {
    'nombre': 'Evento',
    'fecha': datetime.now(),
    'precio': decimal.Decimal('29.99')
}

resultado = json.dumps(datos, default=json_serial, indent=2, ensure_ascii=False)

Solución 3: JSONEncoder personalizado

Para proyectos grandes donde la serialización custom es frecuente, la forma más limpia es heredar de json.JSONEncoder:

import json
from datetime import datetime

class MiEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)

resultado = json.dumps(datos, cls=MiEncoder, indent=2)

CSV vs JSON: cuándo elegir cada uno

Esta pregunta tiene respuesta clara si piensas en la estructura de los datos, no en la familiaridad con el formato.

Criterio CSV JSON
Estructura de datos Tabular: todas las filas tienen los mismos campos Jerárquica: campos variables, objetos anidados, arrays
Tamaño Mejor para volúmenes masivos (millones de filas) Razonable para volúmenes medianos; verboso en escala masiva
Legibilidad humana Legible con cualquier editor o Excel Más legible con estructura compleja; con indent=2, excelente
APIs web Raro; solo en endpoints específicos de export Estándar universal de facto para REST APIs
Configuraciones No adecuado (datos no tabulares) Habitual; YAML es competidor pero JSON es más universal
Compatibilidad Excel ✅ Nativa: doble clic y abre ❌ Requiere plugin o Power Query
Tipos de datos Todo como string; conversión manual obligatoria Soporta int, float, bool, null nativamente

Un criterio rápido: si puedes representar los datos en una hoja de cálculo sin perder información, CSV. Si necesitarías varias hojas o celdas con listas dentro, JSON.

💡 JSON Lines (JSONL): Para volúmenes grandes con estructura JSON, considera el formato JSONL: un fichero donde cada línea es un objeto JSON independiente. Combina la eficiencia de procesamiento línea a línea del CSV con la flexibilidad estructural de JSON. Muy usado en data pipelines modernos y como formato de log estructurado.

❓ Preguntas frecuentes

❓ Preguntas frecuentes sobre CSV y JSON en Python

Las dudas más comunes respondidas de forma clara y directa.

csv.reader devuelve cada fila como una lista de strings sin ningún contexto sobre qué significa cada valor. csv.DictReader, en cambio, usa la primera fila del fichero como cabecera y devuelve cada fila siguiente como un diccionario, donde las claves son los nombres de columna. Así puedes acceder a los datos por nombre (fila['nombre']) en lugar de por índice (fila[0]), lo que hace el código mucho más legible y resistente a cambios en el orden de las columnas.
En España, y otros países con configuración regional de coma decimal, Excel usa el punto y coma como separador para evitar confusión con los números decimales que emplean coma. Si abres un CSV generado con Excel en España, necesitarás especificar delimiter=';' al crear el reader. Esto es solo una convención regional; el estándar CSV (RFC 4180) usa coma. Si controlas el origen del fichero, genera siempre con coma y punto decimal para máxima compatibilidad.
Usa CSV cuando tus datos son tabulares (todas las filas tienen los mismos campos), cuando necesitas compatibilidad directa con Excel o herramientas de análisis, o cuando el volumen es muy grande y necesitas procesar línea a línea. Usa JSON cuando tus datos tienen estructura variable entre registros, cuando necesitas jerarquías o arrays anidados, cuando intercambias datos con APIs web, o cuando el formato debe ser legible por humanos directamente. No hay un ganador universal: la elección depende de la estructura de los datos y del ecosistema de herramientas que los van a consumir.
El estándar JSON solo define seis tipos de datos: string, number, boolean, null, array y object. Las fechas no existen en ese estándar, así que Python no sabe cómo representarlas y lanza TypeError. La solución más limpia es convertir los datetime a string antes de serializar: mi_fecha.isoformat() produce "2026-03-07T14:00:00" (formato ISO 8601). Si necesitas deserializar también, parsea de vuelta con datetime.fromisoformat().
El módulo json estándar carga el fichero completo antes de parsear. Para JSONs de varios gigabytes, la solución habitual es trabajar con JSON Lines (JSONL): un formato donde cada línea es un objeto JSON independiente. Puedes leer el fichero línea a línea con un for sobre el fichero abierto y hacer json.loads() de cada línea. Para datasets realmente grandes, la biblioteca ijson ofrece un parser de streaming que procesa el JSON en chunks. Si controlas el origen, genera JSONL en lugar de un array JSON gigante.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre CSV y JSON en Python? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!