Files.comExaVault

Python FTP: Uploading, Downloading, and Securing Transfers With ftplib

FTP & SFTP

Python FTP is one of the few transfer protocols the standard library handles on its own, and ftplib is why so many nightly jobs still speak FTP to a server set up years ago. Here is how to upload, download, and list with ftplib, how to encrypt it properly with FTP_TLS, and a third way where Files.com makes the FTP or FTPS connection on the cloud side and your Python only talks HTTPS.

Python FTP is one of the few transfer protocols the standard library handles on its own. ftplib has shipped with Python for decades, needs no install, and is the reason a lot of nightly jobs still speak FTP to a server somebody set up in 2011.

FTP itself is old, awkward around firewalls, and insecure unless you add TLS, and it is also still everywhere.

Here is how to upload, download, and list files with ftplib, how to encrypt the connection properly with FTP_TLS so credentials and data stop crossing the network in the clear, and a third way that takes the protocol out of the Python entirely: Files.com makes the FTP or FTPS connection to the other server, and your code talks to Files.com over HTTPS.

Uploading, downloading, and listing with ftplib

A complete upload, with the credentials read from the environment rather than the source:

import os
from ftplib import FTP

with FTP("ftp.example.com") as ftp:
    ftp.login(user=os.environ["FTP_USERNAME"], passwd=os.environ["FTP_PASSWORD"])
    ftp.cwd("/inbound")

    with open("/data/exports/orders.csv", "rb") as f:
        ftp.storbinary("STOR orders.csv", f)

FTP(host) connects on port 21. login authenticates, cwd changes the remote directory, and storbinary uploads: its first argument is the literal FTP command, STOR followed by the destination filename, and its second is an open file in binary mode. The with blocks close both the connection and the local file, including when an exception is raised. For a nonstandard port, construct FTP() with no arguments and call ftp.connect("ftp.example.com", port=2121) before login.

Downloading is the mirror image. retrbinary receives the file in blocks and hands each block to a callback, and the local file's write method is the callback:

with FTP("ftp.example.com") as ftp:
    ftp.login(user=os.environ["FTP_USERNAME"], passwd=os.environ["FTP_PASSWORD"])

    with open("/data/imports/report.csv", "wb") as f:
        ftp.retrbinary("RETR /outbound/report.csv", f.write)

Listing a directory takes one of two forms. nlst returns names only. mlsd returns each name with facts such as size and modification time, on servers that support the MLSD command, which most modern ones do:

for name in ftp.nlst("/outbound"):
    print(name)

for name, facts in ftp.mlsd("/outbound"):
    print(name, facts.get("size"), facts.get("modify"))

Binary mode, passive mode, and the firewall

Use storbinary and retrbinary for everything, including CSV, JSON, and XML. Their line-oriented cousins storlines and retrlines translate line endings, which means the bytes that arrive may not match the bytes you sent, and a CSV with one unexpected byte in it is corrupt. Binary transfers avoid the ambiguity.

An FTP session uses one connection for commands and another for file data, and that design is behind most FTP firewall problems. ftplib uses passive mode by default, which is what you want: the client opens both connections to the server, so your side never has to accept an inbound connection.

The server still has to expose a passive port range, and every FTP server publishes one.

If a script connects, logs in, and then hangs on the first LIST, STOR, or RETR, a blocked passive port between you and the server is the first thing to check. The FTP port guide covers the control and data ports in detail.

Upload to a temporary name, then rename

If another system watches /inbound and starts processing a file the moment it appears, uploading straight to orders.csv lets that system open a half-written file. Upload under a temporary name and rename it once the transfer succeeds:

with open("/data/exports/orders.csv", "rb") as f:
    ftp.storbinary("STOR orders.csv.part", f)

ftp.rename("orders.csv.part", "orders.csv")

On servers where rename is atomic, the final filename appears only when the complete file is there. Confirm the receiving system ignores your temporary extension.

Encrypting it with FTP_TLS

Plain FTP sends the username, the password, every command, and every byte of every file across the network unencrypted. When the server supports FTPS, ftplib includes FTP_TLS, which wraps the same API in explicit FTPS, the variant that starts on port 21 and upgrades to TLS:

import os
import ssl
from ftplib import FTP_TLS

