Módulo 1 · Lección 46 de 59
Rutas de ficheros con pathlib
Cómo construir y manipular rutas de forma que funcionen igual en Windows, macOS y Linux, sin pelearse con las barras.
Escribe una ruta a mano y ya tienes un problema de portabilidad. En Windows las carpetas se separan con barra invertida, datos\informes\ventas.csv, y en macOS y Linux con barra normal, datos/informes/ventas.csv. Si escribes una de las dos en tu código, el programa deja de funcionar en el otro sistema.
Y la barra invertida tiene un problema añadido: dentro de una cadena de Python es el carácter de escape. "datos\nuevos" no contiene la carpeta nuevos, contiene un salto de línea seguido de uevos, porque \n es un salto de línea. Es un fallo silencioso de los peores, y a nadie se le ocurre mirar ahí.
La solución está en la biblioteca estándar y se llama pathlib. En vez de tratar las rutas como texto, las trata como lo que son.
Construir rutas con el operador /#
from pathlib import Path
ruta = Path("datos") / "informes" / "ventas-2026.csv"
print(ruta)
print(type(ruta))datos/informes/ventas-2026.csv
<class 'pathlib.PosixPath'>La barra / entre dos trozos de ruta los une con el separador que corresponda al sistema. En Windows ese mismo programa imprime datos\informes\ventas-2026.csv y el tipo es WindowsPath. Tú escribes lo mismo y Python traduce.
Un objeto Path sirve en cualquier sitio donde antes pondrías una cadena con la ruta, empezando por open():
from pathlib import Path
ruta = Path("datos") / "notas.txt"
with open(ruta, encoding="utf-8") as fichero:
print(fichero.read())Las partes de una ruta#
Un Path sabe descomponerse solo, sin split(".") ni rebanadas contadas a mano:
from pathlib import Path
ruta = Path("datos") / "informes" / "ventas-2026.csv"
print(ruta.name)
print(ruta.stem)
print(ruta.suffix)
print(ruta.parent)
print(ruta.parent.parent)
print(ruta.parts)ventas-2026.csv
ventas-2026
.csv
datos/informes
datos
('datos', 'informes', 'ventas-2026.csv')| Propiedad | Qué devuelve |
|---|---|
name | El nombre del fichero con extensión |
stem | El nombre sin extensión |
suffix | La extensión, con el punto incluido |
parent | La carpeta que lo contiene |
parts | Una tupla con todos los trozos |
Con stem y suffix se construyen nombres derivados sin cortar cadenas:
from pathlib import Path
original = Path("informe.txt")
copia = original.parent / f"{original.stem}-copia{original.suffix}"
print(copia)informe-copia.txtComprobar qué hay ahí#
Tres preguntas, tres métodos, y todos devuelven True o False sin lanzar errores. Suponiendo que junto a tu programa haya una carpeta llamada datos:
from pathlib import Path
carpeta = Path("datos")
print(carpeta.exists())
print(carpeta.is_dir())
print(carpeta.is_file())
print(Path("no-existe.txt").exists())True
True
False
Falseexists() dice si hay algo, is_file() si eso es un fichero e is_dir() si es una carpeta. Preguntar por una ruta inventada no falla: simplemente responde False.
Crear carpetas#
from pathlib import Path
carpeta = Path("datos") / "informes"
carpeta.mkdir(parents=True, exist_ok=True)
print(carpeta.exists(), carpeta.is_dir())True TrueLos dos parámetros son los que hacen que esta línea sea de fiar:
parents=Truecrea también las carpetas intermedias que falten. Sin él, sidatosno existe, creardatos/informesfalla.exist_ok=Truehace que no pase nada si la carpeta ya estaba. Sin él, la segunda ejecución del programa muere conFileExistsError.
Listar y buscar#
Trabajaremos sobre este árbol:
datos/
leeme.md
notas.txt
informes/
ventas-2025.csv
ventas-2026.csviterdir() recorre lo que hay dentro de una carpeta, un nivel y nada más:
from pathlib import Path
for elemento in sorted(Path("datos").iterdir()):
tipo = "carpeta" if elemento.is_dir() else "fichero"
print(f"{tipo}: {elemento.name}")carpeta: informes
fichero: leeme.md
fichero: notas.txtEl sorted() no es decorativo: iterdir() entrega los elementos en el orden que le dé el sistema de ficheros, que no tiene por qué ser alfabético ni ser el mismo en dos ordenadores. Si el orden te importa, ordena tú.
glob() busca por patrón en esa carpeta, y rglob() hace lo mismo bajando por todas las subcarpetas:
from pathlib import Path
print(sorted(Path("datos").glob("*.txt")))
print(sorted(Path("datos").glob("*.csv")))
print(sorted(Path("datos").rglob("*.csv")))[PosixPath('datos/notas.txt')]
[]
[PosixPath('datos/informes/ventas-2025.csv'), PosixPath('datos/informes/ventas-2026.csv')]La segunda línea sale vacía porque en datos no hay ningún .csv: están en la subcarpeta, y glob() no baja. La r de rglob es de recursivo, y es la que los encuentra.
En los patrones, * significa "cualquier cosa" y ? "un carácter cualquiera". Así, *.csv son todos los CSV, ventas-* todo lo que empiece por eso y informe-202?.txt cubre del 2020 al 2029.
Leer y escribir en un solo paso#
Para ficheros pequeños, pathlib te ahorra el with:
from pathlib import Path
destino = Path("saludo.txt")
destino.write_text("Hola desde pathlib\n", encoding="utf-8")
contenido = destino.read_text(encoding="utf-8")
print(repr(contenido))'Hola desde pathlib\n'write_text() crea el fichero o lo reemplaza entero, igual que el modo "w", y read_text() lo devuelve como una cadena, igual que read(). Ambos abren y cierran solos.
Para ficheros grandes sigue usando with open(ruta) y el bucle for, porque read_text() carga todo en memoria de golpe.
Renombrar y borrar#
from pathlib import Path
origen = Path("datos") / "notas.txt"
destino = Path("datos") / "notas-antiguas.txt"
origen.rename(destino)
print(destino.exists(), origen.exists())
temporal = Path("datos") / "borrame.tmp"
temporal.write_text("nada importante\n", encoding="utf-8")
print(temporal.exists())
temporal.unlink()
print(temporal.exists())True False
True
Falserename() cambia el nombre o mueve el fichero, y unlink() lo borra. El nombre unlink viene de Unix y despista; es el borrar de toda la vida.
Las dos rutas de referencia#
Toda ruta relativa se resuelve desde algún punto, y confundir cuál es el punto causa la mitad de los FileNotFoundError.
from pathlib import Path
print(Path.cwd())
print(Path(__file__))
print(Path(__file__).parent)/Users/erwin/curso-python
/Users/erwin/curso-python/programa.py
/Users/erwin/curso-pythonPath.cwd()es la carpeta actual de trabajo: aquella desde la que se lanzó el comandopython. Cambia según desde dónde ejecutes.Path(__file__)es la ruta del fichero.pyque se está ejecutando, y.parentla carpeta donde vive. No cambia nunca.
Cuando escribes open("datos/notas.txt"), Python busca desde Path.cwd(). Si tu programa está en proyecto/ junto a su carpeta datos/, pero lo ejecutas desde el escritorio con python proyecto/programa.py, buscará datos/notas.txt en el escritorio y no lo encontrará.
Un inventario de carpeta#
Juntando rglob, suffix y el Counter de la lección de la biblioteca estándar:
from collections import Counter
from pathlib import Path
CARPETA = Path("datos")
def inventariar(carpeta: Path) -> None:
"""Muestra cuántos ficheros hay de cada extensión, mirando también las subcarpetas."""
extensiones = Counter()
total_bytes = 0
for elemento in carpeta.rglob("*"):
if elemento.is_file():
extensiones[elemento.suffix] += 1
total_bytes += elemento.stat().st_size
print(f"Ficheros en {carpeta}: {sum(extensiones.values())}")
for extension, cuantos in extensiones.most_common():
print(f" {extension:<6} {cuantos}")
print(f"Ocupan {total_bytes} bytes en total")
inventariar(CARPETA)Ficheros en datos: 4
.csv 2
.md 1
.txt 1
Ocupan 76 bytes en totalEl rglob("*") devuelve ficheros y carpetas, por eso hace falta el is_file() para descartar las segundas. stat().st_size da el tamaño en bytes. El total dependerá de lo que tengas dentro, claro.