ftplib en Python: FTP y FTPS seguros

Publicado el: 21/08/2026
Tempo de leitura: 5 minutos
Close-up view of a computer screen displaying code in a software development environment.

El módulo ftplib implementa el lado cliente de FTP en la biblioteca estándar de Python. Permite listar directorios, descargar, subir, renombrar y eliminar archivos en servidores compatibles. FTP todavía aparece en integraciones heredadas, hosting, equipos y flujos empresariales de intercambio de archivos.

FTP simple transmite credenciales y datos sin cifrado. En redes no confiables, prefiere FTPS mediante FTP_TLS. Si el servidor exige SFTP, utiliza una biblioteca específica para SSH: SFTP no es FTP y no está implementado por ftplib. Esta guía prioriza FTPS, timeouts, límites de tamaño y validación.

Conexión FTP básica

from ftplib import FTP

with FTP("ftp.example.com", timeout=15, encoding="utf-8") as ftp:
    ftp.login("usuario", "contraseña")
    print(ftp.pwd())
    print(ftp.nlst())

Este ejemplo utiliza FTP sin cifrado y solo es apropiado para una red controlada o un servidor anónimo. Nunca envíes una contraseña real por FTP simple a través de internet.

FTPS con FTP_TLS

import ssl
from ftplib import FTP_TLS

contexto = ssl.create_default_context()
contexto.minimum_version = ssl.TLSVersion.TLSv1_2

with FTP_TLS(
    "ftp.example.com",
    timeout=15,
    context=contexto,
    encoding="utf-8",
) as ftps:
    ftps.login("usuario", "contraseña")
    ftps.prot_p()
    print(list(ftps.mlsd()))

FTP_TLS protege el canal de control, pero el canal de datos solo queda privado después de prot_p(). Sin esa llamada, listados y archivos pueden viajar sin el cifrado esperado.

El contexto por defecto valida certificado y hostname. No uses un contexto sin verificación para corregir certificados caducados o nombres incorrectos. Consulta ssl en Python.

Credenciales

No coloques usuario y contraseña en el código fuente. Cárgalos desde variables de entorno o un gestor de secretos. Un archivo .netrc puede ayudar en herramientas locales, pero también contiene secretos y necesita permisos restrictivos. La guía de netrc en Python explica los cuidados.

No actives debug del protocolo en producción con cuentas reales. set_debuglevel(2) imprime comandos y respuestas y puede exponer información sensible.

Listado estructurado con MLSD

Cuando el servidor lo admite, mlsd() es preferible a interpretar texto libre de LIST.

with FTP_TLS(HOST, context=contexto, timeout=15) as ftps:
    ftps.login(USER, PASSWORD)
    ftps.prot_p()
    for nombre, hechos in ftps.mlsd(
        "/entrada",
        facts=["type", "size", "modify"],
    ):
        print(nombre, hechos)

El servidor no está obligado a devolver todos los datos solicitados. Usa hechos.get("size") y valida el texto antes de convertirlo.

NLST y LIST

nlst() devuelve nombres, mientras dir() y retrlines("LIST") producen texto dependiente del servidor. No extraigas tamaño y fecha mediante columnas fijas; los formatos varían entre Unix, Windows y productos FTP.

Descargar en bloques

from pathlib import Path

DESTINO = Path("informe.csv")
LIMITE = 50 * 1024 * 1024
recibido = 0

def escribir_bloque(bloque: bytes) -> None:
    global recibido
    recibido += len(bloque)
    if recibido > LIMITE:
        raise ValueError("el archivo supera el límite")
    salida.write(bloque)

with DESTINO.open("wb") as salida:
    ftps.retrbinary(
        "RETR informe.csv",
        escribir_bloque,
        blocksize=64 * 1024,
    )

Escribe en un archivo temporal y renombra solo después del éxito. Si la transferencia falla, el nombre final no debe apuntar a contenido parcial.

Descarga atómica

