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.







