{"record":{"id":"59a3c613f893eed9","repo":"apache/beam","slug":"unable-to-rename-a-directory","errorCode":null,"errorMessage":"Unable to rename a directory.","messagePattern":"Unable to rename a directory\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"sdks/python/apache_beam/io/azure/blobstorageio.py","lineNumber":301,"sourceCode":"  # underlying copy and delete operations are already idempotent operations\n  # protected by retry decorators.\n  def rename_files(self, src_dest_pairs):\n    \"\"\"Renames the given Azure Blob Storage blobs from src to dest.\n\n    Args:\n      src_dest_pairs: List of (src, dest) tuples of\n                      azfs://<storage-account>/<container>/[name]\n                      file 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:\n      return []\n\n    for src, dest in src_dest_pairs:\n      if src.endswith('/') or dest.endswith('/'):\n        raise ValueError('Unable to rename a directory.')\n\n    # Results from copy operation.\n    copy_results = self.copy_paths(src_dest_pairs)\n    paths_to_delete = \\\n        [src for (src, _, error) in copy_results if error is None]\n    # Results from delete operation.\n    delete_results = self.delete_files(paths_to_delete)\n\n    # Get rename file results (list of tuples).\n    results = []\n\n    # Using a dictionary will make the operation faster.\n    delete_results_dict = {src: error for (src, error) in delete_results}\n\n    for src, dest, error in copy_results:\n      # If there was an error in the copy operation.\n      if error is not None:\n        results.append((src, dest, error))","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/python/apache_beam/io/azure/blobstorageio.py#L283-L319","documentation":"BlobStorageIO.rename_files() works by copying individual blobs and deleting the originals; it cannot move a 'directory' prefix as a whole. If either the source or destination path ends with '/', the method refuses with ValueError because directory-level rename must go through rename (copy_tree) instead.","triggerScenarios":"Calling rename_files([['container/dir/', 'container/newdir/']]) or passing a dest with a trailing slash; batch jobs that normalize prefixes with trailing slashes.","commonSituations":"Confusing rename_files (file-level) with rename/copy_tree (tree-level); path-joining utilities that leave trailing slashes on prefixes.","solutions":["Use client.rename(src_dir, dest_dir) (or copy_tree) for directory renames","Strip trailing slashes / rename individual files when using rename_files","Filter src_dest_pairs to blob file paths before calling"],"exampleFix":"// before\nclient.rename_files([['container/logs/', 'container/logs-old/']])\n// after\nclient.rename('container/logs', 'container/logs-old')  # tree-level rename","handlingStrategy":"validation","validationCode":"def renameable_pairs(pairs):\n    return [(s.rstrip('/'), d.rstrip('/')) for s, d in pairs]\npairs = renameable_pairs(src_dest_pairs)  # no trailing slashes remain","typeGuard":"def is_file_path(p):\n    return not p.endswith('/')","tryCatchPattern":"try:\n    client.rename_files(pairs)\nexcept ValueError:\n    client.rename(src_dir, dest_dir)  # tree-level fallback","preventionTips":["Normalize paths with rstrip('/') before rename APIs","Use rename()/copy_tree() for prefixes, rename_files() only for blobs","Write a wrapper API that dispatches file vs tree rename"],"tags":["azure","rename","invalid-argument"],"backgroundTag":"invalid-argument-value","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"}