JSON y CSV
Leer y escribir tablas entrecomilladas con el módulo csv, por qué hace falta `newline=""`, guardar datos estructurados con json y qué tipos cambian en el trayecto.
¿Cómo quieres aprender?
El problema que vamos a resolver
Desde el capítulo 23 partimos los archivos con .split(","), y funcionaba. Ahora, un archivo donde un campo contiene una coma dentro de él:
name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(line.strip().split(","))['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', '"large', ' sturdy"', '850.0']La última fila salió con cuatro campos en lugar de tres, y las comillas pasaron a formar parte de los datos.
El entrecomillado no es un error: es la regla de CSV, y existe justo para esta situación. El error está en nuestro código de lectura. Y escribir esa regla tú mismo significa manejar comillas dentro de comillas y saltos de línea dentro de campos; un trabajo pequeño que se va convirtiendo en uno grande.
Ese trabajo ya está hecho.
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.reader(fh):
print(row)['name', 'note', 'price']
['pen', 'blue ink', '15.0']
['bag', 'large, sturdy', '850.0']Este capítulo trata de dos formatos — CSV para tablas, JSON para estructura — y Python ya trae un módulo para cada uno.
Al terminar este capítulo podrás
- Leer con
csv.readerycsv.DictReader, y escribir concsv.writer - Explicar por qué se escribe
newline="" - Explicar qué separa
json.dumps/loadsdejson.dump/load - Explicar qué tipos de Python vuelven desde JSON y cuáles no
- Elegir entre CSV y JSON
- Leer un
JSONDecodeErrory encontrar el problema
Requisitos previos: Excepciones.
Leer CSV
csv.reader da cada fila como una lista. Pero entonces hay que recordar las posiciones — ¿qué había en row[2]?
csv.DictReader trata la primera fila como encabezados y convierte cada fila en un diccionario:
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
for row in csv.DictReader(fh):
print(row["name"], "-", row["price"], type(row["price"]))pen - 15.0 <class 'str'>
bag - 850.0 <class 'str'>Dos cosas.
El acceso es por nombre, así que reordenar las columnas no rompe el código. DictReader casi siempre es la mejor opción.
Pero price sigue siendo texto. El módulo csv separa campos, no los convierte — nada en un archivo CSV dice qué columna es un número. Llamar a float() e int() te toca a ti, exactamente como en el capítulo 23.
Escribir CSV
import csv
rows = [["pen", "blue ink", 15.0], ["bag", "large, sturdy", 850.0]]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.writer(fh)
writer.writerow(["name", "note", "price"])
writer.writerows(rows)name,note,price
pen,blue ink,15.0
bag,"large, sturdy",850.0Fíjate en que large, sturdy se entrecomilló por sí solo — y blue ink no, porque no hacía falta. Lo que nosotros no podíamos leer, tampoco nos hizo falta escribirlo.
Para una lista de diccionarios, DictWriter:
import csv
rows = [
{"name": "pen", "price": 15.0},
{"name": "bag", "price": 850.0},
]
with open("out.csv", "w", encoding="utf-8", newline="") as fh:
writer = csv.DictWriter(fh, fieldnames=["name", "price"])
writer.writeheader()
writer.writerows(rows)name,price
pen,15.0
bag,850.0fieldnames también fija el orden de las columnas, y sin writeheader() no hay fila de encabezados.
¿Por quénewline=""? El módulocsvdecide por sí mismo qué termina una fila (\r\n). Si el archivo se abre de forma normal, en Windows Python traduce los finales de línea una segunda vez, dejando una línea en blanco después de cada fila. En Linux y macOS esa segunda traducción no ocurre, así que ahí no se ve diferencia.newline=""desactiva esa traducción, de modo que el código se comporta igual en todas partes. Escríbelo también al leer — así los saltos de línea dentro de campos entrecomillados se leen correctamente.
JSON — una estructura que una tabla no puede contener
CSV es una tabla: filas y columnas, todo plano. Pero, ¿y si un pedido contiene una lista de líneas, y cada línea contiene una lista de etiquetas? Una tabla no puede contener eso.
import json
order = {"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": True, "note": None}
print(json.dumps(order)){"name": "pen", "price": 15.0, "tags": ["ink", "blue"], "stocked": true, "note": null}json.dumps convierte un objeto de Python en texto. La forma se parece a la de Python, con dos diferencias: True se convirtió en true y None en null. JSON es un lenguaje aparte que comparten muchos lenguajes de programación, así que tiene su propia ortografía.
Para algo que vaya a leer una persona:
import json
order = {"name": "pen", "tags": ["ink", "blue"]}
print(json.dumps(order, indent=2)){
"name": "pen",
"tags": [
"ink",
"blue"
]
}Y para traerlo de vuelta:
import json
text = '{"name": "pen", "price": 15.0, "stocked": true, "note": null}'
order = json.loads(text)
print(order)
print(type(order["price"]), type(order["stocked"]), order["note"] is None){'name': 'pen', 'price': 15.0, 'stocked': True, 'note': None}
<class 'float'> <class 'bool'> TrueEsta es la diferencia más grande con CSV. price volvió como número y stocked como True, sin llamar a ninguna conversión. JSON trae los tipos; CSV no.
Los cuatro nombres son fáciles de distinguir: dumps/loads trabajan con texto, dump/load trabajan con archivos. La s final es de string.
import json
from pathlib import Path
order = {"name": "pen", "price": 15.0}
Path("order.json").write_text(json.dumps(order, indent=2) + "\n", encoding="utf-8")
with open("order.json", "r", encoding="utf-8") as fh:
back = json.load(fh)
print(back){'name': 'pen', 'price': 15.0}Lo que JSON no devuelve
No todo objeto de Python puede entrar en JSON, y no todo lo que entra vuelve igual.
import json
data = {"pair": (1, 2), "numbers": {3, 4}}
print(json.dumps({"pair": data["pair"]}))
print(json.dumps(data)){"pair": [1, 2]}
TypeError: Object of type set is not JSON serializable(Antes de la línea del error Python también imprime unas líneas de rastreo; en este capítulo solo se muestra la última.)
Una tupla se convierte en lista en silencio. JSON no tiene tuplas, así que al escribir no protesta — pero lo que vuelve es una lista. La promesa del capítulo 14 de que no cambiará se pierde en el camino.
Un conjunto ni siquiera entra, y ese es el mejor comportamiento — detenerse es mejor que equivocarse en silencio. Si lo necesitas, envía sorted(...) como lista.
Otro más, que muerde con más fuerza:
import json
data = {1: "pen", 2: "bag"}
text = json.dumps(data)
print(text)
print(json.loads(text)){"1": "pen", "2": "bag"}
{'1': 'pen', '2': 'bag'}Las claves de JSON siempre son cadenas. Si le das claves numéricas, se convierten en cadenas en silencio y vuelven como cadenas. Así que antes funcionaba data[1], y después hace falta data["1"].
La lista de recordar es corta: diccionarios, listas, cadenas, números, True/False y None son seguros. Las tuplas cambian. Los conjuntos, las fechas y tus propios objetos no entran.
JSON es estricto
import json
print(json.loads("{'name': 'pen'}"))json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)En Python las comillas simples y las dobles son iguales. En JSON no — solo comillas dobles, y sin coma después del último elemento.
El mensaje da una línea y una columna, así que se puede encontrar el sitio incluso en un archivo grande. Y JSONDecodeError es en realidad un ValueError — así que except ValueError lo captura también, como dice la regla de la familia del capítulo anterior.
Cuál y cuándo
Elige CSV cuando los datos son una tabla plana y una persona los abrirá en una hoja de cálculo. Con muchas filas el archivo también se queda más pequeño.
Elige JSON cuando hay estructura — listas dentro, diccionarios dentro —, o cuando los tipos tienen que volver intactos. Los archivos de ajustes y las APIs web casi siempre son JSON.
Un ejemplo completo
items.csv:
name,note,price,quantity
pen,blue ink,15.0,3
bag,"large, sturdy",850.0,1
ink,,120.0,2
clip,broken,,4main.py:
"""Read a CSV of items, total it, and write the result as JSON."""
import csv
import json
from pathlib import Path
TAX_RATE = 0.15
def read_items(path):
"""Returns the usable rows and the complaints. csv handles the quoting."""
items = []
problems = []
with open(path, "r", encoding="utf-8", newline="") as fh:
for number, row in enumerate(csv.DictReader(fh), start=2):
try:
items.append({
"name": row["name"],
"note": row["note"],
"amount": round(
float(row["price"]) * int(row["quantity"]) * (1 + TAX_RATE), 2),
})
except ValueError as err:
problems.append(f"line {number}: {err}")
return items, problems
def main():
items, problems = read_items(Path("items.csv"))
summary = {
"items": items,
"total": round(sum(item["amount"] for item in items), 2),
"skipped": problems,
}
Path("summary.json").write_text(
json.dumps(summary, indent=2) + "\n", encoding="utf-8")
print(Path("summary.json").read_text(encoding="utf-8"), end="")
if __name__ == "__main__":
main(){
"items": [
{
"name": "pen",
"note": "blue ink",
"amount": 51.75
},
{
"name": "bag",
"note": "large, sturdy",
"amount": 977.5
},
{
"name": "ink",
"note": "",
"amount": 276.0
}
],
"total": 1305.25,
"skipped": [
"line 5: could not convert string to float: ''"
]
}Cuatro cosas que merece la pena ver.
Entra CSV y sale JSON, y eso se deduce de lo que es cada formato. La entrada es una tabla plana, que una hoja de cálculo también puede abrir. La salida no es plana — una lista de cosas, cada una un diccionario, con una segunda lista de quejas al lado. Eso no se podría haber escrito como CSV.
large, sturdy sobrevive todo el viaje. csv lo leyó correctamente y json lo escribió correctamente. El problema con el que empezó este capítulo ni se ve.
El note vacío y el price vacío son ambos texto vacío, y terminan de forma distinta. Un campo CSV vacío es "", no None. note se queda como texto y no causa nada; float("") lanza un ValueError, así que la fila clip se omite con una queja. CSV no tiene "faltante", solo "vacío" — y qué signifique eso lo decides tú.
El start=2 no es arbitrario. DictReader consume la primera fila como encabezados, así que la primera fila de datos que da es la segunda línea del archivo. Un mensaje que alguien va a comparar con el archivo tiene que contar como cuenta el archivo.
Cuando falla
Una línea en blanco después de cada fila de mi CSV (en Windows) No se pasó newline="". Pásalo al escribir.
KeyError: 'price' — pero la columna está ahí mismo El encabezado no coincide, en la escritura o en los espacios. Imprime reader.fieldnames para ver qué leyó Python de verdad.
Un TypeError al hacer aritmética con los números csv lo da todo como texto. Hay que llamar a float() o a int().
La primera fila se está tratando como datos Estás usando csv.reader, que no sabe nada de encabezados. Usa DictReader, o quita tú mismo la primera fila.
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes Comillas simples, o una coma sobrante. JSON no es Python.
TypeError: Object of type set is not JSON serializable Se pasó un conjunto, una fecha u un objeto propio. Conviértelo primero en lista o en texto.
Mis tuplas volvieron como listas Como debe ser — JSON no tiene tuplas. Llama tú mismo a tuple(...) después si necesitas una.
data[1] funcionaba, y después de un viaje de ida y vuelta lanza KeyError Las claves de JSON siempre son cadenas. Ahora es data["1"].
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Un archivo de tres columnas, partido a mano. ¿Cuántos campos salen?
# items.csv
# name,note,price
# pen,blue ink,15.0
# bag,"large, sturdy",850.0
with open("items.csv", "r", encoding="utf-8") as fh:
for line in fh:
print(len(line.strip().split(",")))- A3 3 4
- B3 3 3
- C3 3 5
- DUn `ValueError`
DictReader leyó la fila. ¿Qué pasa en la segunda línea?
# items.csv
# name,price
# pen,15
import csv
with open("items.csv", "r", encoding="utf-8", newline="") as fh:
row = next(csv.DictReader(fh))
print(row)
print(row["price"] + 1)- ASe imprime el diccionario, luego `16`
- BSe imprime el diccionario, luego `151`
- CSe imprime el diccionario, luego un `TypeError` — `price` sigue siendo texto
- DUn `KeyError`
A json.dumps se le da un diccionario de Python. ¿Qué se imprime?
import json
print(json.dumps({"name": "pen", "stocked": True, "note": None}))- A{'name': 'pen', 'stocked': True, 'note': None}
- B{"name": "pen", "stocked": True, "note": None}
- C{"name": "pen", "stocked": true}
- D{"name": "pen", "stocked": true, "note": null}
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Crea un books.csv con las columnas title,author,pages,year y al menos seis filas. Pon una coma en al menos un título (para que tenga que ir entrecomillado), deja pages vacío en una fila, y pon letras donde va pages en otra.
Luego escribe un library.py que:
- Lea el archivo con
DictReader, recogiendo una queja con el número de línea por cada fila mala - Construya un resumen por autor — cuántos libros tiene cada uno, y cuántas páginas en total
- Escriba todo lo anterior en
library.jsonconindent=2 - Lea ese archivo de vuelta y compruebe que el total no ha cambiado
Luego, cinco experimentos:
- Usa
line.split(",")en lugar deDictReader. ¿Qué le pasa al título con la coma? - Quita
newline=""al escribir, luego abre el archivo CSV y míralo. En Windows verás una línea en blanco después de cada fila; en Linux y macOS no verás diferencia — pero manténnewline=""también ahí, para que el código se comporte igual en todas partes. - Pon un conjunto en el resumen (el conjunto de autores, por ejemplo) e intenta escribir el JSON. Lee el mensaje y luego arréglalo.
- Abre
library.jsona mano, cambia una comilla doble por una simple e intenta leerlo. ¿El mensaje da una línea y una columna? - Usa el año como clave de diccionario —
{2020: [...]}—, luego escríbelo y léelo de vuelta. ¿Con qué tienes que buscarlo ahora?
Esas dos últimas dejan claro el punto de este capítulo: escribir en un archivo es traducir, y toda traducción cambia algo. Saber qué cambia te permite elegir el formato; no saberlo hace que el formato elija por ti.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz