Today I found myself in need of programmatically getting a recursive list of the files on a remote FTP server to look for some data archives. Much to my surprise, I could not find this functionality implemented in some kind of Python library. So I decided to write a simple recursive version of ftplib.mlsd, that can be found below:


def recursive_mlsd(ftp_object, path="", maxdepth=None):
    """Run the FTP's MLSD command recursively

    The MLSD is returned as a list of tuples with (name, properties) for each
    object found on the FTP server. This function adds the non-standard
    property "children" which is then again an MLSD listing, possibly with more

    ftp_object: ftplib.FTP or ftplib.FTP_TLS
        the (authenticated) FTP client object used to make the calls to the
    path: str
        path to start the recursive listing from
    maxdepth: {None, int}, optional
        maximum recursion depth, has to be >= 0 or None (i.e. no limit).

        the recursive directory listing

    See also
    ftplib.FTP.mlsd : the non-recursive version of this function
    if maxdepth is not None:
        maxdepth = int(maxdepth)
        if maxdepth < 0:
            raise ValueError("maxdepth is supposed to be >= 0")

    def _inner(path_, depth_):
        if maxdepth is not None and depth_ > maxdepth:

        inner_mlsd = list(ftp_object.mlsd(path=path_))
        for name, properties in inner_mlsd:
            if properties["type"] == "dir":
                rec_path = path_+"/"+name if path_ else name
                res = _inner(rec_path, depth_+1)
                if res is not None:
                    properties["children"] = res

        return inner_mlsd

    return _inner(path, 0)

A very basic example usage could be like:

import ftplib
import getpass
import pprint
import sys

import ftphelper

host = "ftp1.at.proftpd.org"   # from the Python docs
directory = "historic"
print(f"Login to '{host}' to list '{directory}'", file=sys.stderr)

default_user = ""   # getpass.getuser() could also be used where appropriate
prompt = f"Username (default: '{default_user}'): "
if sys.stdout.isatty():
    user = input(prompt)
    print(prompt, end="", file=sys.stderr)
    user = input()
user = user.strip()
user = user if user else default_user

with ftplib.FTP(host=host) as ftp:
    ftp.login(user=user, passwd=getpass.getpass())
    listing = ftphelper.recursive_mlsd(ftp, directory)

pp = pprint.PrettyPrinter(indent=2)

As always, every kind of feedback is welcome and much appreciated.


1 Answer 1


Seems good except for a few small improvements:

  • path_+"/"+name is better perceived with f-string formatting: f'{path_}/{name}
  • user if user else default_user is a verbose version of user or default_user
  • 1
    \$\begingroup\$ user or default_user is a cool "trick". Will definitely use it in the future! \$\endgroup\$
    – AlexV
    Nov 19, 2019 at 20:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.