Saltar al contenido

Indice / Entrada y salida / 7.3

Excel con openpyxl

Leer y escribir .xlsx con varias hojas, y darles el formato mínimo para que alguien las pueda abrir. Excel es el formato en el que llegan los datos y en el que los quieren de vuelta.

Antes: 4.2 Leer y mirar Despues: cierra su rama

Para qué sirve

Es la realidad del trabajo: los datos llegan en una planilla que armó una persona - con un título arriba, una fila en blanco y la tabla más abajo - y el resultado hay que entregarlo también en una planilla, con varias hojas y el encabezado en negrita.

pandas hace las dos puntas con read_excel y to_excel. openpyxl es el motor que trabaja abajo, y se usa directamente solo para lo que pandas no hace: estilo, anchos de columna, paneles congelados.

ELI5Explicado facil

Un CSV es texto plano: lo que ves es lo que hay.

Un .xlsx es una carpeta comprimida con varias hojas adentro, más los colores, las fórmulas y los anchos de columna. Por eso hace falta una biblioteca para leerlo, y por eso una hoja puede tener cosas que no son la tabla.

Varias hojas

EjemploEscribir un Excel con varias hojas, y volver a leerlo
import tempfile
from pathlib import Path

import pandas as pd

inventario = pd.read_csv("inventario_productos.csv")
caros = inventario[inventario["precio_unitario"] > 500]
baratos = inventario[inventario["precio_unitario"] <= 500]

# Los ejemplos del curso no escriben en la carpeta de datos: usan un
# directorio temporal que se borra solo.
carpeta = Path(tempfile.mkdtemp())
destino = carpeta / "inventario.xlsx"

with pd.ExcelWriter(destino, engine="openpyxl") as libro:
    caros.to_excel(libro, sheet_name="caros", index=False)
    baratos.to_excel(libro, sheet_name="baratos", index=False)

print("archivo escrito:", destino.name)
print("tamano:", destino.stat().st_size > 0)

vuelta = pd.read_excel(destino, sheet_name="caros")
print()
print("hoja 'caros':", vuelta.shape)
print(vuelta.head(3).to_string(index=False))

print()
print("index=False es obligatorio en la practica: sin el, Excel abre el")
print("archivo con una primera columna sin nombre que confunde a todo el")
print("mundo, igual que en un CSV.")
archivo escrito: inventario.xlsx
tamano: True

hoja 'caros': (17, 5)
codigo            producto   categoria  cantidad  precio_unitario
  P002   Ratón inalámbrico Perifericos        82           781.78
  P003 Monitor 24 pulgadas   Pantallas       108           872.23
  P005           Webcam HD Perifericos        26           713.42

index=False es obligatorio en la practica: sin el, Excel abre el
archivo con una primera columna sin nombre que confunde a todo el
mundo, igual que en un CSV.
ExcelWriter y el with

Para escribir más de una hoja hace falta un ExcelWriter, y hay que usarlo con with: el archivo se termina de armar recién al cerrarlo. Si lo abrís sin with y te olvidás de close(), el .xlsx queda corrupto.

Leer todas las hojas de una

Ejemplosheet_name=None devuelve un diccionario de DataFrames
import tempfile
from pathlib import Path

import pandas as pd

inventario = pd.read_csv("inventario_productos.csv")
carpeta = Path(tempfile.mkdtemp())
destino = carpeta / "por_categoria.xlsx"

# Una hoja por categoria. Los nombres de hoja tienen limites: 31 caracteres
# y sin los caracteres : \ / ? * [ ]
with pd.ExcelWriter(destino, engine="openpyxl") as libro:
    for categoria, grupo in inventario.groupby("categoria"):
        grupo.to_excel(libro, sheet_name=categoria[:31], index=False)

print("hojas escritas:", inventario["categoria"].nunique())

todas = pd.read_excel(destino, sheet_name=None)
print("tipo de lo que devuelve sheet_name=None:", type(todas).__name__)
print("hojas leidas:", len(todas))
print("nombres:", sorted(todas)[:5], "...")

print()
for nombre in sorted(todas)[:3]:
    print(f"{nombre:<16} {todas[nombre].shape}")

print()
juntas = pd.concat(todas.values(), ignore_index=True)
print("apilando todas las hojas:", juntas.shape)
print("el original era        :", inventario.shape)
print("coinciden:", len(juntas) == len(inventario))
hojas escritas: 11
tipo de lo que devuelve sheet_name=None: dict
hojas leidas: 11
nombres: ['Accesorios', 'Almacenamiento', 'Audio', 'Cables', 'Componentes'] ...

Accesorios       (5, 5)
Almacenamiento   (2, 5)
Audio            (2, 5)

apilando todas las hojas: (35, 5)
el original era        : (35, 5)
coinciden: True

