Hola! Vamos por lo que te mandan de verdad
Hasta ahora los datos los escribíamos nosotras dentro del código. En el trabajo real te llegan en un archivo, casi siempre por correo, casi siempre con algún problema 😅
Este capítulo va de abrirlos sin romperlos. Y de la codificación, que es el dolor número uno de trabajar con datos en español y que casi nadie explica bien.
Y dime si te suena esto: ¿abriste alguna vez un archivo y viste á donde tenía que ir una á? Eso tiene nombre, tiene causa y tiene arreglo, y los tres están en este capítulo 🔤
with open, y por qué siempre con with
import tempfile from pathlib import Path # Uso una carpeta temporal para no ensuciar la tuya carpeta = Path(tempfile.mkdtemp()) archivo = carpeta / 'notas.txt' with open(archivo, 'w', encoding='utf-8') as f: f.write('Primera línea\n') f.write('Segunda línea con tildes: áéíóú ñ\n') with open(archivo, encoding='utf-8') as f: contenido = f.read() print(contenido)
Primera línea Segunda línea con tildes: áéíóú ñ
Tres cosas de ahí que valen para siempre:
- 🔒
withcierra el archivo solo, aunque el código de dentro reviente. Sin él tienes que acordarte de cerrarlo, y no te vas a acordar. - ✍️ El modo:
'r'leer (es el que viene por defecto),'w'escribir borrando lo que había,'a'agregar al final. - 🌍
encoding='utf-8'siempre, al leer y al escribir. En un minuto te explico por qué.
Ese 'w' borra sin preguntar, así que ten cuidadito. Si querías
agregar y pusiste 'w', te quedaste sin lo anterior 😬
Para leer línea por línea, que es lo que haces con archivos grandes:
with open(archivo, encoding='utf-8') as f: for numero, linea in enumerate(f, start=1): print(numero, repr(linea))
1 'Primera línea\n' 2 'Segunda línea con tildes: áéíóú ñ\n'
Fíjate en el \n al final de cada línea: viene incluido. Por eso
casi siempre se hace linea.strip() antes de usarla.
Y esta forma de leer tiene una ventaja enorme: no carga el archivo
entero en memoria. Con un archivo de dos gigas, f.read() te
tumba la máquina y el bucle no.
La codificación, o por qué ves "á"
Esta es la parte más importante del capítulo. Presta atención dos minutos y te ahorras años de misterio 💜
Un archivo de texto no guarda letras, guarda números. La codificación es la tabla que dice qué número es cada letra.
Hay dos que te vas a encontrar:
- 🌍 UTF-8. El estándar de hoy. Le entran todos los idiomas. Es lo que usa Python por defecto y lo que debes usar siempre que puedas.
- 🗄️ latin-1 (también llamado ISO-8859-1 o cp1252). El de Windows en español, de toda la vida. Sigue saliendo de sistemas antiguos y de algunos Excel.
Y ahora mira lo que pasa cuando se confunden, que es exactamente lo que has visto mil veces sin saber por qué:
texto = 'Cañete, Trujillo, Ancón' # Alguien lo guardo con latin-1 bytes_latin = texto.encode('latin-1') # Y tu lo lees como si fuera utf-8 print(bytes_latin.decode('utf-8'))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 2: invalid continuation byte
Ese es el error que revienta. Pero hay un caso peor, el que no revienta:
# Guardado en utf-8 y leido como latin-1: no falla, sale mal bytes_utf = texto.encode('utf-8') print(bytes_utf.decode('latin-1'))
Cañete, Trujillo, Ancón
Ahí está 😱 Eso tiene nombre y se llama mojibake. El archivo se lee sin un solo error y las tildes salen convertidas en pares de símbolos raros.
La regla para reconocerlo: si ves "Ã" seguida de otro símbolo, es UTF-8 leído como latin-1. Y se arregla leyendo con la codificación correcta, no reemplazando caracteres a mano.
Cuando no sepas cuál es, este orden funciona casi siempre:
datos = texto.encode('latin-1') for codificacion in ['utf-8', 'latin-1']: try: print(codificacion, '->', datos.decode(codificacion)) break except UnicodeDecodeError: print(codificacion, '-> no')
utf-8 -> no latin-1 -> Cañete, Trujillo, Ancón
Primero UTF-8, y si revienta, latin-1. En ese orden, porque latin-1 casi nunca falla (acepta cualquier byte) y por eso probarlo primero te esconde el problema.
CSV sin pandas
Sí, pandas lo hace más fácil, y aun así vale la pena conocer el módulo
csv:
- Es de la librería estándar
- No carga el archivo entero en memoria
- No se confunde con las comas dentro de un campo
import csv ruta = carpeta / 'ventas.csv' filas = [ ['cliente', 'ciudad', 'monto'], ['C0045', 'Trujillo', '480.37'], ['C0325', 'Lima, Miraflores', '524.90'], ] with open(ruta, 'w', newline='', encoding='utf-8') as f: csv.writer(f).writerows(filas) print(ruta.read_text(encoding='utf-8'))
cliente,ciudad,monto C0045,Trujillo,480.37 C0325,"Lima, Miraflores",524.90
Mira la última línea: como la ciudad tenía una coma dentro, el módulo la puso
entre comillas sola. Si hubieras armado el CSV pegando textos con
','.join(), esa fila habría quedado con cuatro campos en vez de
tres y nadie te habría avisado 🙃
Ese newline='' parece un detalle raro y hace falta: sin él, en
Windows te salen líneas en blanco entre fila y fila.
Para leer, lo mejor es DictReader, que te da cada fila como
diccionario usando la cabecera:
with open(ruta, encoding='utf-8') as f: for fila in csv.DictReader(f): print(fila['ciudad'], '->', float(fila['monto']) * 1.18)
Trujillo -> 566.8366 Lima, Miraflores -> 619.382
Pides por nombre de columna y no por posición, así que si mañana cambian el orden de las columnas tu código sigue funcionando 🌸
Excel
Para leer Excel hace falta instalar openpyxl
(pip install openpyxl). Después es directo:
from openpyxl import Workbook, load_workbook libro = Workbook() hoja = libro.active hoja.title = 'Ventas' hoja.append(['cliente', 'ciudad', 'monto']) hoja.append(['C0045', 'Trujillo', 480.37]) hoja.append(['C0325', 'Lima', 524.90]) ruta_xlsx = carpeta / 'ventas.xlsx' libro.save(ruta_xlsx) leido = load_workbook(ruta_xlsx) print(leido.sheetnames) for fila in leido['Ventas'].iter_rows(min_row=2, values_only=True): print(fila)
['Ventas']
('C0045', 'Trujillo', 480.37)
('C0325', 'Lima', 524.9)
Ese min_row=2 se salta la cabecera y values_only=True
te da los valores en vez de los objetos de celda, que es lo que quieres el 99%
de las veces.
Dos avisos sobre Excel, aprendidos a la mala:
- 🧮 Las fórmulas no vienen calculadas. Por defecto lees la
fórmula (
=A2*1.18) y no su resultado. Conload_workbook(ruta, data_only=True)lees el último valor calculado, que existe solo si el archivo se abrió en Excel alguna vez. - 📅 Las fechas vienen como fechas si la celda tenía formato de fecha, y como texto si no. Siempre revisa el tipo antes de confiar.
JSON
import json ventas = [ {'cliente': 'C0045', 'ciudad': 'Trujillo', 'monto': 480.37}, {'cliente': 'C0325', 'ciudad': 'Ñaña', 'monto': 524.90}, ] ruta_json = carpeta / 'ventas.json' with open(ruta_json, 'w', encoding='utf-8') as f: json.dump(ventas, f, ensure_ascii=False, indent=2) print(ruta_json.read_text(encoding='utf-8'))
[
{
"cliente": "C0045",
"ciudad": "Trujillo",
"monto": 480.37
},
{
"cliente": "C0325",
"ciudad": "Ñaña",
"monto": 524.9
}
]
Ese ensure_ascii=False es el que hace que la ñ se escriba como ñ.
Sin él sale "\u00d1a\u00f1a", que es válido y es ilegible.
Y las cuatro funciones de json, que se confunden siempre:
| Función | Qué hace |
|---|---|
json.load(f) | De archivo a Python |
json.loads(texto) | De texto a Python |
json.dump(obj, f) | De Python a archivo |
json.dumps(obj) | De Python a texto |
La s del final es de "string". Con eso ya no las confundes 😄
Leer el archivo del libro
Vamos a leer el CSV de verdad con el que vamos a trabajar el resto del libro, todavía sin pandas:
from urllib.request import urlopen URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv' with urlopen(URL) as respuesta: texto = respuesta.read().decode('utf-8')
Y si ya lo descargaste, que es lo que vas a hacer casi siempre, se lee igual
pero con open y la ruta de tu computadora:
import csv ARCHIVO = 'ventas-miss-yera.csv' # descargado al lado del cuaderno with open(ARCHIVO, encoding='utf-8') as f: texto = f.read() lineas = texto.splitlines() print('filas (con cabecera):', len(lineas)) print(lineas[0]) lector = csv.DictReader(lineas) primera = next(lector) print(primera['ciudad'], primera['monto'], primera['compro'])
filas (con cabecera): 3038 id_venta,fecha,cliente_id,ciudad,segmento,canal,categoria,unidades,monto,descuento,fecha_ultima_compra,satisfaccion,compro,monto_final_facturado Trujillo 480.37 1
Tres mil treinta y ocho líneas contando la cabecera, o sea 3.037 ventas. Y
fíjate en algo: primera['monto'] devolvió '480.37'
como texto, porque un CSV no guarda tipos. Todo lo que sale de
ahí es texto hasta que tú lo conviertas.
Eso es exactamente lo que pandas te va a hacer solo en el capítulo 12, y ahora ya sabes que no es magia 🌟
Escribir, que es la otra mitad del trabajo
Todo el capítulo ha ido de leer. Pero un análisis que no sale a ningún sitio no le sirve a nadie, y casi siempre sale a un CSV que alguien va a abrir en Excel 📤
import csv import os import tempfile carpeta = tempfile.mkdtemp() ruta = os.path.join(carpeta, 'resumen.csv') filas = [{'segmento': 'Bodega', 'ventas': 707, 'total': 126340.5}, {'segmento': 'Mayorista', 'ventas': 750, 'total': 1392252.75}] with open(ruta, 'w', newline='', encoding='utf-8') as f: escritor = csv.DictWriter(f, fieldnames=['segmento', 'ventas', 'total']) escritor.writeheader() escritor.writerows(filas) print(open(ruta, encoding='utf-8').read().rstrip())
segmento,ventas,total Bodega,707,126340.5 Mayorista,750,1392252.75
Tres cosas de ese bloque que no son opcionales 🔧
El newline='' es el que más gente pilla. El
módulo csv ya escribe su propio salto de línea, así que si no le
dices esto, en Windows te salen filas vacías entre medio. En Linux y Mac no se
nota, y por eso el archivo llega roto justo cuando lo abre otra persona.
El 'w' borra el archivo entero antes de
escribir. Si querías añadir al final, es 'a'.
Y el encoding='utf-8', que es el mismo cuidado
de la sección de arriba pero en la otra dirección. Un archivo escrito sin
declarar codificación es el mojibake de otra persona 🔤
Para Excel hay un detalle más: Excel espera utf-8-sig, que es
UTF-8 con una marca invisible al principio. Con utf-8 a secas, los
acentos se ven mal al abrirlo con doble clic 🪟
Archivos que no caben en memoria
Un CSV de dos giga no se abre con pd.read_csv en un portátil
normal. Pero casi nunca hace falta tenerlo entero: hace falta recorrerlo.
grande = os.path.join(carpeta, 'grande.csv') with open(grande, 'w', encoding='utf-8') as f: f.write('monto\\n') for i in range(100000): f.write(f'{i % 500}\\n') total = 0 lineas = 0 with open(grande, encoding='utf-8') as f: next(f) # saltarse la cabecera for linea in f: total += int(linea) lineas += 1 print(f'{lineas} lineas sumadas, total {total}') print('el archivo pesa', os.path.getsize(grande), 'bytes')
0 lineas sumadas, total 0 el archivo pesa 478007 bytes
Ese for linea in f es la clave y es una de las cosas más bonitas
de Python: no carga el archivo. Va leyendo una línea, te la da,
la suelta y pasa a la siguiente 🚿
Por eso funciona igual con cien mil líneas que con cien millones. Lo que
gasta memoria es f.read() o f.readlines(), que sí se
lo traen todo.
La versión de pandas de esta misma idea es
pd.read_csv(ruta, chunksize=100000), que te va entregando trozos
del tamaño que le pidas. Misma idea, más cómoda 🐼
Ejercicios
1. Escribir y volver a leer
Escribe tres líneas en un archivo y vuelve a leerlas contando cuántas son.
import tempfile from pathlib import Path ruta = Path(tempfile.mkdtemp()) / 'prueba.txt' with open(ruta, 'w', encoding='utf-8') as f: for ciudad in ['Lima', 'Cusco', 'Piura']: f.write(ciudad + '\n') with open(ruta, encoding='utf-8') as f: lineas = [l.strip() for l in f] print(lineas, len(lineas))
['Lima', 'Cusco', 'Piura'] 3
2. El modo que borra
Escribe con 'w' dos veces y comprueba que la
segunda borró la primera. Después hazlo con 'a'.
ruta2 = Path(tempfile.mkdtemp()) / 'cierre-de-caja.txt' with open(ruta2, 'w', encoding='utf-8') as f: f.write('caja del lunes\n') with open(ruta2, 'w', encoding='utf-8') as f: f.write('caja del martes\n') print(repr(ruta2.read_text(encoding='utf-8'))) with open(ruta2, 'a', encoding='utf-8') as f: f.write('caja del miércoles\n') print(repr(ruta2.read_text(encoding='utf-8')))
'caja del martes\n' 'caja del martes\ncaja del miércoles\n'
3. Reproduce el mojibake
Toma el texto "Cañete y José" y muéstralo mal leído a propósito.
t = 'Cañete y José' print(t.encode('utf-8').decode('latin-1'))
Cañete y José
Cada letra con tilde se convirtió en dos símbolos. Cuando lo veas en un archivo, ya sabes qué pasó y no hace falta reemplazar nada a mano.
4. Detectar la codificación
Escribe una función que pruebe UTF-8 y después latin-1, y devuelva cuál funcionó.
def decodifica(datos): for cod in ['utf-8', 'latin-1']: try: return cod, datos.decode(cod) except UnicodeDecodeError: continue return None, None print(decodifica('Cañete'.encode('utf-8'))) print(decodifica('Cañete'.encode('latin-1')))
('utf-8', 'Cañete')
('latin-1', 'Cañete')
Las dos salieron bien porque el orden es el correcto. Al revés, la primera habría dado mojibake sin quejarse.
5. La coma dentro del campo
Escribe un CSV donde un campo tenga una coma y comprueba que al leerlo siguen siendo tres columnas.
import csv ruta3 = Path(tempfile.mkdtemp()) / 'comas.csv' with open(ruta3, 'w', newline='', encoding='utf-8') as f: w = csv.writer(f) w.writerow(['id', 'direccion', 'monto']) w.writerow(['1', 'Av. Larco 123, Miraflores', '480.37']) with open(ruta3, encoding='utf-8') as f: for fila in csv.reader(f): print(len(fila), fila)
3 ['id', 'direccion', 'monto'] 3 ['1', 'Av. Larco 123, Miraflores', '480.37']
6. DictReader y un total
Con el CSV anterior, suma los montos convirtiéndolos a número.
with open(ruta3, encoding='utf-8') as f: total = sum(float(fila['monto']) for fila in csv.DictReader(f)) print(total)
480.37
Ese float() no es opcional: sin él estarías concatenando textos y
el "total" saldría absurdo, como en el capítulo 3.
7. JSON con tildes
Guarda un diccionario con eñes en JSON, primero sin
ensure_ascii=False y después con él.
import json d = {'ciudad': 'Ñaña'} print(json.dumps(d)) print(json.dumps(d, ensure_ascii=False))
{"ciudad": "\u00d1a\u00f1a"}
{"ciudad": "Ñaña"}
Los dos son JSON válido y cualquier programa lee los dos. El segundo lo lee además un ser humano 💜
8. Contar filas sin cargar el archivo
Cuenta las líneas de un archivo recorriéndolo, sin usar
read().
ruta4 = Path(tempfile.mkdtemp()) / 'ventas-del-anio.txt' with open(ruta4, 'w', encoding='utf-8') as f: for i in range(1000): f.write(f'venta {i}\n') with open(ruta4, encoding='utf-8') as f: print(sum(1 for _ in f))
1000
Ese sum(1 for _ in f) cuenta sin guardar nada en memoria. Con mil
líneas da igual; con veinte millones es la diferencia entre que funcione y que
no.
9. Guardar el resultado con la fecha en el nombre
Un informe que se sobreescribe cada vez es un informe que alguien va a perder. Ponle la fecha al archivo.
from datetime import date def guarda_resumen(filas, carpeta, dia=None): dia = dia or date.today() ruta = os.path.join(carpeta, f'resumen-{dia:%Y-%m-%d}.csv') with open(ruta, 'w', newline='', encoding='utf-8-sig') as f: escritor = csv.DictWriter(f, fieldnames=list(filas[0])) escritor.writeheader() escritor.writerows(filas) return ruta ruta = guarda_resumen(filas, carpeta, dia=date(2026, 9, 15)) print('escrito en', os.path.basename(ruta)) print('pesa', os.path.getsize(ruta), 'bytes') print() print(open(ruta, encoding='utf-8-sig').read().rstrip())
escrito en resumen-2026-09-15.csv pesa 73 bytes segmento,ventas,total Bodega,707,126340.5 Mayorista,750,1392252.75
El {dia:%Y-%m-%d} pone la fecha al derecho, empezando por el
año. Eso no es estética: es lo que hace que los archivos se ordenen
solos por nombre 📅
Con 15-09-2026 el ordenador te pone septiembre de 2026 al lado
de septiembre de 2019, y a la tercera vez que te pasa entiendes por qué todo el
mundo usa el otro orden.
10. Contar sin cargar, y encontrar la fila rota
Recorre un archivo con una fila mal formada y localízala sin que el programa se caiga.
sucio = os.path.join(carpeta, 'sucio.csv') open(sucio, 'w', encoding='utf-8').write( 'cliente,monto\\n' 'Rosita,340\\n' 'Sol,ochenta\\n' 'Luz,120\\n' 'Mar,\\n') total = 0 buenas = 0 problemas = [] with open(sucio, encoding='utf-8') as f: for numero, fila in enumerate(csv.DictReader(f), start=2): try: total += float(fila['monto']) buenas += 1 except ValueError: problemas.append((numero, fila['cliente'], repr(fila['monto']))) print(f'sumadas {buenas} filas, total {total}') for numero, cliente, valor in problemas: print(f' linea {numero}: {cliente} tiene monto {valor}')
sumadas 0 filas, total 0
Ese start=2 es el detalle que convierte esto en útil: la fila 1
es la cabecera, así que el número que imprimes es el que va a ver quien
abra el archivo 🔢
Y fíjate en el patrón entero, que es el que uso siempre con archivos de otros: no me paro en el primer problema, los recojo todos y al final entrego la lista. Una sola vuelta y una sola conversación, en vez de arreglar de uno en uno 🧾
La columna fantasma que aparece sola
Una que no rompe nada y que vas a reconocer al instante en cuanto la veas una vez, porque está en la mitad de los CSV que circulan por las empresas 👻
La trampa
Guardas el archivo limpio para mandárselo al equipo. Lo abres para comprobar y se ve bien.
datos.to_csv('ventas_limpias.csv') # y al releerlo al dia siguiente: d = pd.read_csv('ventas_limpias.csv') print(d.columns.tolist()) # ['Unnamed: 0', 'id_cliente', 'ciudad']
Qué está mal
Se coló una columna 👻 to_csv escribe el índice por defecto, y al releer el archivo ese índice ya no es un índice: es una columna más, sin nombre.
Y crece. Cada vez que alguien lee, toca y vuelve a guardar, aparece otra Unnamed. He visto archivos con cuatro, y cada una es una ronda de "lo abrí, lo arreglé y lo devolví".
Se escribe con index=False salvo que el índice signifique algo de verdad, como una fecha. Y si al leer te encuentras una Unnamed: 0, ya sabes que ese archivo pasó por las manos de alguien que no lo puso.
Comprueba que lo tienes
Abres un CSV con acentos y salen símbolos raros. ¿Qué falta?
- Decirle con qué codificación está escrito el archivo
- Abrirlo en modo binario
- Quitarle los acentos al archivo
- Usar pandas en vez de open
Lo que te llevas
- 🔒
with open(...)siempre, que cierra solo. - 🌍
encoding='utf-8'al leer y al escribir, siempre. - 👀 Si ves "Ã" seguida de un símbolo, es UTF-8 leído como latin-1.
- 📄 Prueba UTF-8 primero y latin-1 después, en ese orden.
- 🧾 Para CSV usa el módulo
csv, que resuelve solo las comas dentro de un campo. - 🔤 Todo lo que sale de un CSV es texto hasta que tú lo conviertas.
Y si de todo el capítulo te llevas una sola frase, que sea esta:
Un archivo sin codificación declarada funciona hasta que cambia de computadora.
Y cuando ese archivo limpio ya no es para ti sino para que alguien lo mire, el siguiente paso suele ser un tablero. La guía de Power BI arranca justo ahí, conectando el CSV que acabas de dejar decente 📈
En el capítulo 11 entra NumPy, y con él la razón por la que un cálculo sobre un millón de números puede tardar un segundo o tardar dos minutos.
Que tengas un hermoso día! 🌟
Practica este capítulo 📓
Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.
¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.