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

> ## Agent Instructions
> These docs contain multiple Speckle product experiences. For Speckle 2026.9, prefer /next/ pages for the topics and changes they cover. The navigation label Current does not override this version-specific precedence.
> Coverage under /next/ is incremental. Use Current documentation for unchanged topics and when a /next/ page says coverage is unavailable or explicitly refers you to Current. Do not infer a feature was removed from a missing page.
> Read the page-specific documentation status, applicability, affected guidance and replacement links in agent-only content. Impacted, changed and superseded apply only to the stated scope; they do not mean the entire feature is deprecated. Historical connector guides apply only to the legacy connector described.
> Match guidance to the customer's deployment version, model data format and connector. Respect plan, permission and compatibility restrictions. If an unknown version or data format changes the answer, ask a focused clarification; do not infer deployment version from the date alone.

# Assess specklepy breaking changes

> Before-and-after specklepy examples so you can judge 2026.9 migration impact.

Documentation set: Speckle 2026.9. Status: current. Authority: canonical for this version and the coverage on this page.

Applies to version 2026.9, from 2026-09; this is not a rollout date for every deployment.

Authoritative Speckle 2026.9 documentation for the topic covered here. For Speckle 2026.9, prefer this page over conflicting earlier guidance. Follow the linked pages in the other documentation version for unchanged topics and where this page says coverage is not yet available. Respect data-format, deployment, plan, permission, and compatibility limitations stated in the page.

<Note>
  This is 2026.9 documentation. Coverage here is incremental: a page exists only where 2026.9
  differs or is newly documented. For any other topic, switch to **Current** (2026.8 and earlier) in
  the version selector.
</Note>

Use this page to judge what a specklepy script must change for 2026.9. It is not a replacement for
the [specklepy](/developers/sdks/python/introduction) guides, and it is not a full API reference.

Prior to 2026.9, the path is still **PAT → `operations.receive` → walk a `Base` tree**. In 2026.9
that receive is a compatibility shim. The new path is **`operations.receive3`**, which returns a
disposable `Model` with columnar properties and typed relations. Dates, compatibility mode, and who
must act are on
[Data model migration for developers](/developers/migration/data-model-migration). The object-model
change itself is on [Object model in 2026.9](/next/developers/object-model/overview).