sheet_name=None devuelve un diccionario de DataFrames, uno por hoja. Con concat se apilan y se recupera el original - el mismo patrón de la lección 6.1, pero con hojas en vez de archivos.

Los nombres de hoja tienen reglas

Máximo 31 caracteres, y no pueden contener : \ / ? * [ ]. Si tu nombre viene de una columna de datos, hay que recortarlo y limpiarlo, y ahí aparece un problema nuevo: dos categorías largas distintas pueden quedar con el mismo nombre de hoja recortado.

Estilo

EjemploEstilo basico: encabezado en negrita y columnas anchas
import tempfile
from pathlib import Path

import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import Alignment, Font

inventario = pd.read_csv("inventario_productos.csv").head(6)
carpeta = Path(tempfile.mkdtemp())
destino = carpeta / "con_estilo.xlsx"

inventario.to_excel(destino, sheet_name="inventario", index=False)

# pandas escribe los datos; openpyxl los viste. Se abre el archivo ya
# escrito, se toca lo que haga falta y se guarda.
libro = load_workbook(destino)
hoja = libro["inventario"]

for celda in hoja[1]:                       # la fila 1 es el encabezado
    celda.font = Font(bold=True)
    celda.alignment = Alignment(horizontal="center")

anchos = {"A": 10, "B": 24, "C": 16, "D": 10, "E": 16}
for columna, ancho in anchos.items():
    hoja.column_dimensions[columna].width = ancho

hoja.freeze_panes = "A2"                    # el encabezado queda fijo
libro.save(destino)

print("hoja:", hoja.title)
print("filas:", hoja.max_row, "| columnas:", hoja.max_column)
print("encabezado en negrita:", hoja["A1"].font.bold)
print("ancho de B:", hoja.column_dimensions["B"].width)
print("paneles congelados en:", hoja.freeze_panes)

print()
print("Que se lee igual:", pd.read_excel(destino).shape)
print()
print("El estilo NO viaja en los datos: si despues lees el archivo con")
print("pandas, la negrita y el ancho no existen. Son para el humano que")
print("abre la planilla.")
hoja: inventario
filas: 7 | columnas: 5
encabezado en negrita: True
ancho de B: 24.0
paneles congelados en: A2

Que se lee igual: (6, 5)

El estilo NO viaja en los datos: si despues lees el archivo con
pandas, la negrita y el ancho no existen. Son para el humano que
abre la planilla.

La división de trabajo es clara: pandas escribe los datos, openpyxl los viste. Se abre el archivo ya escrito con load_workbook, se toca lo que haga falta y se guarda.

Y algo que conviene tener presente: el estilo no viaja en los datos. Si después leés ese archivo con pandas, la negrita y el ancho no existen. Son para la persona que abre la planilla, no para el programa que la procesa.

La planilla que armó una persona

EjemploLa planilla con titulo arriba, que es como llegan de verdad
import tempfile
from pathlib import Path

import pandas as pd
from openpyxl import Workbook

carpeta = Path(tempfile.mkdtemp())
destino = carpeta / "reporte.xlsx"

# Asi arma una planilla una persona: un titulo, una fila en blanco, y
# recien despues la tabla.
libro = Workbook()
hoja = libro.active
hoja.title = "Reporte"
hoja["A1"] = "Inventario al 24/08/2026"
hoja["A2"] = "Area: Sistemas"
hoja.append([])
hoja.append(["codigo", "producto", "cantidad"])
for fila in [["P001", "Teclado", 112], ["P002", "Raton", 82],
             ["P003", "Monitor", 108]]:
    hoja.append(fila)
libro.save(destino)

print("leido sin decir nada:")
mal = pd.read_excel(destino)
print(mal.to_string())
print("columnas:", list(mal.columns))

print()
print("con skiprows=3:")
bien = pd.read_excel(destino, skiprows=3)
print(bien.to_string(index=False))
print("columnas:", list(bien.columns))
print("dtypes:")
print(bien.dtypes.to_string())

print()
print("Sin skiprows, el titulo se convierte en el nombre de la columna y")
print("las tres filas de datos quedan mezcladas con las de adorno. No da")
print("error: da una tabla que no sirve.")
leido sin decir nada:
  Inventario al 24/08/2026 Unnamed: 1 Unnamed: 2
0           Area: Sistemas        NaN        NaN
1                      NaN        NaN        NaN
2                   codigo   producto   cantidad
3                     P001    Teclado        112
4                     P002      Raton         82
5                     P003    Monitor        108
columnas: ['Inventario al 24/08/2026', 'Unnamed: 1', 'Unnamed: 2']

con skiprows=3:
codigo producto  cantidad
  P001  Teclado       112
  P002    Raton        82
  P003  Monitor       108
