> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encord.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Label Versions & Branches

Label branches let you keep multiple versions of a Project's labels side-by-side. Use them to preserve a labeled state before a re-labeling pass, run an experiment without disturbing `main`, or hold a reviewer-only copy of labels.

You create a new label version by copying labels from an existing branch onto a new branch with `project.copy_labels_to_branch()`. Every Project starts with a `main` branch. Copying from `main` (or any branch) into a new branch name creates that branch and populates it with the matching label rows.

<Tip>
  Label branches are the recommended method for **Pre-labeling Agents** and **Exporting Labels Agents**.

  * A **Pre-labeling Agent** writes its model predictions onto a dedicated branch (for example, `pre-labels-v1`) instead of `main`. Annotators continue working on `main` undisturbed, and you can review or merge the agent's output on its own branch with `copy_labels_to_branch(..., overwrite=True)` once it's been validated.

  * An **Exporting Labels Agent** copies the current state of `main` onto a frozen snapshot branch (for example, `export-2026-05-12`) before export. The export reads from that branch via `list_label_rows_v2(branch_name=...)`, so the result is reproducible even if annotation continues on `main` in parallel.
</Tip>

<CodeGroup>
  ```python Template theme={"dark"}
  project.copy_labels_to_branch(
      target_branch="<new-branch-name>",
      source_branch="main",
      overwrite=False,
      data_hashes=None,
      label_hashes=None,
      batch_size=50,
  )
  ```

  ```python Example theme={"dark"}
  project.copy_labels_to_branch(
      target_branch="pre-label-v1",
      source_branch="main",
  )
  ```
</CodeGroup>

The method returns the total number of label rows created or updated on the target branch.

<Warning>
  `source_branch` and `target_branch` MUST be different. Passing the same name for both raises a `ValueError`.
</Warning>

<Warning>
  By default, data units that already have a label row on `target_branch` are **skipped**. Set `overwrite=True` to replace existing label rows on the target branch with the source content.
</Warning>

