{"record":{"id":"8d0f7663cd64ec4b","repo":"apache/beam","slug":"cannot-rename-a-directory","errorCode":null,"errorMessage":"Cannot rename a directory","messagePattern":"Cannot rename a directory","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/io/aws/s3io.py","lineNumber":528,"sourceCode":"        time.mktime(updated.timetuple()) - time.timezone +\n        updated.microsecond / 1000000.0)\n\n  def rename_files(self, src_dest_pairs):\n    \"\"\"Renames the given S3 objects from src to dest.\n\n    Args:\n      src_dest_pairs: list of (src, dest) tuples of s3://<bucket>/<name> file\n                      paths to rename from src to dest\n    Returns: List of tuples of (src, dest, exception) in the same order as the\n            src_dest_pairs argument, where exception is None if the operation\n            succeeded or the relevant exception if the operation failed.\n    \"\"\"\n    if not src_dest_pairs: return []\n\n    # TODO: Throw value error if path has directory\n    for src, dest in src_dest_pairs:\n      if src.endswith('/') or dest.endswith('/'):\n        raise ValueError('Cannot rename a directory')\n\n    copy_results = self.copy_paths(src_dest_pairs)\n    paths_to_delete = [src for (src, _, err) in copy_results if err is None]\n    delete_results = self.delete_files(paths_to_delete)\n\n    delete_results_dict = {src: err for (src, err) in delete_results}\n    rename_results = []\n    for src, dest, err in copy_results:\n      if err is not None: rename_results.append((src, dest, err))\n      elif delete_results_dict[src] is not None:\n        rename_results.append((src, dest, delete_results_dict[src]))\n      else:\n        rename_results.append((src, dest, None))\n\n    return rename_results\n\n\nclass S3Downloader(Downloader):","sourceCodeStart":510,"sourceCodeEnd":546,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/io/aws/s3io.py#L510-L546","documentation":"S3IO.rename_files refuses to rename any source or destination path that ends with '/', i.e. a directory/prefix. S3 has no atomic directory rename; only object keys can be copied-and-deleted, so prefix renames are rejected up front.","triggerScenarios":"Calling rename_files([('s3://bucket/dir/', 's3://bucket/dir2/')]) or any pair where either key keeps a trailing slash.","commonSituations":"Users thinking of S3 prefixes as folders and trying to 'move a folder'; code generated from POSIX semantics (mv dir1 dir2); renaming results of list_prefix that retain trailing slashes.","solutions":["Rename individual object keys without trailing slashes.","To move a prefix, list all keys under it and copy/delete each key individually.","Strip trailing '/' from paths before building src_dest_pairs.","Consider using S3 CopyObject in bulk via the AWS CLI / SDK for prefix moves outside Beam."],"exampleFix":"// before\nio.rename_files([('s3://bucket/data/', 's3://bucket/data2/')])  # ValueError\n// after\nkeys = io.list_files('s3://bucket/data/')\nio.rename_files([(k, k.replace('data/', 'data2/', 1)) for k in keys])","handlingStrategy":"validation","validationCode":"if any(s.endswith('/') or d.endswith('/') for s, d in pairs):\n    raise ValueError('S3 rename does not support directories/prefixes')","typeGuard":"def is_object_key(path):\n    return not path.endswith('/')","tryCatchPattern":"try:\n    io.rename_files(pairs)\nexcept ValueError:\n    # expand prefix into per-object pairs\n    pairs = expand_prefix_pairs(pairs)\n    io.rename_files(pairs)","preventionTips":["Treat S3 prefixes as folders in name only; renames are per-key.","Strip trailing slashes when building src/dest lists.","For bulk prefix moves, list then copy/delete each key."],"tags":["python","aws","s3","unsupported-operation"],"backgroundTag":"unsupported-operation","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-20T03:17:13.778Z"}