columnas: ['codigo', 'producto', 'cantidad']
dtypes:
codigo        str
producto      str
cantidad    int64

Sin skiprows, el titulo se convierte en el nombre de la columna y
las tres filas de datos quedan mezcladas con las de adorno. No da
error: da una tabla que no sirve.

Sin skiprows, el título se convierte en el nombre de la primera columna y las filas de adorno quedan mezcladas con los datos. No da error: da una tabla que no sirve, con columnas llamadas Unnamed: 1.

Unnamed: 1 es la firma de este problema

Cuando veas columnas con ese nombre, la fila de encabezado está en otro lado. skiprows= para saltear filas de arriba, header= para decir cuál fila es el encabezado, y usecols="B:E" si además sobran columnas a la izquierda.

Dónde se rompe

ImportError: Missing optional dependency 'openpyxl'

Causa: pandas sabe leer Excel pero necesita el motor, que se instala aparte.

Arreglo: pip install openpyxl. Para archivos .xls viejos el motor es otro (xlrd).

(sin error) las columnas se llaman Unnamed: 1, Unnamed: 2

Causa: la hoja tiene un título o filas de adorno arriba de la tabla.

Arreglo: skiprows=3, o header=3. Abrí la planilla y contá las filas: es más rápido que adivinar.

(sin error) un código con ceros a la izquierda perdió los ceros

Causa: lo mismo que en CSV: pandas infiere que es un número.

Arreglo: dtype={"codigo": "str"}, que read_excel también acepta.

(sin error) el archivo escrito no se abre

Causa: el ExcelWriter no se cerró, así que el .xlsx quedó incompleto.

Arreglo: usarlo siempre con with.

InvalidWorksheetName / el nombre de hoja se recortó

Causa: el nombre supera los 31 caracteres o tiene un carácter prohibido.

Arreglo: recortar y limpiar antes, y comprobar que no queden dos hojas con el mismo nombre después de recortar.

Acá hay que elegir

¿CSV o Excel para entregar?
  • CSV - cuando el destino es otro programa. Es texto plano, se versiona en git, se compara con diff, y no depende de ninguna biblioteca. Todos los objetivos de este curso son CSV por eso.
  • Excel - cuando el destino es una persona que va a abrir el archivo. Varias hojas, encabezado fijo, columnas anchas: cosas que un CSV no puede tener y que hacen la diferencia para quien lo lee.

Y si son las dos cosas, escribí los dos: el CSV es la fuente de verdad, el Excel es la presentación. Lo que no conviene es que el .xlsx sea el único lugar donde existe el resultado.

Recetario

Excel
ExpresiónQué haceCuándo
pd.read_excel(f)Lee la primera hojaEl caso simple
read_excel(f, sheet_name="ventas")Una hoja por nombreCuando sabés cuál
read_excel(f, sheet_name=None)dict de DataFramesTodas las hojas
skiprows=3Saltea filas de arribaPlanillas con título
usecols="B:E"Solo ese rango de columnasCuando sobra a la izquierda
dtype={"c": "str"}Fuerza el tipoCódigos con ceros
df.to_excel(f, index=False)Escribe una hojaSalida simple
with pd.ExcelWriter(f) as w:Varias hojasSiempre con with
load_workbook(f)Abre el archivo con openpyxlPara darle estilo
celda.font = Font(bold=True)NegritaEncabezados
hoja.column_dimensions["B"].width = 24Ancho de columnaPara que se lea
hoja.freeze_panes = "A2"Fija el encabezado al scrollearTablas largas

Ponete a prueba

1. ¿Que devuelve pd.read_excel(archivo, sheet_name=None)?Nivel 1 · Recordar
2. ¿Para que se usa openpyxl directamente, si pandas ya lee y escribe Excel?Nivel 1 · Recordar
3. Las columnas de un Excel leido se llaman Unnamed: 1 y Unnamed: 2. ¿Que pasa?Nivel 2 · Predecir la salida
4. ¿Por que ExcelWriter se usa siempre con with?Nivel 2 · Predecir la salida
5. Escribis una hoja por categoria usando el nombre de la categoria. ¿Que puede salir mal?Nivel 3 · Elegir entre dos casi correctas
6. Le pusiste negrita al encabezado con openpyxl y despues leiste el archivo con pandas. ¿Donde quedo el estilo?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo tiene que entregar tres hojas y el archivo no se abre en Excel. ¿Por que?
libro = pd.ExcelWriter("salida.xlsx")
limpios.to_excel(libro, sheet_name="limpios", index=False)
rechazos.to_excel(libro, sheet_name="rechazos", index=False)
resumen.to_excel(libro, sheet_name="resumen", index=False)
Nivel 4 · Encontrar el defecto

Prácticas de esta lección