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

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.

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ónos.pathpathlib
Construir rutaos.path.join("a", "b", "c")Path("a") / "b" / "c"
Nombre del ficheroos.path.basename(p)Path(p).name
Carpeta padreos.path.dirname(p)Path(p).parent
Extensiónos.path.splitext(p)[1]Path(p).suffix
Nombre sin extensiónos.path.splitext(os.path.basename(p))[0]Path(p).stem
Ruta absolutaos.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 textoopen(p).read()Path(p).read_text()
Escribir textoopen(p, "w").write(...)Path(p).write_text(...)
Listar carpetaos.listdir(p)Path(p).iterdir()
Buscar por patrónglob.glob("*.csv")Path(".").glob("*.csv")
Crear carpetaos.makedirs(p, exist_ok=True)Path(p).mkdir(parents=True, exist_ok=True)
Borrar ficheroos.remove(p)Path(p).unlink()
Borrar carpetashutil.rmtree(p)shutil.rmtree(p) (sigue siendo el camino)
Carpeta homeos.path.expanduser("~")Path.home()
Carpeta actualos.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 en Path. Cuando ves Path("a") / "b" Python llama a Path.__truediv__("a", "b") y devuelve Path("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 usando with 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:

  1. 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.
  2. Funciones que aún devuelven strings. Algunos módulos te devuelven rutas como string. Ahí o las envuelves con Path() o las trabajas con os.path directamente.
  3. pathlib no cubre 100%. shutil.rmtree, os.walk con 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ándoQué
Código nuevopathlib siempre
Mantener código viejoos.path sin obsesionarte
Función externa devuelve stringEnvolver con Path(...) al entrar
Buscar ficheros con patrónPath(...).glob() y .rglob()
Construir rutasPath("a") / "b" / "c"
Cambiar extensiónruta.with_suffix(".json")
Carpeta del propio scriptPath(__file__).resolve().parent
Crear carpetas anidadas.mkdir(parents=True, exist_ok=True)
Leer/escribir texto pequeñoread_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.

Ver el curso completo →

37+ horas · 734 actividades · Proyecto real · Acceso de por vida · 14 días de garantía

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicar un comentario