Skip to main content

How to Download and Upload Files to an FTP Server in Python

FTP (File Transfer Protocol) is one of the oldest and most widely used protocols for transferring files between a client and a remote server. Whether you need to automate backups, sync files across systems, or integrate with legacy infrastructure, knowing how to interact with an FTP server programmatically is a valuable skill.

Python's built-in ftplib module makes it straightforward to connect to an FTP server, upload files, download files, and manage remote directories, all without installing any third-party packages.

This guide walks you through the entire process with complete, runnable examples.

What Is FTP?

File Transfer Protocol (FTP) is an application-layer protocol that runs on top of TCP. It is designed specifically for transferring files between a client and a server over a network. FTP uses two separate connections:

  • Control connection (port 21): handles commands and responses (login, directory listing, etc.).
  • Data connection (port 20): transfers the actual file data.

While FTP is widely supported, it transmits data (including credentials) in plain text. For secure transfers, consider using FTPS (FTP over TLS) via ftplib.FTP_TLS, which is also covered in this guide.

What Is the ftplib Module?

The ftplib module is part of Python's standard library, so no installation required. It provides the FTP class (and FTP_TLS for secure connections) that implements the client side of the FTP protocol. With it, you can:

  • Connect and authenticate with an FTP server
  • Upload and download files
  • List, create, rename, and delete remote directories and files
  • Switch between binary and text transfer modes

Connecting to an FTP Server

The first step in any FTP operation is establishing a connection and authenticating.

import ftplib

# FTP server credentials
HOSTNAME = "ftp.example.com"
USERNAME = "user@example.com"
PASSWORD = "password"

# Connect and authenticate
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD)

# Force UTF-8 encoding for file names
ftp_server.encoding = "utf-8"

# Print the welcome message from the server
print(ftp_server.getwelcome())

Sample output:

220 Welcome to the Test FTP Server
caution
  • Never hardcode real credentials in your source code. Use environment variables or a .env file instead.

Using Environment Variables for Credentials

import os
import ftplib

HOSTNAME = os.getenv("FTP_HOST", "ftp.example.com")
USERNAME = os.getenv("FTP_USER", "user@example.com")
PASSWORD = os.getenv("FTP_PASS", "password")

ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD)
ftp_server.encoding = "utf-8"

Uploading a File to an FTP Server

To upload a file, use the storbinary() method, which transfers data in binary mode. The command passed to the method follows the format STOR <filename>.

import ftplib

HOSTNAME = "ftp.example.com"
USERNAME = "user@example.com"
PASSWORD = "password"

# Connect to the server
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD)
ftp_server.encoding = "utf-8"

# File to upload
filename = "example.txt"

# Open the file in binary read mode and upload
with open(filename, "rb") as file:
ftp_server.storbinary(f"STOR {filename}", file)

print(f"'{filename}' uploaded successfully.")

# List remote directory to confirm
print("\nRemote directory contents:")
ftp_server.dir()

# Close the connection
ftp_server.quit()

Sample output:

'example.txt' uploaded successfully.

Remote directory contents:
-rw-r--r-- 1 0 0 25 Jul 23 12:00 example.txt

How It Works

  1. The file is opened in binary read mode ("rb").
  2. storbinary(command, file) reads the file object and sends its contents to the server.
  3. The STOR command tells the FTP server to store the incoming data as a file with the given name.
  4. ftp_server.dir() lists the remote directory contents to verify the upload.

Downloading a File from an FTP Server

To download a file, use the retrbinary() method with the RETR <filename> command. The method accepts a callback function that processes each chunk of data, typically file.write.

import ftplib

HOSTNAME = "ftp.example.com"
USERNAME = "user@example.com"
PASSWORD = "password"

# Connect to the server
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD)
ftp_server.encoding = "utf-8"

# File to download
filename = "example.txt"

# Open a local file in binary write mode and download
with open(filename, "wb") as file:
ftp_server.retrbinary(f"RETR {filename}", file.write)

print(f"'{filename}' downloaded successfully.")

# Display the file contents to verify
with open(filename, "r") as file:
print(f"\nFile content:\n{file.read()}")

# Close the connection
ftp_server.quit()

Sample output:

'example.txt' downloaded successfully.

File content:
Hello from Tutorial Reference!

How It Works

  1. A local file is opened in binary write mode ("wb").
  2. retrbinary(command, callback) retrieves the remote file in chunks and passes each chunk to the callback (file.write).
  3. After the download completes, the file is read back in text mode to verify its contents.

Listing Remote Files and Directories

Before uploading or downloading, you may want to inspect the remote directory structure:

import ftplib

ftp_server = ftplib.FTP("ftp.example.com", "user@example.com", "password")
ftp_server.encoding = "utf-8"

# Detailed directory listing
print("Detailed listing:")
ftp_server.dir()

print()

# Simple list of file names
print("File names only:")
files = ftp_server.nlst()
for f in files:
print(f" {f}")

ftp_server.quit()
MethodDescription
ftp.dir()Prints a detailed directory listing (like ls -l)
ftp.nlst()Returns a list of file and directory names
ftp.mlsd()Returns an iterable of (name, facts) tuples with metadata

You can change the working directory, create new directories, and remove them:

import ftplib

ftp_server = ftplib.FTP("ftp.example.com", "user@example.com", "password")
ftp_server.encoding = "utf-8"

# Print current directory
print(f"Current directory: {ftp_server.pwd()}")

# Create a new directory
ftp_server.mkd("test_folder")

# Change into the new directory
ftp_server.cwd("test_folder")
print(f"Changed to: {ftp_server.pwd()}")