context = ssl.create_default_context()

with FTP_TLS("ftp.example.com", context=context) as ftp:
    ftp.login(user=os.environ["FTP_USERNAME"], passwd=os.environ["FTP_PASSWORD"])
    ftp.prot_p()
    ftp.cwd("/inbound")

    with open("/data/exports/orders.csv", "rb") as f:
        ftp.storbinary("STOR orders.csv", f)

Two lines make the difference, and both are easy to leave out. context=ssl.create_default_context() verifies the server's certificate against the system trust store. Without it, FTP_TLS encrypts the connection but never checks who is on the other end, so it will happily encrypt your files on their way to an impostor.

When a TLS error appears, fix the hostname, the certificate chain, or the local trust store; do not disable verification to make the error go away.

prot_p() switches the data channel to TLS as well. FTPS encrypts the control and data channels separately, and without prot_p() the login is encrypted and the files are not, which is the default and a common surprise.

FTP_TLS speaks explicit FTPS only. Implicit FTPS, the older variant that is encrypted from the first byte on port 990, needs the socket wrapped in TLS before ftplib sees it, and most people who need it either reach for a library that supports it directly or ask the server for explicit mode.

FTPS is also not SFTP: SFTP is a different protocol built on SSH, ftplib cannot speak it, and the Python SFTP walkthrough covers paramiko for that case.

If you are choosing between the three, the comparison of FTP, FTPS, and SFTP lays out the differences.

Let Files.com make the FTP connection

Either version above puts the protocol inside your Python, and the code to move one file is the small part. Each connection brings credentials to store and rotate, a passive port range to negotiate with the network team, a certificate to validate, server-specific quirks, retries and resumes to write, logs to keep, and a different FTP or FTPS configuration for every partner.

The third option takes FTP out of the code. Files.com connects to the other FTP or FTPS server on the cloud side, and your Python only talks to Files.com.

Mount the partner's server as a folder

A Remote Server Mount connects an empty folder on your Files.com site to the partner's FTP or FTPS server. Mount it at /partners/greenfield and that folder becomes a live window onto the partner's machine: uploading, downloading, listing, renaming, and deleting through the folder passes each operation to the remote server as it happens, and Files.com keeps no copy.

The partner's hostname, port, and credentials live on the mount, stored encrypted, and Files.com always negotiates the strongest TLS the partner's server supports. The partner allowlists the Files.com IP addresses once.

Your Python talks HTTPS

With the mount in place, the script uses the Files.com Python SDK over HTTPS on port 443 with an API key:

pip3 install Files.com
import os
import files_sdk

files_sdk.set_api_key(os.environ["FILES_API_KEY"])

files_sdk.file.upload_file(
    "/data/exports/orders.csv",
    "/partners/greenfield/inbound/orders.csv",
)

files_sdk.file.download_file(
    "/partners/greenfield/outbound/report.csv",
    "/data/imports/report.csv",
)

The upload goes to Files.com over HTTPS, and Files.com writes it to the partner's server over FTPS through the mount as it arrives.

The application uses one API instead of a different FTP configuration per partner, no partner credential lives on the application server, nothing negotiates passive connections or certificate chains, the SDK retries and resumes interrupted transfers, and every operation lands in the Files.com audit log. If your site runs on a custom domain, set files_sdk.base_url once.

You may not need Python at all

If the job only moves files on a schedule, the script itself may be the unnecessary part. Once the partner's server is a folder on Files.com, a Sync pushes to or pulls from it on a schedule, and an Automation moves, renames, copies, or routes files when they arrive.

The documented pattern is a partner that uploads invoices to its own server: Files.com pulls them every ten minutes, and a Move Files automation routes each one to its processing folder.

Python keeps the parts that carry your application's logic and drops the parts that only reproduce scheduling, retries, and logging.

Your ftplib script keeps working too

Files.com also accepts inbound FTPS: explicit FTPS on port 21 (or 3021), implicit FTPS on port 990 (or 3990), a passive range of 40000 to 50000, and plain FTP turned off by default.

Point the FTP_TLS code above at yourcompany.files.com, STOR into /partners/greenfield/inbound, and Files.com relays the file to the partner. The script keeps using FTPS against one consistent endpoint with a real certificate, and the partner-specific credentials and quirks live outside it.