<Warning>
  Versions published by a 2026.9 connector need specklepy **2026.9.0b3** or later, installed with
  the `bundle` extra. An older build 404s on that data. Versions from older connectors keep working
  on the specklepy you have today. See [Who's
  affected](/developers/migration/data-model-migration#whos-affected).
</Warning>

<Note>
  Prefer `operations.receive3` for new scripts. `operations.receive` stays callable on legacy
  object-graph versions, and on bundle-only versions it returns a projected `Base` tree.
</Note>

## What changes for a script

| Concern | Prior to 2026.9 | In 2026.9 |
| - | - | - |
| Package | `pip install specklepy` | `pip install "specklepy[bundle]==2026.9.0b3"`. The `bundle` extra pulls in pyarrow |
| Load a version | `operations.receive` with `referenced_object` | `operations.receive3` with account plus project, model, and version ids. Returns `Model` |
| Filter and count | Walk `elements`, then `DataObject.properties` | `model.objects`, `objects_with`, and `get_string` / `get_double` |
| Nested property walk | `properties["x"]` then `["y"]` | Dotted path (`get_string("x.y")`) or `PropertyView.under` |
| Level, material, groups | Unpack proxy `objects` lists | Accessors on `ModelObject` (`level`, `host`, `collection`) |
| Publish from a script | `operations.send` then `client.version.create` | `operations.send3` with a `BundleBuilder`. The version is visible only after ingestion completes |
| Transports | `send` / `receive` with `ServerTransport` | Frozen legacy surface. A bundle reference through `receive` needs an authenticated `ServerTransport` |

## Update specklepy first

Install a 2026.9 specklepy that can receive bundle-only versions, including the `bundle` extra.
**2026.9.0b3** is the first public build that does this. A later stable **2026.9.0** replaces the
beta pin; the calls on this page do not change.

```bash theme={null}
pip install "specklepy[bundle]==2026.9.0b3"
```

On a bundle-only version, `version.referenced_object` is a bundle reference such as
`bundle.<projectId>.<modelId>.<versionId>`, not a content hash. An older specklepy treats that
string as an object id, requests it from the objects API, and receives a 404 whose body tells you
to upgrade.

<Warning>
  Do not fetch `version.referenced_object` from the objects REST endpoints. Bundle references always
  404 there, including after you upgrade. Pass ids to `operations.receive3`, or pass the reference
  to `operations.receive` and let specklepy dispatch.
</Warning>

## Load a model

Prior to 2026.9, you resolve `referenced_object` and call `operations.receive`. In 2026.9,
`operations.receive3` takes an `Account` plus project, model, and version ids and returns a
disposable `Model`. Use it as a context manager: it owns the downloaded bundle files on disk until
the `with` block ends.

Auth and version lookup stay on [Authentication](/developers/sdks/python/getting-started/authentication)
and the [Quickstart](/developers/sdks/python/getting-started/quickstart). The snippets below assume
`account`, `client`, `transport`, `project_id`, `model_id`, and `version_id` are already in scope.
Build `account` with `Account.from_token` and a PAT, or `get_default_account()`.

<Tabs>
  <Tab title="Prior to 2026.9 (receive)">
    ```python Prior to 2026.9 lines icon="python" theme={null}
    version = client.version.get(version_id, project_id)
    root = operations.receive(version.referenced_object, remote_transport=transport)
    ```
  </Tab>

  <Tab title="2026.9 (receive3)">
    ```python 2026.9 lines icon="python" theme={null}
    with operations.receive3(account, project_id, model_id, version_id) as model:
        pass
    ```
  </Tab>
</Tabs>

<Tip>
  Close the model (leave the `with` block, or call `model.close()`) so the download directory is
  deleted. Parsed objects stay usable after close. Geometry you have not touched yet cannot be read
  afterwards.
</Tip>

## Count and filter objects

Prior to 2026.9, you walk a `Base` tree. In 2026.9, objects are a flat list. Properties are
path-keyed (`"category"`, `"Constraints.Base Offset"`). Relations are accessors, not Proxy lists.

<Tabs>
  <Tab title="Prior to 2026.9">
    ```python Prior to 2026.9 lines icon="python" theme={null}
    from specklepy.objects.data_objects import DataObject

    def iter_elements(obj):
        yield obj
        for child in getattr(obj, "elements", None) or []:
            yield from iter_elements(child)


    counts = {}
    for obj in iter_elements(root):
        if not isinstance(obj, DataObject):
            continue
        key = (obj.properties or {}).get("category") or "Unknown"
        counts[key] = counts.get(key, 0) + 1

    by_level = {}
    for obj in iter_elements(root):
        if not isinstance(obj, DataObject):
            continue
        key = (obj.properties or {}).get("level") or "Unknown"
        by_level.setdefault(key, []).append(obj)

    walls = [
        o
        for o in iter_elements(root)
        if isinstance(o, DataObject) and (o.properties or {}).get("category") == "Walls"
    ]
    ```
  </Tab>

  <Tab title="2026.9">
    ```python 2026.9 lines icon="python" theme={null}
    counts = {}
    for obj in model.objects:
        key = obj.get_string("category") or obj.name or "Unknown"
        counts[key] = counts.get(key, 0) + 1

    by_level = {}
    for obj in model.objects:
        key = obj.level.name if obj.level else "Unknown"
        by_level.setdefault(key, []).append(obj)

    walls = [o for o in model.objects_with("category") if o.get_string("category") == "Walls"]
    ```
  </Tab>
</Tabs>

Look up one object with `model.object_by_application_id(id)`. `application_id` is the only identity
a bundle object has. Do not treat `Base.id` as a content hash on bundle-only versions.

## Replace property walks

Prior to 2026.9, nested parameters are dictionaries you walk: `obj.properties["x"]` then `["y"]`.
In 2026.9 those paths are flat. `PropertyView` is the type that replaces that walk.
`ModelObject.properties` is already this view.

* **`get_string` / `get_double` / `get_bool`:** typed lookup. `None` if the path is missing or the
  value is another type. Instance, then type, then root-scalar precedence.
* **Indexer (`obj["path"]`):** untyped lookup, same precedence.
* **`properties.under("Constraints")`:** the subtree under that prefix, with the prefix stripped.
* **`to_nested()`:** the old nested-dictionary tree. Allocates. Use only when a caller still
  requires that shape.

Prefer `get_string` / `get_double` for a single path. Use `under` when you need a group.

The snippets below assume `wall` is already in scope: a `DataObject` from the tree, or a
`ModelObject` from `model.objects`.

<Tabs>
  <Tab title="Prior to 2026.9">
    ```python Prior to 2026.9 lines icon="python" theme={null}
    groups = (wall.properties or {}).get("parameters") or {}
    x = groups.get("x") or {}
    y = x.get("y")
    ```
  </Tab>

  <Tab title="2026.9">
    ```python 2026.9 lines icon="python" theme={null}
    y = wall.get_string("parameters.x.y")
    offset = wall.get_double("Constraints.Base Offset")

    constraints = wall.properties.under("Constraints")
    for key, value in constraints.items():
        print(f"{key}: {value}")
    ```
  </Tab>
</Tabs>

<Warning>
  If the script reads `parameter["value"]` or other per-parameter metadata from the received tree,
  that shape is gone on bundle-only versions. Read the value from a dotted path, not from a metadata
  wrapper.
</Warning>

## Replace Proxy walks

Prior to 2026.9, grouping by level or material means indexing `applicationId`s and resolving proxy
`objects` lists. In 2026.9 those links are typed relations. See
[Relations in 2026.9](/next/developers/object-model/relations) for the names.

<Tabs>
  <Tab title="Prior to 2026.9">
    ```python Prior to 2026.9 lines icon="python" theme={null}
    by_app_id = {}
    for obj in iter_elements(root):
        app_id = getattr(obj, "applicationId", None)
        if app_id:
            by_app_id[app_id] = obj
    ```
  </Tab>

  <Tab title="2026.9">
    ```python 2026.9 lines icon="python" theme={null}
    by_level = {}
    for obj in model.objects:
        key = obj.level.name if obj.level else "Unknown"
        by_level.setdefault(key, []).append(obj)

    hosted = [o for o in model.objects if o.host is not None]
    ```
  </Tab>
</Tabs>

A missing nested `elements` tree or Proxy `objects` list on 2026.9 data is expected. Do not unpack
Proxies to rebuild containment.

## What operations.receive still does

`operations.receive` remains callable. It is the compatibility path, not the 2026.9 default.

* **Legacy object-graph versions:** behaviour is unchanged. You still pass `referenced_object` as a
  content hash.
* **Bundle-only versions:** `referenced_object` is a bundle reference
  (`bundle.<projectId>.<modelId>.<versionId>`). A 2026.9 specklepy dispatches on that prefix and
  returns a best-effort `Base` tree (`Model.to_base()`, DataObject idiom, `version = 4` on the
  root). That projection does not scale to the model sizes the bundle format is built for, and it
  is not a lossless round trip. It needs an authenticated `ServerTransport` for the reference's
  project.
* **Older specklepy:** fetching that `referenced_object` from the objects API returns **404** with
  upgrade guidance. Upgrade specklepy rather than parsing the 404 body.

<Warning>
  Receiving a bundle-only version as a `Base` tree and sending it again with `operations.send` is
  not a supported copy workflow. The tree is a baked view, not the authored graph. Receive, derive
  new data, then send remains ordinary supported use.
</Warning>

## Publish from a script

Most analysis scripts only receive. If you currently call `operations.send` then
`client.version.create`, the 2026.9 replacement is `operations.send3` with a `BundleBuilder`.
`operations.send` stays accepted and starts emitting deprecation warnings. There is no removal date.

`send3` returns a reserved version id immediately. Queries, the version URL, and the `version_create`
webhook fire when ingestion completes. Poll for the version, or watch the project's model
ingestions, if you need to follow up in the same process.

## FAQ

<AccordionGroup>
  <Accordion title="Do I have to rewrite an operations.receive script on day one?">
    Upgrade specklepy first so versions from a 2026.9 connector do not 404. Versions from older
    connectors keep working on `operations.receive`. Rewrite to `operations.receive3` when you need
    relations, columnar property lookups, or models that are too large to materialize as a `Base`
    tree.
  </Accordion>

  <Accordion title="Why does receive3 return Model instead of Base?">
    `Model` is the bundle itself: objects, path-keyed properties, and relation accessors, with
    geometry parsed only when you ask. A `Base` tree inflates every object and mesh into Python
    objects. That is why `operations.receive` is the compatibility path for the new format, not the
    default.
  </Accordion>

  <Accordion title="What happens if I forget to close Model?">
    Downloaded parquet files stay under a scratch directory until `close` runs. Always use a `with`
    block. Objects and already-loaded geometry stay in memory after close; geometry you have not
    touched yet cannot be read afterwards.
  </Accordion>

  <Accordion title="Can I keep using ServerTransport?">
    Yes for hash-era data, until that path is retired. For bundle-only versions,
    `operations.receive` still needs an authenticated `ServerTransport` so it can dispatch to
    `receive3`. Move server-facing scripts to `operations.receive3`.
  </Accordion>

  <Accordion title="Why did parameter['value'] stop working?">
    On a bundle-only receive, parameter metadata is collapsed to scalars on dotted paths. Read the
    value with `get_double` or `obj["Width"]`, not from a metadata wrapper. That is a contracted
    receive-fidelity change, not a bug in your filter.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.