未翻訳のページ
このページはまだ日本語に翻訳されていないため、英語の原文を表示しています。 翻訳に協力する
ftplib Module Complexity¶
The ftplib module is a synchronous FTP client. Commands travel as single lines over a control
connection, and every file transfer or directory listing opens a new data connection of its own.
Transfers stream: retrbinary() and storbinary() move one block at a time and hand each block to
your callback or socket, so memory follows the block size rather than the file. Only nlst() and
mlsd() hold a whole listing.
b is the bytes a transfer or listing moves over the data connection, k is blocksize, l is
the characters in the longest line of a line-mode transfer, c is the characters in the command
lines a call sends, and r is the characters in the server's replies to them; a transfer or
listing adds that O(c + r) to its own row. Every reply line and every line of a line-mode transfer
is read with a cap of 8,192 characters, line ending included (bytes, for storlines()), so l
never exceeds that. The
bounds price the client's own work and memory with debugging off, and exclude the work of any
callback you pass. Round trips, DNS lookups, connection setup, TLS handshakes and the server's own
work are outside every bound.
Complexity Reference¶
FTP connections¶
| Operation | Time | Space | Notes |
|---|---|---|---|
ftplib.FTP(host='', user='', passwd='', acct='', ...) |
O(c + r) | O(c + r) | Connects when host is given and logs in when user is too; O(1) with neither |
FTP.connect(host='', port=0, ...) |
O(r) | O(r) | Returns the server's welcome |
FTP.getwelcome() |
O(1) | O(1) | The welcome connect() stored |
FTP.login(user='anonymous', passwd='', acct='') |
O(c + r) | O(c + r) | USER, then PASS and ACCT only when the server asks for them |
Leaving a with FTP(...) as ftp: block |
O(r) | O(r) | Sends QUIT and closes, ignoring a connection already gone |
FTP.set_pasv(val) |
O(1) | O(1) | Passive by default; False makes the server connect back to the client for each transfer |
FTP.set_debuglevel(level) |
O(1) | O(1) | 1 prints each command and reply to standard output, 2 every line sent and read; passwords are masked |
FTP.abort() |
O(r) | O(r) | Sends ABOR for a transfer in progress |
FTP.quit() |
O(r) | O(r) | Sends QUIT, then closes; an error reply raises before the close, so follow it with close() |
FTP.close() |
O(1) | O(1) | Closes the sockets without sending anything; safe to call twice |
FTP commands¶
| Operation | Time | Space | Notes |
|---|---|---|---|
FTP.sendcmd(cmd) |
O(c + r) | O(c + r) | Returns the reply; a 4xx raises error_temp and a 5xx error_perm |
FTP.voidcmd(cmd) |
O(c + r) | O(c + r) | As sendcmd(), and raises error_reply unless the reply is 2xx |
FTP.cwd(pathname) |
O(c + r) | O(c + r) | '..' sends CDUP |
FTP.pwd(), FTP.mkd(pathname) |
O(c + r) | O(c + r) | Return the path the server quotes in its 257 reply |
FTP.rmd(dirname), FTP.delete(filename) |
O(c + r) | O(c + r) | |
FTP.rename(fromname, toname) |
O(c + r) | O(c + r) | Two commands, RNFR then RNTO |
FTP.size(filename) |
O(c + r) | O(c + r) | An int from a 213 reply |
FTP transfers¶
| Operation | Time | Space | Notes |
|---|---|---|---|
FTP.retrbinary(cmd, callback, blocksize=8192, rest=None) |
O(b) | O(k) | Calls callback with each block of at most k bytes as it arrives; rest starts the server at that offset |
FTP.storbinary(cmd, fp, blocksize=8192, callback=None, rest=None) |
O(b) | O(k) | Reads fp a block at a time and sends each block before reading the next |
FTP.retrlines(cmd, callback=None) |
O(b) | O(l) | Calls callback with each line, line ending stripped; the default prints it |
FTP.storlines(cmd, fp, callback=None) |
O(b) | O(l) | Reads a binary fp a line at a time and sends each with a CRLF ending |
FTP.transfercmd(cmd, rest=None) |
O(c + r) | O(c + r) | Returns the data socket; the caller reads or writes it, closes it, then calls voidresp() |
FTP.ntransfercmd(cmd, rest=None) |
O(c + r) | O(c + r) | As transfercmd(), returning (socket, size); size comes from the 150 reply, or is None |
FTP listings¶
| Operation | Time | Space | Notes |
|---|---|---|---|
FTP.nlst(argument[, ...]) |
O(b) | O(b) | Returns every name as one list |
FTP.dir(argument[, ...][, callback]) |
O(b) | O(l) | Streams the LIST output a line at a time to callback, which prints by default |
FTP.mlsd(path='', facts=[]) |
O(b) | O(b) | A generator: nothing is sent until the first next(), which reads the whole listing before yielding the first entry |
FTP_TLS¶
| Operation | Time | Space | Notes |
|---|---|---|---|
ftplib.FTP_TLS(host='', user='', passwd='', acct='', *, context=None, ...) |
O(c + r) | O(c + r) | As FTP; login() secures the control connection first unless called with secure=False |
FTP_TLS.auth() |
O(r) | O(r) | Sends AUTH TLS and wraps the control connection; raises ValueError if it is already secured |
FTP_TLS.prot_p() |
O(r) | O(r) | Sends PBSZ 0 and PROT P; from then on every data connection is wrapped in TLS, one handshake per transfer |
FTP_TLS.prot_c() |
O(r) | O(r) | Data connections go back to clear text, which is the default |
FTP_TLS.ccc() |
O(r) | O(r) | Returns the control connection to clear text |
FTP_TLS.ssl_version |
O(1) | O(1) | Python 3.11 only; removed in 3.12 |
Exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
ftplib.error_reply |
O(1) | O(1) | A reply of a kind the call did not expect |
ftplib.error_temp, ftplib.error_perm |
O(1) | O(1) | A 4xx or a 5xx reply |
ftplib.error_proto |
O(1) | O(1) | A reply that does not begin with a digit from 1 to 5 |
ftplib.all_errors |
O(1) | O(1) | A tuple of ftplib.Error, the base class of the four above, OSError and EOFError, plus ssl.SSLError where ssl is available |
Transferring Files¶
Streaming Downloads and Uploads¶
retrbinary() hands each block to its callback as it arrives and keeps nothing, so a callback that
writes to disk or updates a hash holds one block. It is the callback that decides whether the
download is held whole. storbinary() is the mirror image: it reads fp a block at a time.
import ftplib
import hashlib
import io
with ftplib.FTP('ftp.example.com', 'anonymous') as ftp: # connects and logs in
with open('archive.bin', 'wb') as f:
ftp.retrbinary('RETR /pub/archive.bin', f.write) # O(b) time, O(k) memory
digest = hashlib.sha256()
ftp.retrbinary('RETR /pub/archive.bin', digest.update) # O(k) memory: hashed block by block
# Collecting the blocks instead holds the whole file
buffer = io.BytesIO()
ftp.retrbinary('RETR /pub/archive.bin', buffer.write) # O(b) time and memory
assert hashlib.sha256(buffer.getvalue()).digest() == digest.digest()
# Uploading reads the file a block at a time
with open('archive.bin', 'rb') as f:
ftp.storbinary('STOR /upload/archive.bin', f) # O(b) time, O(k) memory
assert ftp.size('/upload/archive.bin') == len(buffer.getvalue()) # O(c + r)
Resuming a Download¶
rest asks the server to start at an offset, so a resumed download moves only the bytes still
missing. transfercmd() returns the data socket itself and leaves reading it, and collecting the
server's final reply, to you.
import ftplib
import os
with ftplib.FTP('ftp.example.com', 'anonymous') as ftp:
ftp.voidcmd('TYPE I') # O(c + r) - binary mode, as retrbinary() sets it
# Read the start of the file from the data connection, then stop
with ftp.transfercmd('RETR /pub/archive.bin') as conn, open('archive.bin', 'wb') as f:
f.write(conn.recv(65536)) # at most 64 KiB
try:
ftp.voidresp() # the reply to the RETR cut short
except ftplib.error_temp:
pass # 426: the server saw the data connection close
# Resume where the file on disk ends
with open('archive.bin', 'ab') as f:
ftp.retrbinary('RETR /pub/archive.bin', f.write, rest=f.tell()) # O(b) - the rest only
assert os.path.getsize('archive.bin') == ftp.size('/pub/archive.bin')
Line Mode and the Line Limit¶
retrlines() and storlines() work a line at a time, and read each line, its ending included,
with a cap: 8,192 characters for retrlines() and 8,192 bytes for storlines(), so a line of
multibyte text reaches the upload cap sooner. A longer line raises ftplib.Error, the base class
of the four error_* exceptions, instead of growing without bound. storlines() needs a binary
file.
import ftplib
import io
with ftplib.FTP('ftp.example.com', 'anonymous') as ftp:
lines = []
ftp.retrlines('RETR /pub/notes.txt', lines.append) # O(b) time, O(l) memory
assert lines == ['one', 'two', 'three']
ftp.storlines('STOR /upload/more.txt', io.BytesIO(b'four\nfive\n')) # O(b), O(l)
uploaded = []
ftp.retrlines('RETR /upload/more.txt', uploaded.append)
assert uploaded == ['four', 'five']
try:
ftp.storlines('STOR /upload/long.txt', io.BytesIO(b'x' * 10_000 + b'\n'))
except ftplib.Error as error:
assert 'got more than 8192 bytes' in str(error)
ftp.voidresp() # the reply to the abandoned STOR
else:
raise AssertionError('a 10,000-character line was sent')
Listing Directories¶
The three listing calls cost the same O(b) to run and differ in what they hold. dir() passes each
line on and keeps none. nlst() returns every name in one list. mlsd() is a generator, but that
buys nothing in memory: its first next() reads the whole listing before yielding the first entry.
import ftplib
with ftplib.FTP('ftp.example.com', 'anonymous') as ftp:
names = ftp.nlst('/pub') # O(b) time and memory - the whole listing
assert names == ['archive.bin', 'notes.txt', 'readme.txt']
long_form = []
ftp.dir('/pub', long_form.append) # O(b) time, O(l) memory beyond what the callback keeps
assert len(long_form) == 3
entries = ftp.mlsd('/pub', facts=['type', 'size']) # O(1) - nothing is sent yet
name, facts = next(entries) # O(b) - the whole listing is read here
assert name == 'archive.bin' and facts['size'] == '1048576'
assert {name: facts['type'] for name, facts in entries} == {
'notes.txt': 'file',
'readme.txt': 'file',
}
Commands and Replies¶
Each command is one line out and one reply back, so its client-side cost is the length of the two.
A 4xx or 5xx reply raises rather than returning, and voidcmd() also raises on anything other
than 2xx.
import ftplib
with ftplib.FTP('ftp.example.com', 'anonymous') as ftp:
assert ftp.getwelcome().startswith('220') # O(1) - read by connect()
assert ftp.mkd('/upload/reports') == '/upload/reports' # O(c + r)
ftp.cwd('/upload/reports') # O(c + r)
assert ftp.pwd() == '/upload/reports'
ftp.cwd('..') # sends CDUP
assert ftp.pwd() == '/upload'
assert ftp.rmd('reports').startswith('250')
assert ftp.sendcmd('NOOP').startswith('200') # O(c + r) - returns the reply
try:
ftp.cwd('/missing')
except ftplib.error_perm as error: # a 5xx reply
assert str(error).startswith('550')
else:
raise AssertionError('a missing directory was entered')
Secure Connections¶
FTP_TLS secures the control connection when you log in, but data connections stay in clear text
until prot_p(). After it, every transfer and listing pays a TLS handshake for its own data
connection.
import ftplib
import ssl
context = ssl.create_default_context()
with ftplib.FTP_TLS('ftp.example.com', context=context) as ftps:
ftps.login('anonymous') # AUTH TLS first, then USER and PASS
ftps.prot_p() # O(r) - data connections are encrypted from here on
assert 'readme.txt' in ftps.nlst('/pub') # O(b), plus a handshake for this listing
Common Patterns¶
Mirroring a Directory¶
Every file is a transfer of its own, so a directory of n files costs n + 1 data connections: one for the listing and one per file. Keep the control connection open across them rather than logging in again for each.
import ftplib
import pathlib
target = pathlib.Path('mirror')
target.mkdir()
with ftplib.FTP('ftp.example.com', 'anonymous') as ftp:
ftp.cwd('/pub')
for name in ftp.nlst(): # one data connection for the listing
with open(target / name, 'wb') as f:
ftp.retrbinary(f'RETR {name}', f.write) # and one per file
assert sorted(path.name for path in target.iterdir()) == ['archive.bin', 'notes.txt', 'readme.txt']
Performance Best Practices¶
✅ Do:
- Give
retrbinary()a callback that writes or hashes each block, so memory stays at one block - Pass a file object to
storbinary()rather than reading it into memory first - Resume an interrupted download with
rest, which moves only the missing bytes - Use
dir()with a callback to scan a large directory; it holds one line at a time - Reuse one logged-in connection for many transfers
❌ Avoid:
- Collecting a large download in a
BytesIOor a list, which costs O(b) memory - Treating
mlsd()as lazy: it holds the whole listing once iteration starts - Relying on
FTP_TLSalone to protect file contents; callprot_p()after logging in
Version Notes¶
- Python 3.12+:
FTP_TLSno longer takeskeyfileorcertfile,contextis keyword-only, andFTP_TLS.ssl_versionis gone; pass anssl.SSLContextascontext