<Note>
  All examples on this page use [`bundle`](/sdk-documentation/sdk-references/http.bundle#bundle-objects) when calling `initialise_labels()` or `save()` to reduce network round-trips. Keep bundle sizes **under 1000** operations — larger bundles can degrade performance. A good starting point is `BUNDLE_SIZE = 100`; for videos with many labels, start at `20` and adjust. `copy_labels_to_branch()` does not need a `bundle` — it batches internally via its own `batch_size` parameter (default `50`).
</Note>

<Warning>
  The examples below are intended as **quick tests** and cap themselves at the first `MAX_LABEL_ROWS = 10` label rows so you can validate the workflow safely. Remove that cap **deliberately** before running against a full Project — copying or overwriting labels on tens of thousands of rows without filtering can take a long time and is hard to undo.
</Warning>

## Copy All Labels to a New Branch

The following example creates a new label version named `pre-label-v1` by copying every label row on `main` into the new branch. This is the most common pattern: snapshot the current label state before making changes.

```python Copy all labels to a new branch theme={"dark"}
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
TARGET_BRANCH = "pre-label-v1"  # Replace with the name of the new branch
BATCH_SIZE = 50  # Number of label rows copied per API request (server-side batching)
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove or raise this for a full Project copy.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    # For US platform users use domain="https://api.us.encord.com"
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Pick the first MAX_LABEL_ROWS data units from the source branch.
# Metadata-only — no initialization needed, so no bundle required here.
source_rows = project.list_label_rows_v2(branch_name="main")
data_hashes = [row.data_hash for row in source_rows[:MAX_LABEL_ROWS]]

# copy_labels_to_branch batches internally; no bundle required here.
rows_copied = project.copy_labels_to_branch(
    target_branch=TARGET_BRANCH,
    source_branch="main",
    data_hashes=data_hashes,
    batch_size=BATCH_SIZE,
)

print(f"Copied {rows_copied} label rows from 'main' to '{TARGET_BRANCH}' (test run).")
```

## Copy Labels for Specific Data Units

To version labels for only a subset of your Project, pass the `data_hashes` (or `label_hashes`) you want to include. Everything outside that list stays untouched on the target branch.

When you need to discover the `label_hashes` first, fetch them with `list_label_rows_v2()` from the source branch. You only need to initialise label rows (with a `bundle`) when you intend to read their contents — for the hash-discovery step alone, the metadata returned by `list_label_rows_v2()` is enough.

<CodeGroup>
  ```python Filter by data hashes theme={"dark"}
  # Import dependencies
  from encord import EncordUserClient

  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
  TARGET_BRANCH = "subset-v1"  # Replace with the name of the new branch
  BATCH_SIZE = 50

  DATA_HASHES = [
      "11111111-1111-1111-1111-111111111111",
      "22222222-2222-2222-2222-222222222222",
  ]  # Replace with the data hashes you want to copy

  user_client = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  project = user_client.get_project(PROJECT_ID)

  rows_copied = project.copy_labels_to_branch(
      target_branch=TARGET_BRANCH,
      source_branch="main",
      data_hashes=DATA_HASHES,
      batch_size=BATCH_SIZE,
  )

  print(f"Copied {rows_copied} label rows for {len(DATA_HASHES)} data units to '{TARGET_BRANCH}'.")
  ```

  ```python Filter by label hashes theme={"dark"}
  # Import dependencies
  from encord import EncordUserClient

  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
  TARGET_BRANCH = "subset-v1"  # Replace with the name of the new branch
  BATCH_SIZE = 50
  MAX_LABEL_ROWS = 10  # Quick-test cap. Remove or raise this for a full Project copy.

  user_client = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  project = user_client.get_project(PROJECT_ID)

  # Discover label hashes from the source branch. Metadata-only — no initialization
  # needed, so no bundle required here.
  source_rows = project.list_label_rows_v2(
      branch_name="main",
      data_title_like="cherries-%",  # Replace with your filter, or omit to list everything
  )
  label_hashes = [row.label_hash for row in source_rows if row.label_hash][:MAX_LABEL_ROWS]

  rows_copied = project.copy_labels_to_branch(
      target_branch=TARGET_BRANCH,
      source_branch="main",
      label_hashes=label_hashes,
      batch_size=BATCH_SIZE,
  )

  print(f"Copied {rows_copied} label rows to '{TARGET_BRANCH}' (test run).")
  ```
</CodeGroup>

<Warning>
  When both `data_hashes` and `label_hashes` are specified, `data_hashes` is applied first and `label_hashes` is applied to the result.
</Warning>

## Switch to a Branch and Read Labels

Once a branch exists, use `list_label_rows_v2()` with `branch_name` to fetch the label rows on that branch. Initialise each label row inside a `create_bundle()` context so all the download calls are batched into a single round-trip.

```python Read labels from a branch theme={"dark"}
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
BRANCH_NAME = "pre-label-v1"  # Replace with the branch you want to read from
BUNDLE_SIZE = 100  # Keep bundle sizes under 1000 for best performance
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to read every label row on the branch.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Fetch label rows for a specific branch, then cap at MAX_LABEL_ROWS for the test run.
label_rows = project.list_label_rows_v2(branch_name=BRANCH_NAME)[:MAX_LABEL_ROWS]

# Initialise labels using bundles to reduce network calls
with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
    for label_row in label_rows:
        label_row.initialise_labels(bundle=bundle)

for label_row in label_rows:
    print(f"{label_row.data_title}: {len(label_row.get_object_instances())} object instances on '{BRANCH_NAME}'")
```

<Warning>
  `branch_name` cannot be combined with `include_all_label_branches=True`. Pick one: either query a single branch by name, or pull every branch as separate label row objects.
</Warning>

To compare versions, list rows on each branch and inspect their contents. Bundle the initialization calls for each branch separately so each batch maps cleanly to one branch.

```python Compare two branches theme={"dark"}
# Import dependencies
from encord import EncordUserClient

SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
SOURCE_BRANCH = "main"
TARGET_BRANCH = "pre-label-v1"
BUNDLE_SIZE = 100
MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to compare every data unit.

user_client = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

project = user_client.get_project(PROJECT_ID)

# Cap each branch at MAX_LABEL_ROWS for the test run.
source_rows = {
    r.data_hash: r
    for r in project.list_label_rows_v2(branch_name=SOURCE_BRANCH)[:MAX_LABEL_ROWS]
}
# Only compare data units that exist on both branches.
target_rows = {
    r.data_hash: r
    for r in project.list_label_rows_v2(
        branch_name=TARGET_BRANCH,
        data_hashes=list(source_rows.keys()),
    )
}

# Initialise the source branch in a single bundled request
with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
    for row in source_rows.values():
        row.initialise_labels(bundle=bundle)

# Initialise the target branch in a single bundled request
with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
    for row in target_rows.values():
        row.initialise_labels(bundle=bundle)

for data_hash, source_row in source_rows.items():
    target_row = target_rows.get(data_hash)
    if target_row is None:
        print(f"{source_row.data_title}: missing on '{TARGET_BRANCH}'")
        continue

    src_count = len(source_row.get_object_instances())
    tgt_count = len(target_row.get_object_instances())
    print(f"{source_row.data_title}: {SOURCE_BRANCH}={src_count}, {TARGET_BRANCH}={tgt_count}")
```

## Sync or Merge Branches

There is no dedicated "merge" call. To push updates from one branch back into another, call `copy_labels_to_branch()` again with `overwrite=True`. This replaces label rows on the target branch with the latest content from the source branch.

<Warning>
  `overwrite=True` is destructive. Label rows on the target branch are replaced with the source content for every matched data unit. There is no automatic conflict resolution — the source wins. Filter with `data_hashes` or `label_hashes` if you want to limit the sync.
</Warning>

<CodeGroup>
  ```python Sync changes from a branch back to main theme={"dark"}
  # Import dependencies
  from encord import EncordUserClient

  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
  SOURCE_BRANCH = "pre-label-v1"  # Branch with the updated labels
  TARGET_BRANCH = "main"  # Branch to update
  BATCH_SIZE = 50
  MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to sync every data unit on the source branch.

  user_client = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  project = user_client.get_project(PROJECT_ID)

  # Pick the first MAX_LABEL_ROWS data units from the source branch to sync back.
  source_rows = project.list_label_rows_v2(branch_name=SOURCE_BRANCH)
  data_hashes = [row.data_hash for row in source_rows[:MAX_LABEL_ROWS]]

  rows_synced = project.copy_labels_to_branch(
      target_branch=TARGET_BRANCH,
      source_branch=SOURCE_BRANCH,
      overwrite=True,
      data_hashes=data_hashes,
      batch_size=BATCH_SIZE,
  )

  print(f"Synced {rows_synced} label rows from '{SOURCE_BRANCH}' to '{TARGET_BRANCH}' (test run).")
  ```

  ```python Sync only specific data units theme={"dark"}
  # Import dependencies
  from encord import EncordUserClient

  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
  SOURCE_BRANCH = "pre-label-v1"
  TARGET_BRANCH = "main"
  BATCH_SIZE = 50

  DATA_HASHES_TO_MERGE = [
      "11111111-1111-1111-1111-111111111111",
      "22222222-2222-2222-2222-222222222222",
  ]  # Replace with the data units to merge back

  user_client = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  project = user_client.get_project(PROJECT_ID)

  rows_synced = project.copy_labels_to_branch(
      target_branch=TARGET_BRANCH,
      source_branch=SOURCE_BRANCH,
      overwrite=True,
      data_hashes=DATA_HASHES_TO_MERGE,
      batch_size=BATCH_SIZE,
  )

  print(f"Merged {rows_synced} label rows from '{SOURCE_BRANCH}' into '{TARGET_BRANCH}'.")
  ```

  ```python Read, edit, and save back to a branch theme={"dark"}
  # Import dependencies
  from encord import EncordUserClient

  SSH_PATH = "/Users/chris-encord/ssh-private-key.txt"  # Replace with the file path to your SSH private key
  PROJECT_ID = "00000000-0000-0000-0000-000000000000"  # Replace with the unique Project ID
  BRANCH_NAME = "pre-label-v1"
  BUNDLE_SIZE = 100
  MAX_LABEL_ROWS = 10  # Quick-test cap. Remove this slice to edit every label row on the branch.

  user_client = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  project = user_client.get_project(PROJECT_ID)
  label_rows = project.list_label_rows_v2(branch_name=BRANCH_NAME)[:MAX_LABEL_ROWS]

  # Bundle the reads
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for label_row in label_rows:
          label_row.initialise_labels(bundle=bundle)

  # Perform any edits on label_rows here.

  # Bundle the writes back to the branch
  with project.create_bundle(bundle_size=BUNDLE_SIZE) as bundle:
      for label_row in label_rows:
          label_row.save(bundle=bundle)

  print(f"Saved {len(label_rows)} label rows on '{BRANCH_NAME}' (test run).")
  ```
</CodeGroup>

After copying, verify the result by listing label rows on the target branch:

```python Verify the copy theme={"dark"}
verification_rows = project.list_label_rows_v2(branch_name="pre-label-v1")
print(f"'pre-label-v1' now contains {len(verification_rows)} label rows.")
```
