Working with FTP Servers Using Python's ftplib Module
Overview
ftplib is Python’s standard library for interacting with FTP (File Transfer Protocol) servers. It handles the FTP protocol’s command-response pattern, letting you connect to servers, navigate directory trees, upload and download files, and manage passive/active modes. For most modern use cases involving file transfers, you might reach for paramiko (SFTP/SCP) or cloud storage SDKs instead — but for plain anonymous or credential-based FTP, ftplib gets the job done without extra dependencies.
Connection
Basic anonymous connection
from ftplib import FTP
ftp = FTP("ftp.example.com")
ftp.login() # anonymous login with empty user/pass
print(ftp.getwelcome())
Connecting with credentials
When you need authenticated access, pass a username and password to the login method. You can either call FTP(host) followed by login() on separate lines, or use connect() explicitly with a port number before authenticating:
from ftplib import FTP
ftp = FTP("ftp.example.com")
ftp.login(user="alice", passwd="secret123")
# or:
ftp.connect("ftp.example.com", 21)
ftp.login(user="alice", passwd="secret123")
Context manager (Python 3.2+)
from ftplib import FTP
with FTP("ftp.example.com") as ftp:
ftp.login(user="alice", passwd="secret123")
print(ftp.pwd())
# Connection closes automatically
Using TLS (FTPS)
For secure connections, use FTP_TLS instead of plain FTP. After logging in, call prot_p() to encrypt the data channel — without this call, your credentials go over the wire securely but file transfers remain in plaintext:
from ftplib import FTP_TLS
with FTP_TLS("ftp.example.com") as ftp:
ftp.login(user="alice", passwd="secret123")
ftp.prot_p() # switch to protected data channel
print(ftp.pwd())
Navigating Directories
Once connected, you can move around the remote filesystem. The pwd command prints the current working directory, cwd changes into a target directory, and retrlines("LIST") or dir() shows the contents of the current working directory:
ftp.pwd() # current directory
ftp.cwd("/pub/incoming") # change directory
ftp.retrlines("LIST") # list files as text
ftp.dir() # same as retrlines("LIST")
Listing files
There are several ways to list directory contents depending on what information you need. The LIST command returns human-readable details like permissions, owner, size, and modification date, while nlst returns only filenames as a Python list, which is easier to iterate over programmatically:
# Simple listing
ftp.retrlines("LIST")
# Detailed listing
ftp.retrlines("LIST -la")
# Use nlst for just filenames
files = ftp.nlst()
print(files) # ['file1.txt', 'subdir', 'report.pdf']
Downloading Files
Text mode (retrlines)
from ftplib import FTP
with FTP("ftp.example.com") as ftp:
ftp.login()
ftp.cwd("/pub/docs")
# Read into a list of strings
lines = []
ftp.retrlines("RETR readme.txt", lines.append)
print("\n".join(lines))
Binary mode (retrbinary)
For binary files like images, PDFs, or archives, use retrbinary with a callback that writes chunks to a file opened in "wb" mode. This method transfers data in blocks rather than line by line, making it suitable for any non-text content:
with FTP("ftp.example.com") as ftp:
ftp.login()
ftp.cwd("/pub/images")
with open("photo.jpg", "wb") as f:
ftp.retrbinary("RETR photo.jpg", f.write)
Download with progress tracking
def track_progress(data, f):
f.write(data)
print(".", end="", flush=True)
with FTP("ftp.example.com") as ftp:
ftp.login()
with open("largefile.zip", "wb") as f:
ftp.retrbinary("RETR largefile.zip", lambda d: track_progress(d, f))
Uploading Files
Binary upload (storbinary)
with FTP("ftp.example.com") as ftp:
ftp.login()
ftp.cwd("/pub/incoming")
with open("upload.txt", "rb") as f:
ftp.storbinary("STOR upload.txt", f)
Text upload (storlines)
When you have text content already in memory — such as a list of strings — storlines sends each line to the server as part of the STOR command, making it convenient for small text files that do not need to exist on disk first:
lines = ["First line", "Second line", "Third line"]
with FTP("ftp.example.com") as ftp:
ftp.login()
ftp.cwd("/pub/incoming")
ftp.storlines("STOR notes.txt", lines)
Upload from a file object
You can also upload directly from an open file handle. This is the most common pattern when you already have the file on disk and want to push it to the remote server in binary mode:
with open("data.csv", "rb") as f:
with FTP("ftp.example.com") as ftp:
ftp.login()
ftp.storbinary("STOR data.csv", f)
Renaming and Deleting
ftp.rename("old_name.txt", "new_name.txt") # rename
ftp.delete("unwanted.txt") # delete file
ftp.mkd("/new_directory") # create directory
ftp.rmd("/empty_directory") # remove empty directory
Transfer Mode
FTP has two transfer modes that matter in ftplib:
Passive mode — The client tells the server which port to use for data transfer. This is the default in Python’s ftplib and works better through firewalls and NAT:
ftp = FTP("ftp.example.com")
ftp.login()
ftp.set_pasv(True) # explicitly enable passive (default)
Active mode — The server opens a connection back to the client. This can fail behind firewalls or NAT:
ftp.set_pasv(False) # disable passive = active mode
You rarely need to switch to active mode in practice. Passive mode is the default for good reason — it lets the client initiate both the control and data connections, which works reliably through most network configurations. Active mode requires the server to connect back to the client, which corporate firewalls and home routers typically block.
Error Handling
from ftplib import FTP, FTP_TLS, all_errors
import ftplib
try:
with FTP("ftp.example.com", timeout=10) as ftp:
ftp.login(user="alice", passwd="secret123")
files = ftp.nlst()
except ftplib.error_perm as e:
print(f"Permission error: {e}")
except ftplib.error_temp as e:
print(f"Temporary error (retry): {e}")
except ftplib.all_errors as e:
print(f"FTP error: {e}")
Common exceptions:
error_perm— 4xx errors (authentication failed, file not found)error_temp— 4xx transient errors (try again)error_proto— protocol-level errors
Downloading a File List with a Timeout
Setting a timeout on the FTP connection prevents your script from hanging indefinitely if the server becomes unresponsive. The timeout applies to both the initial connection and subsequent data transfers, and you can wrap directory operations in a try/except block to handle transient server errors gracefully:
import ftplib
def fetch_filelist(ftp, dir_path):
ftp.cwd(dir_path)
files = []
ftp.retrlines("LIST", files.append)
return files
with FTP("ftp.example.com", timeout=30) as ftp:
ftp.login()
try:
file_list = fetch_filelist(ftp, "/pub")
for line in file_list:
print(line)
except ftplib.error_temp:
print("Server busy, try again later")
See Also
- /reference/modules/socket-module/ — lower-level socket networking
- /reference/modules/os-module/ — file and directory operations
- /tutorials/python-fundamentals/file-io/ — reading and writing files in Python