from pathlib import Path
from tempfile import NamedTemporaryFile

final = Path("informe.csv")

with NamedTemporaryFile(
    mode="wb",
    dir=final.parent,
    delete=False,
) as temporal:
    ruta_temp = Path(temporal.name)
    ftps.retrbinary("RETR informe.csv", temporal.write)

ruta_temp.replace(final)

Añade manejo de excepciones que elimine el temporal si ocurre un error. La guía de tempfile en Python muestra patrones seguros.

Verificación de integridad

FTP y FTPS no demuestran que un archivo sea el artefacto esperado. Compara tamaño, un hash publicado por canal confiable o una firma digital.

import hashlib

with open("informe.csv", "rb") as archivo:
    digest = hashlib.file_digest(archivo, "sha256").hexdigest()

if digest != SHA256_ESPERADO:
    raise ValueError("hash diferente")

Consulta hashlib en Python. Un hash descargado del mismo servidor comprometido no ofrece autenticación independiente.

Subir archivos

from pathlib import Path

ruta = Path("salida.zip")

if ruta.stat().st_size > 100 * 1024 * 1024:
    raise ValueError("archivo demasiado grande")

with ruta.open("rb") as archivo:
    ftps.storbinary(
        "STOR salida.zip.part",
        archivo,
        blocksize=64 * 1024,
    )

ftps.rename("salida.zip.part", "salida.zip")

Subir con nombre temporal evita que consumidores vean un archivo antes de terminar. Confirma si el rename es atómico en el servidor.

Modo texto frente a modo binario

Usa retrbinary() y storbinary() para casi todos los archivos, incluidos CSV y texto, cuando importan los bytes exactos. retrlines() y storlines() aplican semántica de líneas y pueden cambiar finales.

Encoding de nombres

Desde Python 3.9, el valor por defecto es UTF-8 conforme a RFC 2640. Servidores antiguos pueden usar otra codificación. Configura encoding únicamente desde el contrato y no pruebes codecs arbitrarios silenciosamente.

Normaliza Unicode cuando la aplicación local necesite comparación consistente, pero conserva el nombre remoto para operaciones remotas. Nunca uses un nombre remoto directamente como ruta local sin validación.

Proteger rutas locales

from pathlib import Path

RAIZ = Path("descargas").resolve()

def destino_seguro(nombre_remoto: str) -> Path:
    nombre = Path(nombre_remoto).name
    if nombre in {"", ".", ".."}:
        raise ValueError("nombre inválido")
    destino = (RAIZ / nombre).resolve()
    if RAIZ not in destino.parents:
        raise ValueError("path traversal")
    return destino

Rechaza separadores, nombres reservados y caracteres de control según el sistema local.

Directorios remotos

cwd(), mkd(), rmd() y pwd() manipulan directorios remotos. Evita montar comandos con rutas no validadas. No existe un escaping universal que haga seguro cualquier nombre.

Modo pasivo

El modo pasivo está activado por defecto y suele funcionar mejor con NAT y firewalls. set_pasv(False) selecciona modo activo, que puede requerir conexiones entrantes al cliente. Decide con el equipo de red y restringe rangos de puertos.

Reanudar transferencias

retrbinary() y storbinary() aceptan rest, normalmente un offset de bytes. El servidor puede no admitirlo.

offset = ruta_temp.stat().st_size
with ruta_temp.open("ab") as archivo:
    ftps.retrbinary(
        "RETR imagen.iso",
        archivo.write,
        rest=offset,
    )

Antes de reanudar, verifica que el archivo remoto no cambió mediante tamaño, fecha y, de ser posible, hash. De lo contrario, el archivo local puede combinar versiones distintas.

Excepciones de ftplib

Las clases principales son error_temp para respuestas 4xx temporales, error_perm para 5xx permanentes, error_reply y error_proto. all_errors también incluye fallos de socket.

from ftplib import error_perm, error_temp