# Go back to the parent directory
ftp_server.cwd("..")

# Remove the directory (must be empty)
ftp_server.rmd("test_folder")

ftp_server.quit()

Adding Error Handling

Production code should handle connection failures, authentication errors, and missing files gracefully:

import ftplib

HOSTNAME = "ftp.example.com"
USERNAME = "user@example.com"
PASSWORD = "password"

try:
# Connect to the FTP server
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD, timeout=10)
ftp_server.encoding = "utf-8"
print("Connected successfully.")

# Attempt to download a file
filename = "example.txt"
with open(filename, "wb") as file:
ftp_server.retrbinary(f"RETR {filename}", file.write)
print(f"'{filename}' downloaded successfully.")

except ftplib.error_perm as e:
print(f"FTP permission error: {e}")

except ftplib.all_errors as e:
print(f"FTP error: {e}")

except FileNotFoundError:
print(f"Local file path is invalid.")

finally:
try:
ftp_server.quit()
print("Connection closed.")
except Exception:
pass
tip

ftplib.error_perm is raised for permanent errors like invalid credentials (530), file not found (550), or permission denied. ftplib.all_errors catches all FTP-related exceptions.

Common Mistake: Forgetting to Close the Connection

Leaving an FTP connection open wastes server resources and may cause your program to hang:

# ❌ Wrong: connection is never closed
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD)
ftp_server.retrbinary(f"RETR {filename}", open(filename, "wb").write)
# Program ends without calling quit()

The correct approach is to always close the connection, ideally using a try/finally block or a context manager:

# ✅ Correct: connection is always closed
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD)
try:
with open(filename, "wb") as file:
ftp_server.retrbinary(f"RETR {filename}", file.write)
finally:
ftp_server.quit()

Using FTPS for Secure Transfers

Standard FTP sends credentials and data in plain text, which is a security risk. For encrypted transfers, use ftplib.FTP_TLS:

import ftplib

HOSTNAME = "ftp.example.com"
USERNAME = "user"
PASSWORD = "password"

# Connect using FTP over TLS
ftp_server = ftplib.FTP_TLS(HOSTNAME, USERNAME, PASSWORD)

# Secure the data connection as well
ftp_server.prot_p()

ftp_server.encoding = "utf-8"

# Now upload/download as usual
ftp_server.dir()

ftp_server.quit()
danger

Never use plain FTP for transferring sensitive data over the internet. Always prefer FTPS (FTP_TLS) or SFTP (which uses SSH and is available via the third-party paramiko library).

Complete Example: Upload and Download

Here is a complete, self-contained script that demonstrates both uploading and downloading a file:

import ftplib
import os

# Configuration
HOSTNAME = "ftp.example.com"
USERNAME = "user@example.com"
PASSWORD = "password"
FILENAME = "test_upload.txt"

def create_sample_file(filename):
"""Create a sample text file for testing."""
with open(filename, "w") as f:
f.write("Hello! This file was uploaded via Python ftplib.\n")
print(f"Created local file: {filename}")

def upload_file(ftp, filename):
"""Upload a file to the FTP server."""
with open(filename, "rb") as file:
ftp.storbinary(f"STOR {filename}", file)
print(f"Uploaded: {filename}")

def download_file(ftp, remote_filename, local_filename):
"""Download a file from the FTP server."""
with open(local_filename, "wb") as file:
ftp.retrbinary(f"RETR {remote_filename}", file.write)
print(f"Downloaded: {remote_filename} -> {local_filename}")

def main():
# Step 1: Create a sample file
create_sample_file(FILENAME)

try:
# Step 2: Connect to the FTP server
ftp_server = ftplib.FTP(HOSTNAME, USERNAME, PASSWORD, timeout=15)
ftp_server.encoding = "utf-8"
print(f"Connected to {HOSTNAME}")

# Step 3: Upload the file
upload_file(ftp_server, FILENAME)

# Step 4: List remote files
print("\nRemote directory:")
ftp_server.dir()

# Step 5: Download the file with a different local name
downloaded_name = "downloaded_copy.txt"
download_file(ftp_server, FILENAME, downloaded_name)

# Step 6: Verify the download
with open(downloaded_name, "r") as f:
print(f"\nDownloaded file content:\n{f.read()}")

except ftplib.all_errors as e:
print(f"FTP error: {e}")

finally:
ftp_server.quit()
print("Connection closed.")

# Clean up local files
for f in [FILENAME, "downloaded_copy.txt"]:
if os.path.exists(f):
os.remove(f)

if __name__ == "__main__":
main()

Quick Reference: Key ftplib Methods

MethodDescription
FTP(host, user, passwd)Connect and authenticate
storbinary(cmd, file)Upload a file in binary mode
retrbinary(cmd, callback)Download a file in binary mode
dir()Print detailed directory listing
nlst()Return list of file names
cwd(path)Change working directory
pwd()Print current working directory
mkd(dirname)Create a directory
rmd(dirname)Remove a directory
delete(filename)Delete a file
rename(old, new)Rename a file or directory
quit()Close the connection gracefully

Conclusion

Python's built-in ftplib module provides everything you need to interact with FTP servers, from simple file uploads and downloads to directory management and secure FTPS connections:

  • Use storbinary() to upload files and retrbinary() to download them.
  • Always close connections with quit() in a finally block.
  • Add error handling with ftplib.all_errors and ftplib.error_perm for robust production code.
  • Use FTP_TLS instead of plain FTP when security matters.
  • Store credentials in environment variables, not in your source code.

With these techniques, you can automate file transfers, build backup systems, or integrate with any FTP-based infrastructure reliably and securely.