{"id":5938,"library":"ftputil","title":"High-level FTP client library (virtual file system and more)","description":"ftputil is a high-level FTP client library for the Python programming language. It implements a virtual file system for accessing FTP servers, providing many functions similar to those in the `os`, `os.path`, and `shutil` modules. It also offers convenience functions for conditional uploads and downloads, and handles FTP clients and servers in different timezones. The current stable version is 5.1.0, released on 2024-01-06. The project follows semantic versioning, with major version changes indicating backward incompatibility. Releases are announced on its mailing list.","status":"active","version":"5.1.0","language":"en","source_language":"en","source_url":"https://git.sr.ht/~sschwarzer/ftputil","tags":["ftp","file transfer","client","virtual filesystem","ftps"],"install":[{"cmd":"pip install ftputil","lang":"bash","label":"Install latest version"}],"dependencies":[],"imports":[{"note":"The primary class `FTPHost` is typically accessed via the main `ftputil` module namespace.","wrong":"from ftputil import FTPHost","symbol":"FTPHost","correct":"import ftputil\nftp_host = ftputil.FTPHost(...)"}],"quickstart":{"code":"import ftputil\nimport os\n\nFTP_HOST = os.environ.get('FTP_HOST', 'ftp.example.com')\nFTP_USER = os.environ.get('FTP_USER', 'your_username')\nFTP_PASSWORD = os.environ.get('FTP_PASSWORD', 'your_password')\n\n# Example: Download files from the login directory\nwith ftputil.FTPHost(FTP_HOST, FTP_USER, FTP_PASSWORD) as ftp_host:\n    print(f\"Connected to {FTP_HOST}. Current directory: {ftp_host.getcwd()}\")\n    names = ftp_host.listdir(ftp_host.curdir)\n    for name in names:\n        if ftp_host.path.isfile(name):\n            print(f\"Downloading {name}...\")\n            # remote name, local name\n            ftp_host.download(name, name)\n\n# Example: Create a new directory and upload a file\nLOCAL_FILE_CONTENT = b\"This is a test file.\\n\"\nLOCAL_FILE_NAME = \"local_test.txt\"\nREMOTE_DIR_NAME = \"new_remote_dir\"\nREMOTE_FILE_NAME = f\"{REMOTE_DIR_NAME}/remote_test.txt\"\n\nwith open(LOCAL_FILE_NAME, 'wb') as f:\n    f.write(LOCAL_FILE_CONTENT)\n\nwith ftputil.FTPHost(FTP_HOST, FTP_USER, FTP_PASSWORD) as ftp_host:\n    print(f\"Creating remote directory {REMOTE_DIR_NAME}...\")\n    ftp_host.makedirs(REMOTE_DIR_NAME, exist_ok=True)\n    print(f\"Uploading {LOCAL_FILE_NAME} to {REMOTE_FILE_NAME}...\")\n    ftp_host.upload(LOCAL_FILE_NAME, REMOTE_FILE_NAME)\n    print(\"Upload complete.\")\n\n# Clean up local file\nos.remove(LOCAL_FILE_NAME)","lang":"python","description":"This quickstart demonstrates how to connect to an FTP server, list files, download a file, create a remote directory, and upload a file using `ftputil.FTPHost`. It uses environment variables for credentials for security and easy testing."},"warnings":[{"fix":"If hidden files are required, custom session factories or other workarounds may be needed, or consider explicit filtering if the server supports different `DIR` options.","message":"ftputil 4.0.0 changed the default behavior of the `DIR` command by no longer sending the `-a` option. This means 'hidden' files/directories (starting with a dot) are no longer included in listings by default, which may alter `listdir` or `walk` results.","severity":"breaking","affected_versions":">=4.0.0"},{"fix":"Review and adjust time shift calculations or call `ftp_host.synchronize_times()` to ensure correct time synchronization between client and server.","message":"The definition of 'time shift' used for `upload_if_newer` and `download_if_newer` changed in ftputil 4.0.0. Previously it was `server_time - local_client_time`; now it's defined as `server_listing_time - UTC`. This is backward-incompatible and may require adjusting existing code, especially if `synchronize_times` or explicit `set_time_shift` calls are used.","severity":"breaking","affected_versions":">=4.0.0"},{"fix":"Upgrade to the latest 5.x.x version and test thoroughly. If using custom `session_factory` implementations, review against Python 3.9+ `ftplib` changes.","message":"ftputil 5.0.0 is not backward-compatible with 4.0.0 and earlier due to compatibility changes with `ftplib` in Python 3.9. This primarily affects internal workings and session factory implementations.","severity":"breaking","affected_versions":"5.0.0"},{"fix":"Always construct full paths using `ftp_host.path.join(parent_path, name)` before passing them to `isdir`, `isfile`, `islink`, or similar methods if the `name` isn't from `ftp_host.curdir`.","message":"Calling `isdir`, `isfile`, or `islink` on path names returned by `listdir` can incorrectly return `False` if the `listdir` call was not for the current directory. This happens because these methods expect a full path or a path relative to the current working directory of the `FTPHost` instance.","severity":"gotcha","affected_versions":"All"},{"fix":"Avoid calling `lstat` directly on the root path '/'. For checks on the root, use `listdir('/')` to see its contents instead.","message":"The `FTPHost.lstat('/')` method (for the root directory) will raise a `RootDirError`. This is a known limitation of the underlying algorithm. You can `stat` items *in* the root directory, but not the root directory itself.","severity":"gotcha","affected_versions":"All"},{"fix":"Ensure correct time zone synchronization using `ftp_host.synchronize_times()`. Be aware that if timestamps are only minute-precise, ftputil errs on the side of transferring too much data. For interrupted transfers, consider using `upload`/`download` directly or explicitly removing the incomplete remote file before retrying `upload_if_newer`.","message":"The methods `upload_if_newer` and `download_if_newer` may behave unexpectedly (e.g., unnecessary transfers or failing to transfer) due to subtle differences in server timestamp precision (often only to the minute) or incorrect time zone synchronization between the client and server.","severity":"gotcha","affected_versions":"All"}],"env_vars":null,"last_verified":"2026-04-14T00:00:00.000Z","next_check":"2026-07-13T00:00:00.000Z"}