try:
    ftps.retrbinary("RETR archivo.dat", callback)
except error_temp as error:
    print("fallo posiblemente temporal", error)
except error_perm as error:
    print("permiso denegado o archivo ausente", error)

No repitas automáticamente errores permanentes. Para temporales, usa backoff, jitter y límite de intentos.

Timeouts

Configura timeout en la conexión. También controla duración total y tamaño. Una transferencia que sigue recibiendo bloques pequeños puede durar indefinidamente sin disparar timeout de inactividad.

Cerrar conexiones

Usa context manager. quit() envía QUIT de forma educada, pero puede fallar si la conexión ya está rota. La limpieza del contexto cierra recursos. No reutilices la instancia después de quit() o close().

FTP, FTPS y SFTP

FTP es el protocolo original sin cifrado. FTPS añade TLS a FTP y es compatible con FTP_TLS. SFTP es un protocolo distinto sobre SSH. Confundirlos causa errores de conexión y decisiones inseguras.

Pruebas recomendadas

Prueba login inválido, certificado inválido, ausencia de prot_p(), nombres Unicode, servidor sin MLSD, archivo vacío, límite excedido, conexión interrumpida, reanudación no soportada, rename, errores temporales y permanentes y limpieza del temporal.

Errores comunes

Los fallos frecuentes son usar FTP simple con contraseña, olvidar prot_p(), desactivar TLS, interpretar LIST por columnas fijas, confiar en nombres remotos, escribir directamente al archivo final, omitir límites, registrar credenciales, repetir uploads no idempotentes y confundir FTPS con SFTP.

Conclusión

ftplib cubre integraciones FTP y FTPS sin dependencias externas. Usa FTP_TLS con contexto validado y prot_p(), prefiere mlsd(), transfiere bloques a archivos temporales, verifica tamaño e integridad, protege rutas y clasifica errores.

Consulta la documentación oficial de ftplib, el RFC 959 de FTP y el RFC 4217 de FTPS. Para sistemas nuevos, considera protocolos más sencillos de proteger y operar.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Rack de servidores que representa un endpoint creado con xmlrpc.server en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    xmlrpc.server: crea servidores XML-RPC

    Aprende xmlrpc.server en Python para crear servidores XML-RPC, registrar funciones, limitar métodos y rutas y evitar exposición insegura.

    Ler mais

    Tempo de leitura: 6 minutos
    21/08/2026
    Código de error sobre datos binarios que representa fallos manejados con urllib.error en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    urllib.error en Python: maneja errores HTTP

    Aprende urllib.error en Python para manejar URLError, HTTPError, descargas incompletas, retries selectivos y diagnósticos de red claros.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Teclas con la palabra HTML que representan entidades HTML en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    html.entities: convierte entidades HTML

    Aprende html.entities en Python para consultar entidades HTML, convertir nombres y code points y no confundir decodificación con sanitización.

    Ler mais

    Tempo de leitura: 7 minutos
    21/08/2026
    Carpeta con archivos que representa tipos MIME identificados con mimetypes en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    mimetypes: detecta tipos MIME de archivos

    Aprende mimetypes en Python para identificar tipos de archivo, validar cargas y definir Content-Type con más seguridad.

    Ler mais

    Tempo de leitura: 6 minutos
    20/08/2026
    Código HTML en una pantalla que representa análisis con html.parser en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    html.parser en Python: analiza HTML

    Aprende html.parser en Python para extraer texto, enlaces y metadatos, procesar HTML por bloques y no confundir parsing con sanitización.

    Ler mais

    Tempo de leitura: 4 minutos
    20/08/2026
    Persona usando un portátil en una sesión web que representa cookies con http.cookiejar en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    http.cookiejar en Python: gestiona cookies

    Aprende http.cookiejar en Python para mantener sesiones, aplicar políticas, persistir cookies de forma segura e integrar urllib.request.

    Ler mais

    Tempo de leitura: 5 minutos
    20/08/2026