CSV y JSON en Python
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.
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.
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 condatetime.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')
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.
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
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 Pythonjson.dumps(objeto)→ serializa un objeto Python a string JSONjson.load(fichero)→ lee un fichero abierto y devuelve un objeto Pythonjson.dump(objeto, fichero)→ serializa y escribe directamente en un fichero
La conversión de tipos entre JSON y Python es directa:
| JSON | Python | Ejemplo |
|---|---|---|
object | dict | {"clave": "valor"} |
array | list | [1, 2, 3] |
string | str | "texto" |
number (int) | int | 42 |
number (real) | float | 3.14 |
true / false | True / False | true |
null | None | null |
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)
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).
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.
❓ Preguntas frecuentes
❓ Preguntas frecuentes sobre CSV y JSON en Python
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre CSV y JSON en Python? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!