A partner whose FTP server sits inside a private network is reachable through the Files.com Agent, which connects outbound with no inbound firewall rule.

Running it on a schedule

Whichever route you take, a few practices decide whether the job is reliable. Read passwords and API keys from environment variables or a secret manager, never from the .py file, and never log them.

Pass timeout=60 (or a value that fits the server) to FTP and FTP_TLS so a stalled network operation cannot hang forever. Record the start and end time, the paths, the bytes moved, and the exception when one is raised.

Exit nonzero on any failure, because cron, systemd, and every other scheduler watch the exit code and a script that fails and exits zero alerts nobody.

And test as the account the job runs under: environment variables, file permissions, and certificate stores are all per user, and a script that works from your terminal can fail under cron for that reason alone.

Which one to use

Plain FTP only when the server supports nothing safer, the traffic stays on a network you control, and you accept that credentials and files travel in the clear.

FTP_TLS with a verified context and prot_p() whenever the server offers explicit FTPS and the integration count is small enough to manage certificates, credentials, passive ranges, and monitoring by hand.

SFTP whenever the partner offers it, since it has one connection instead of two and none of FTP's firewall trouble.

The Files.com mount when you would rather not run the protocol in Python at all, when the far end is already Files.com, when several partners are involved, or when the remote server has to stay where it is and you want its protocol details kept out of your application.

When scripts stop being the right answer

A handful of FTP jobs is manageable. Dozens, each with its own owner, credential, passive range, retry strategy, and schedule, are the reason teams move transfers onto a platform that runs them. At that point the question is no longer how to call storbinary(). It is how to operate file transfers as a reliable service.

Files.com is the cloud-native File Orchestration Platform: one platform that replaces the stack of legacy tools IT teams run to move files, including the FTP servers, the scripts that feed them, and the schedulers holding it all together. It speaks every protocol, connects 50+ cloud and on-prem systems, automates every transfer, and keeps a complete audit trail.

For the job in this post, that means the nightly upload becomes a Files.com automation with retries and a delivery record, and Python stays for the logic that is genuinely yours, calling the SDK. If the server has to stay in your own data center, the ExaVault appliance is a free FTP, FTPS, and SFTP server for up to 50 users that every example above can target.

Start a free Files.com trial and point the SDK at it. No credit card required, and an FTPS endpoint is live in minutes.

Frequently asked questions

How do I upload a file with Python ftplib?

Connect with FTP(host) or FTP_TLS(host, context=ssl.create_default_context()), call login(user, password), open the local file in binary mode, and pass it to storbinary("STOR filename", f), all inside with blocks so the connection and file close on failure. With Files.com, files_sdk.file.upload_file(local, remote) over HTTPS does the same job and a Remote Server Mount carries the file on to the partner's FTP server.

Is Python ftplib secure?

Plain FTP is not: credentials and file data cross the network unencrypted. FTP_TLS encrypts the connection, but it only verifies the server's certificate when you pass context=ssl.create_default_context(), and it only encrypts the data channel after you call prot_p().

Does ftplib support implicit FTPS on port 990?

Not directly. FTP_TLS speaks explicit FTPS, which starts as FTP on port 21 and upgrades to TLS. Implicit FTPS is encrypted from the first byte and requires wrapping the socket before ftplib uses it, or a library that supports it directly.

Why does my FTP transfer hang after login?

Almost always a blocked passive data port. The control connection on port 21 works, but the data connection for LIST, STOR, or RETR cannot reach the server's passive range through a firewall. Ask the server's administrator for the range and open it.

Can Files.com connect to my partner's FTP server for me?

Yes. A Remote Server Mount connects a folder on your Files.com site to any FTP or FTPS server in real time, holds the credentials encrypted, negotiates the strongest TLS the partner supports, and passes every operation through. Your Python uploads to Files.com over HTTPS with the SDK, or a scheduled Sync moves the files with no code at all.

FTP, SFTP, FTPS — in a Modern UI

Files.com is the cloud File Orchestration Platform. Bring your FTP clients; pick up a real web file manager, share links, automations, and SOC 2 / HIPAA-BAA compliance.