The ftplib module implements the client side of FTP in Python’s standard library. It can list directories, download, upload, rename, and delete files on compatible servers. FTP still appears in legacy integrations, hosting systems, appliances, and enterprise file exchanges.
Plain FTP sends credentials and data without encryption. On untrusted networks, prefer FTPS through FTP_TLS. If a server requires SFTP, use an SSH-specific library: SFTP is not FTP and is not implemented by ftplib. This guide prioritizes FTPS, timeouts, size limits, and validation.
A basic FTP connection
from ftplib import FTP
with FTP("ftp.example.com", timeout=15, encoding="utf-8") as ftp:
ftp.login("user", "password")
print(ftp.pwd())
print(ftp.nlst())
This example uses unencrypted FTP and is appropriate only for a controlled network or anonymous server. Never send a real password over plain FTP across the internet.
FTPS with FTP_TLS
import ssl
from ftplib import FTP_TLS
context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
with FTP_TLS(
"ftp.example.com",
timeout=15,
context=context,
encoding="utf-8",
) as ftps:
ftps.login("user", "password")
ftps.prot_p()
print(list(ftps.mlsd()))
FTP_TLS protects the control channel, but the data channel becomes private only after prot_p(). Without that call, directory listings and file contents may travel without the expected encryption.
The default context verifies the certificate and hostname. Do not use an unverified context as a workaround for expired or mismatched certificates. Read Python ssl.
Credentials
Do not put usernames and passwords in source code. Load them from environment variables or a secret manager. A .netrc file can help local tools, but it also contains secrets and needs restrictive permissions. The guide to Python netrc explains the trade-offs.
Do not enable protocol debug output in production with real accounts. set_debuglevel(2) prints commands and responses and can expose sensitive information.
Structured listings with MLSD
When supported, mlsd() is preferable to parsing free-form LIST output.
with FTP_TLS(HOST, context=context, timeout=15) as ftps:
ftps.login(USER, PASSWORD)
ftps.prot_p()
for name, facts in ftps.mlsd(
"/incoming",
facts=["type", "size", "modify"],
):
print(name, facts)
A server is not required to return every requested fact. Use facts.get("size") and validate text before converting it.
NLST and LIST
nlst() returns names, while dir() and retrlines("LIST") produce server-dependent text. Do not extract size and date from fixed positions in LIST output; formats differ across Unix, Windows, and FTP products.
Downloading in blocks
from pathlib import Path
DESTINATION = Path("report.csv")
LIMIT = 50 * 1024 * 1024
received = 0
def write_block(block: bytes) -> None:
global received
received += len(block)
if received > LIMIT:
raise ValueError("file exceeds limit")
output.write(block)
with DESTINATION.open("wb") as output:
ftps.retrbinary(
"RETR report.csv",
write_block,
blocksize=64 * 1024,
)
Write to a temporary file and rename only after success. If the transfer fails, the final filename should not point to partial content.
Atomic downloads
from pathlib import Path
from tempfile import NamedTemporaryFile
final = Path("report.csv")
with NamedTemporaryFile(
mode="wb",
dir=final.parent,
delete=False,
) as temporary:
temp_path = Path(temporary.name)
ftps.retrbinary("RETR report.csv", temporary.write)
temp_path.replace(final)
Add exception handling that deletes the temporary file on failure. The guide to Python tempfile shows safe patterns.
Integrity verification
FTP and FTPS do not prove that a file is the expected artifact. Compare size, a hash published through a trusted channel, or a digital signature.
import hashlib
with open("report.csv", "rb") as file:
digest = hashlib.file_digest(file, "sha256").hexdigest()
if digest != EXPECTED_SHA256:
raise ValueError("hash mismatch")
See Python hashlib. A hash downloaded from the same compromised server is not independent authentication.
Uploading files
from pathlib import Path
path = Path("output.zip")
if path.stat().st_size > 100 * 1024 * 1024:
raise ValueError("file too large")
with path.open("rb") as file:
ftps.storbinary(
"STOR output.zip.part",
file,
blocksize=64 * 1024,
)
ftps.rename("output.zip.part", "output.zip")
Uploading with a temporary remote name prevents consumers from seeing a file before the transfer completes. Confirm whether rename is atomic on the target server.
Text mode versus binary mode
Use retrbinary() and storbinary() for almost every file, including CSV and text, when exact bytes matter. retrlines() and storlines() apply line-oriented semantics and may alter line endings.
Filename encoding
Since Python 3.9, the default is UTF-8 according to RFC 2640. Older servers may use another encoding. Configure encoding only from the service contract; do not silently try arbitrary codecs.
Normalize Unicode when the local application needs consistent comparison, but preserve the remote name for remote operations. Never use a remote name directly as a local path without validation.
Protecting local paths
from pathlib import Path
ROOT = Path("downloads").resolve()
def safe_destination(remote_name: str) -> Path:
name = Path(remote_name).name
if name in {"", ".", ".."}:
raise ValueError("invalid name")
destination = (ROOT / name).resolve()
if ROOT not in destination.parents:
raise ValueError("path traversal")
return destination
Reject path separators, reserved names, and control characters according to the local operating system.
Remote directories
cwd(), mkd(), rmd(), and pwd() manipulate remote directories. Avoid assembling commands from unvalidated paths. There is no universal escaping rule that makes every remote name safe.
Passive mode
Passive mode is enabled by default and generally works better through NAT and firewalls. set_pasv(False) selects active mode, which may require inbound connections to the client. Choose the mode with the network team and restrict server-side port ranges.
Resuming transfers
retrbinary() and storbinary() accept rest, usually a byte offset. The server may not support it.
offset = temp_path.stat().st_size
with temp_path.open("ab") as file:
ftps.retrbinary(
"RETR image.iso",
file.write,
rest=offset,
)
Before resuming, verify that the remote file has not changed using size, modification data, and ideally a hash. Otherwise, the local file can combine different versions.
ftplib exceptions
Main exceptions include error_temp for temporary 4xx replies, error_perm for permanent 5xx replies, error_reply, and error_proto. all_errors also includes socket failures.
from ftplib import error_perm, error_temp
try:
ftps.retrbinary("RETR file.dat", callback)
except error_temp as error:
print("possibly temporary failure", error)
except error_perm as error:
print("permission denied or missing file", error)
Do not retry permanent errors automatically. For temporary failures, use backoff, jitter, and an attempt limit.
Timeouts
Set timeout on the connection. Also control the total task duration and file size. A transfer that keeps receiving small chunks may run indefinitely without triggering a socket inactivity timeout.
Closing connections
Use a context manager. quit() sends a polite QUIT command but may fail if the connection is already broken. Context cleanup still closes resources. Never reuse an instance after quit() or close().
FTP, FTPS, and SFTP
FTP is the original unencrypted protocol. FTPS adds TLS to FTP and is supported by FTP_TLS. SFTP is a different protocol over SSH. Confusing these names causes connection errors and unsafe architecture decisions.
Recommended tests
Test invalid login, invalid certificate, missing prot_p(), Unicode names, servers without MLSD, empty files, size limit, interrupted connection, unsupported resume, rename, temporary and permanent errors, and temporary-file cleanup.
Common mistakes
Frequent mistakes include using plain FTP with passwords, forgetting prot_p(), disabling TLS verification, parsing LIST with fixed columns, trusting remote names, writing directly to the final file, omitting size limits, logging credentials, retrying non-idempotent uploads, and confusing FTPS with SFTP.
Conclusion
ftplib supports FTP and FTPS integrations without external dependencies. Use FTP_TLS with a verified context and prot_p(), prefer mlsd(), transfer blocks into temporary files, verify size and integrity, protect paths, and classify errors.
Read the official ftplib documentation, FTP RFC 959, and FTPS RFC 4217. For new systems, consider protocols that are easier to secure and operate.







