pathlib` vs `os.path` — Cómo manejar rutas en Python sin liarte

Si llevas un tiempo programando Python te has encontrado con dos formas distintas de hablar de rutas: la antigua, con os.path + strings; y la moderna, con pathlib.Path + objetos. Las dos funcionan. Las dos hacen prácticamente lo mismo. Y todavía hoy ves código mezclado en muchos proyectos.
En esta entrada te enseño la diferencia real, te doy las equivalencias lado a lado para que migres de una a la otra sin pensar, y te muestro casos prácticos donde una gana clarísimo a la otra. Spoiler: pathlib es el camino. Pero saber leer os.path te abre todo el código heredado del mundo Python.
Contenido
La idea en 30 segundos
os.path trata las rutas como strings. Tienes funciones sueltas que reciben y devuelven strings:
import os.path
ruta = os.path.join("datos", "peliculas", "2026.csv")
nombre = os.path.basename(ruta) # "2026.csv"
existe = os.path.exists(ruta)
pathlib.Path trata las rutas como objetos con métodos:
from pathlib import Path
ruta = Path("datos") / "peliculas" / "2026.csv"
nombre = ruta.name # "2026.csv"
existe = ruta.exists()
Mismo resultado. Pero pathlib es mucho más legible cuando encadenas operaciones, y hace de la ruta un objeto con métodos coherentes en lugar de una bolsa de funciones sueltas.
Comparativa lado a lado
Las operaciones más típicas en ambos estilos:
| Operación | os.path | pathlib |
|---|---|---|
| Construir ruta | os.path.join("a", "b", "c") | Path("a") / "b" / "c" |
| Nombre del fichero | os.path.basename(p) | Path(p).name |
| Carpeta padre | os.path.dirname(p) | Path(p).parent |
| Extensión | os.path.splitext(p)[1] | Path(p).suffix |
| Nombre sin extensión | os.path.splitext(os.path.basename(p))[0] | Path(p).stem |
| Ruta absoluta | os.path.abspath(p) | Path(p).resolve() |
| ¿Existe? | os.path.exists(p) | Path(p).exists() |
| ¿Es fichero? | os.path.isfile(p) | Path(p).is_file() |
| ¿Es directorio? | os.path.isdir(p) | Path(p).is_dir() |
| Leer texto | open(p).read() | Path(p).read_text() |
| Escribir texto | open(p, "w").write(...) | Path(p).write_text(...) |
| Listar carpeta | os.listdir(p) | Path(p).iterdir() |
| Buscar por patrón | glob.glob("*.csv") | Path(".").glob("*.csv") |
| Crear carpeta | os.makedirs(p, exist_ok=True) | Path(p).mkdir(parents=True, exist_ok=True) |
| Borrar fichero | os.remove(p) | Path(p).unlink() |
| Borrar carpeta | shutil.rmtree(p) | shutil.rmtree(p) (sigue siendo el camino) |
| Carpeta home | os.path.expanduser("~") | Path.home() |
| Carpeta actual | os.getcwd() | Path.cwd() |
Una sola tabla y ya tienes el 90% del trabajo cubierto.
Por qué pathlib gana en legibilidad
Mira este ejemplo realista:
# Estilo os.path — verboso, anida llamadas
import os.path
import glob
def csv_recientes(carpeta_raiz):
ruta_csvs = os.path.join(carpeta_raiz, "datos", "exportaciones")
if not os.path.isdir(ruta_csvs):
return []
return sorted(
f for f in glob.glob(os.path.join(ruta_csvs, "*.csv"))
if os.path.basename(f).startswith("2026")
)
# Estilo pathlib — encadena, lee como inglés
from pathlib import Path
def csv_recientes(carpeta_raiz):
ruta = Path(carpeta_raiz) / "datos" / "exportaciones"
if not ruta.is_dir():
return []
return sorted(p for p in ruta.glob("*.csv") if p.name.startswith("2026"))
La diferencia visual es enorme. Y como Path tiene métodos en cadena, escribes operaciones en orden mental, no en orden de “envuelve esta función dentro de esta otra”.
💡 Tip-friki: el operador
/está sobrecargado enPath. Cuando vesPath("a") / "b"Python llama aPath.__truediv__("a", "b")y devuelvePath("a/b"). La barra es el patrón idiomático de pathlib y lo encontrarás en cualquier proyecto moderno.
📥 Llévate el cheatsheet de Python (gratis)
PDF de 6 páginas con lo esencial: tipos, condicionales, bucles, estructuras de datos, funciones y los errores que más vas a cometer. Para tener al lado mientras programas.
Sin spam. Te apuntas a la lista, descargas el cheatsheet y recibes contenido de Python cada semana.
Operaciones potentes que solo brillan con pathlib
Iterar carpetas con filtros
from pathlib import Path
# Todos los .csv de una carpeta
for csv in Path("datos").glob("*.csv"):
print(csv.name)
# Recursivo: todos los .py en cualquier subcarpeta
for py in Path(".").rglob("*.py"):
print(py)
rglob (recursive glob) es uno de los métodos más útiles de pathlib. En os.path necesitas combinar os.walk + filtros y queda el doble de largo.
Construir y resolver rutas
ruta = Path("datos") / "peliculas" / "2026.csv"
print(ruta) # datos/peliculas/2026.csv
print(ruta.absolute()) # /Users/tu/proyecto/datos/peliculas/2026.csv
print(ruta.parent) # datos/peliculas
print(ruta.parents[1]) # datos
print(ruta.parts) # ('datos', 'peliculas', '2026.csv')
print(ruta.name) # 2026.csv
print(ruta.stem) # 2026
print(ruta.suffix) # .csv
print(ruta.with_suffix(".json")) # datos/peliculas/2026.json
with_suffix(".json") es magia: cambia la extensión sin tocar el nombre. Probar a hacer eso con os.path te quita la sonrisa.
Leer y escribir directo
from pathlib import Path
# Leer texto
contenido = Path("README.md").read_text(encoding="utf-8")
# Escribir texto
Path("salida.txt").write_text("Hola mundo", encoding="utf-8")
# Bytes
datos = Path("imagen.png").read_bytes()
Path("copia.png").write_bytes(datos)
Estas son las únicas líneas que no tienen equivalente directo en os.path — ahí necesitas open() + with. Path te ahorra el with para archivos pequeños donde no necesitas streaming.
⚠️ Cuándo NO usar
read_text(): ficheros grandes. Carga todo en memoria. Para esos sigue usandowith open(...)y procesa línea a línea.
Pero os.path no se va a ningún lado
Hay tres motivos por los que aún ves os.path por todos lados:
- Compatibilidad con código antiguo. Cualquier proyecto con más de 5 años está lleno de
os.path. Migrar todo de golpe no compensa. - Funciones que aún devuelven strings. Algunos módulos te devuelven rutas como string. Ahí o las envuelves con
Path()o las trabajas conos.pathdirectamente. pathlibno cubre 100%.shutil.rmtree,os.walkcon detalles raros, manipulaciones específicas… a veces tiras de stdlib clásica y conviven los dos estilos.
La regla práctica: escribe nuevo código con pathlib. Lee y mantén código viejo con os.path sin obsesionarte con migrar. Si una función externa te devuelve un string, conviértelo a Path la primera línea de tu función:
def procesar_csv(ruta_str):
ruta = Path(ruta_str) # de aquí en adelante, todo limpio
...
Casos reales típicos
Encontrar todos los __init__.py de un proyecto
from pathlib import Path
for init in Path(".").rglob("__init__.py"):
print(init.parent) # carpeta del paquete
Crear directorio si no existe
from pathlib import Path
salida = Path("out") / "imagenes" / "thumbs"
salida.mkdir(parents=True, exist_ok=True)
parents=True crea los padres que falten. exist_ok=True no da error si ya existe.
Cambiar la extensión de un fichero
ruta = Path("informe.docx")
nueva = ruta.with_suffix(".pdf")
# informe.pdf
Iterar todos los ficheros de un dir, ordenados por fecha de modificación
from pathlib import Path
archivos = sorted(Path("logs").iterdir(), key=lambda p: p.stat().st_mtime)
for a in archivos[-5:]: # los 5 más recientes
print(a.name, a.stat().st_size)
p.stat().st_mtime es el timestamp de modificación. Y p.stat().st_size el tamaño en bytes. Mismas APIs que os.stat() pero accedidas como método del objeto Path.
💡 ¿Vas a procesar muchas fechas? Mira manejar fechas en Python con
datetime.
Ruta del fichero actual (__file__)
Pattern muy típico cuando tu script necesita encontrar ficheros relativos a su propia ubicación:
from pathlib import Path
AQUI = Path(__file__).resolve().parent # carpeta de este script
DATOS = AQUI / "datos"
Ahora DATOS apunta siempre a la carpeta datos/ al lado de tu script, da igual desde dónde lo ejecutes.
Errores típicos al usar pathlib
# 1. Olvidar que / sobrecargado funciona solo con Path al inicio
"datos" / "peliculas" # ❌ TypeError: unsupported operand
Path("datos") / "peliculas" # ✓
# 2. Path no crea el fichero ni la carpeta — solo es la ruta
ruta = Path("nuevo.txt")
# ruta.exists() → False
# Crear el fichero requiere acción explícita: write_text, touch, etc.
# 3. Comparar Path con string (a veces sale fino, a veces no)
Path("a/b") == "a/b" # False
str(Path("a/b")) == "a/b" # True
# 4. Confundir name, stem y suffix
Path("informe.tar.gz").name # 'informe.tar.gz'
Path("informe.tar.gz").stem # 'informe.tar' ← cuidado: NO es 'informe'
Path("informe.tar.gz").suffix # '.gz' ← solo el último
Path("informe.tar.gz").suffixes # ['.tar', '.gz']
# 5. iterdir() vs glob()
list(Path(".").iterdir()) # TODO: archivos, carpetas, ocultos...
list(Path(".").glob("*")) # ojo: NO incluye los que empiezan con punto
list(Path(".").glob("*.csv")) # filtro por patrón
Resumen
| Cuándo | Qué |
|---|---|
| Código nuevo | pathlib siempre |
| Mantener código viejo | os.path sin obsesionarte |
| Función externa devuelve string | Envolver con Path(...) al entrar |
| Buscar ficheros con patrón | Path(...).glob() y .rglob() |
| Construir rutas | Path("a") / "b" / "c" |
| Cambiar extensión | ruta.with_suffix(".json") |
| Carpeta del propio script | Path(__file__).resolve().parent |
| Crear carpetas anidadas | .mkdir(parents=True, exist_ok=True) |
| Leer/escribir texto pequeño | read_text(encoding="utf-8") / write_text(...) |
¿Te ha valido esto?
Si te ha resultado útil, llévate el cheatsheet de Python en PDF — 6 páginas con tipos, condicionales, bucles, estructuras de datos, funciones y los errores típicos. Para tener al lado mientras programas. Gratis.
Sin spam. Email + cheatsheet + un correo por semana con tutoriales nuevos.
Tu siguiente paso
Si has llegado hasta aquí, ya tienes claro cuándo cada uno y cómo migrar. La próxima vez que tengas que tocar rutas, abre con Path y verás cómo el código sale más limpio en automático. Y la próxima vez que veas os.path.join(...) en código viejo, sabrás exactamente lo que hace y cómo se traduce a la versión moderna.
Si quieres aprender Python desde la base hasta automatizar tareas, procesar ficheros y trabajar con datos, en El Pythonista lo enseño paso a paso.
Un abrazo,
Oscar
¿Quieres aprender Python en orden, no a saltos?
Esto que has leído es solo una pieza. En El Pythonista lo verás todo encadenado: 11 módulos, 37+ horas de vídeo, 734 actividades y un proyecto real (MovieTracker) que crece contigo desde la primera variable hasta el deploy a producción.
37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía
