Durante años, trabajar con rutas en Python significaba concatenar strings con os.path.join() y rezar para que el resultado fuera correcto en Windows y Linux. pathlib, disponible desde Python 3.4 y parte del estándar desde 3.6, resuelve eso de raíz: una ruta deja de ser un string frágil y se convierte en un objeto Path con comportamiento propio.
La diferencia conceptual importa. Un string "/home/user/proyecto" no sabe que es una ruta; es solo texto. Un Path sí lo sabe: entiende su estructura, puede navegar por ella, leer archivos y responder preguntas sobre el filesystem. Esto es lo que significa modelar algo como objeto en lugar de como dato primitivo.
Internamente, pathlib detecta el sistema operativo y crea una subclase concreta — PosixPath en Unix/macOS o WindowsPath en Windows — pero tú siempre trabajas con Path genérico. Eso es exactamente el manejo multiplataforma que os.path nunca te dio de forma tan limpia.
¿Cuándo usarlo? Siempre que trabajes con el filesystem: leer configs, escribir logs, recorrer directorios, construir rutas dinámicamente. La única razón para seguir usando os.path hoy es compatibilidad con código legado que no controlas.
Lo que se rompe si ignoras pathlib y sigues con strings: concatenas rutas con + o / a mano, el código explota en Windows porque las barras van al revés, y terminas con bugs que solo aparecen en producción en máquinas de otro equipo.
from pathlib import Path
# ── Construcción de rutas ──────────────────────────────────────────────
# El operador / no es división aquí: Path lo sobrecarga para unir rutas
base = Path("/tmp/proyecto")
config_file = base / "config" / "settings.toml"
data_file = base / "datos" / "entrada.csv"
print(config_file) # /tmp/proyecto/config/settings.toml
# ── Creación de directorios ────────────────────────────────────────────
# parents=True crea todos los intermedios; exist_ok=True no falla si ya existe
(base / "config").mkdir(parents=True, exist_ok=True)
(base / "datos").mkdir(parents=True, exist_ok=True)
(base / "salida").mkdir(parents=True, exist_ok=True)
# ── Escritura y lectura ────────────────────────────────────────────────
config_file.write_text("[base]\ntimeout = 30\n", encoding="utf-8")
data_file.write_text("nombre,edad\nAna,28\nLuis,34\n", encoding="utf-8")
contenido = config_file.read_text(encoding="utf-8")
print(contenido)
# write_bytes / read_bytes para contenido binario (imágenes, etc.)
imagen_src = Path("/usr/share/pixmaps/python3.xpm")
imagen_dest = base / "salida" / "copia.xpm"
if imagen_src.exists():
imagen_dest.write_bytes(imagen_src.read_bytes())
# ── Consultas sobre el filesystem ─────────────────────────────────────
print(config_file.exists()) # True
print(config_file.is_file()) # True
print(base.is_dir()) # True
print((base / "fantasma.txt").exists()) # False
# ── Propiedades de la ruta (no tocan el disco) ────────────────────────
ruta = Path("/tmp/proyecto/datos/entrada.csv")
print(ruta.name) # entrada.csv ← nombre completo
print(ruta.stem) # entrada ← nombre sin extensión
print(ruta.suffix) # .csv ← extensión con punto
print(ruta.parent) # /tmp/proyecto/datos
# ── Búsqueda con patrones ─────────────────────────────────────────────
# Crea algunos archivos extra para demostrar glob
for nombre in ["reporte_2024.csv", "reporte_2023.csv", "notas.txt"]:
(base / "datos" / nombre).write_text("placeholder", encoding="utf-8")
# glob: busca en el directorio directo (no recursivo)
csv_directos = list((base / "datos").glob("*.csv"))
print([p.name for p in csv_directos])
# ['entrada.csv', 'reporte_2024.csv', 'reporte_2023.csv']
# rglob: busca recursivamente en todo el árbol bajo base
todos_csv = list(base.rglob("*.csv"))
print([p.relative_to(base) for p in todos_csv])
# [datos/entrada.csv, datos/reporte_2024.csv, ...]
Qué está pasando aquí
El operador / es el gesto más elocuente de pathlib. Python permite sobrecargar operadores a través del data model, y Path implementa __truediv__ para construir rutas. El resultado no es concatenar strings — construye un nuevo objeto Path con la estructura correcta para el SO actual. En Windows, ese mismo código produce C:\tmp\proyecto\config\settings.toml sin que tú toques nada.
mkdir(parents=True, exist_ok=True) merece atención. parents=True es el equivalente a mkdir -p en bash: crea todos los directorios intermedios si no existen. exist_ok=True hace que no lance FileExistsError si el directorio ya estaba ahí. En código de inicialización, quieres ambos casi siempre; sin ellos, tu script falla en la primera ejecución limpia o en la segunda ejecución.
.read_text() y .write_text() eliminan el patrón open(...) as f: f.read() para casos simples. Pasar encoding="utf-8" explícitamente es una buena costumbre: sin él, Python usa el encoding del sistema, que varía entre plataformas y puede causarte bugs difíciles de reproducir.
Las propiedades .name, .stem, .suffix, .parent no hacen ninguna llamada al sistema operativo — operan puramente sobre la estructura de la ruta como string estructurado. Puedes usarlas sobre rutas que ni siquiera existen todavía, lo cual es útil cuando estás construyendo la ruta de destino de un archivo antes de crearlo.
.glob() vs .rglob(): glob busca solo en el directorio donde lo llamas, rglob desciende en todos los subdirectorios. La r es de recursive. Ambos devuelven un generador, así que envuelves en list() solo si necesitas materializar todos los resultados; si vas a iterar una sola vez, deja el generador como está.
.relative_to(base) en el último bloque convierte rutas absolutas en relativas respecto a un punto de referencia — útil para mostrar rutas en logs sin exponer la estructura de directorios del servidor.
Errores que debes conocer
Error: Pasar un Path a una función antigua que espera str y obtener TypeError.
import os
ruta = Path("/tmp/proyecto/datos/entrada.csv")
# ❌ Algunas APIs antiguas no aceptan Path directamente
os.system("cat " + ruta) # TypeError: can only concatenate str (not "PosixPath") to str
# ✅ Convierte explícitamente con str() o usa os.fspath()
os.system("cat " + str(ruta))
Desde Python 3.6 la mayoría de las funciones de os y open() aceptan Path directamente gracias al protocolo os.fspath, pero si usas librerías de terceros antiguas, str(ruta) es la solución limpia.
Error: Usar glob("**/*.csv") en lugar de rglob("*.csv") y confundirse con el comportamiento.
directorio = Path("/tmp/proyecto")
# ❌ glob con ** requiere que pases también el separador explícitamente
# y el comportamiento puede ser confuso para quien lee el código
resultados = list(directorio.glob("**/*.csv"))
# ✅ rglob("*.csv") es exactamente equivalente y más legible
resultados = list(directorio.rglob("*.csv"))
rglob(pattern) es azúcar sobre glob("**/" + pattern) — el resultado es idéntico, pero la intención queda más clara.
Error: Asumir que .parent en una ruta de un solo nivel devuelve algo útil.
# ❌ Si la ruta no tiene directorio explícito, .parent devuelve "."
ruta = Path("archivo.txt")
print(ruta.parent) # . ← no es lo que esperas si quieres el directorio actual
# ✅ Usa Path.cwd() / "archivo.txt" si necesitas la ruta absoluta desde el inicio
ruta = Path.cwd() / "archivo.txt"
print(ruta.parent) # /ruta/absoluta/al/directorio/actual
Path("archivo.txt") es una ruta relativa válida, pero .parent solo puede darte lo que la ruta contiene; si no tiene directorio, devuelve .. Resuelve la ruta con .resolve() o constrúyela absoluta desde el principio.
N° 84