> ## 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.

# Data Traversal

> Understanding how to navigate and extract data from Speckle's object graphs

## Overview

**Data traversal** is the systematic process of navigating Speckle's object graphs to extract data, find specific objects, and preserve hierarchical context. Understanding traversal is fundamental to working with Speckle data effectively.

<Info>
  **Prerequisites:** This guide assumes familiarity with the [Base class](/developers/sdks/python/concepts/objects) and [data types](/developers/sdks/python/concepts/data-types). If you're new to Speckle objects, start with those concepts first.
</Info>

Speckle objects form hierarchical graphs - tree-like structures where objects contain other objects, arrays of objects, and references to objects stored elsewhere. Unlike simple flat lists, these graphs require strategic navigation to:

* Extract all objects of a specific type (e.g., all walls, all meshes)
* Preserve parent-child relationships and organizational context
* Filter data based on properties or structure
* Build analysis pipelines that understand object hierarchies

**Key Concepts:**

* **Graph Structure**: Objects are connected through properties (parent → child relationships)
* **Traversal**: The process of visiting every object in a graph systematically
* **Filtering**: Selecting only objects that match specific criteria during traversal
* **Flattening**: Converting a hierarchical graph into a flat list of objects

```python lines icon="python" theme={null}
from specklepy.api import operations
from specklepy.objects import Base

# Receive an object graph
obj = operations.receive(object_id, remote_transport=transport)

# Navigate the graph
for name in obj.get_member_names():
    value = getattr(obj, name)
    print(f"{name}: {type(value).__name__}")
```

## Understanding Object Graphs

Speckle data comes in different structural complexities depending on the source and use case. Understanding these patterns helps you choose the right traversal strategy.

### Simple Graph (Custom Data)

```python lines icon="python" theme={null}
# Linear structure
project
├── name: "Building A"
├── date: "2024-01-15"
└── measurements: [Point, Point, Point]
```

### Medium Graph (Simple Model)

```python lines icon="python" theme={null}
# 2-3 levels deep
collection
├── name: "Walls"
├── items: [
│   ├── Mesh (displayValue)
│   │   ├── vertices: [...]
│   │   └── properties: {...}
│   └── Mesh
│       ├── vertices: [...]
│       └── properties: {...}
└── count: 2
```

### Complex Graph (BIM Data)

```python lines icon="python" theme={null}
# 3+ levels with references (v3 DataObject)
DataObject
├── name: "Basic Wall - 200mm"
├── speckle_type: "Objects.Data.DataObject"
├── properties
│   ├── category: "Walls"
│   ├── family: "Basic Wall"
│   ├── type: "200mm"
│   ├── volume: 15.5
│   ├── area: 45.0
│   ├── level: "Level 1"
│   └── materials
│       ├── Concrete: {volume: 10.2, area: 30.5}
│       └── Steel: {volume: 0.5, area: 2.1}
├── displayValue: [Mesh, Mesh]  # List of geometry
└── applicationId: "guid-123..."
```

## Why Traversal Matters

Different data structures require different traversal strategies:

| Data Type                | Structure      | Best Strategy                   | Why                              |
| ------------------------ | -------------- | ------------------------------- | -------------------------------- |
| **Custom Data**          | Flat or simple | Direct access                   | Properties are known             |
| **Geometry Collections** | Medium depth   | Type-based traversal            | Find specific geometry types     |
| **BIM Models**           | Deep hierarchy | Property/displayValue filtering | BIM semantics in properties      |
| **Large Models**         | Very deep      | elements\[] traversal           | Follows organizational structure |

### Common Traversal Goals

1. **Find specific objects** - "Get all walls" or "Find all meshes"
2. **Extract properties** - "List all volumes" or "Get material quantities"
3. **Flatten hierarchy** - "Convert nested structure to flat list"
4. **Group objects** - "Organize by level" or "Group by category"
5. **Analyze relationships** - "Which objects are on Level 1?"

## Traversal Strategies Explained

<Info>
  **For v3 Data:** The most effective strategies are **Property-Filtered**, **displayValue-Based**, and **elements\[] Hierarchy** traversal. Type-filtered traversal using `speckle_type` is primarily useful for geometry primitives (Mesh, Point, Line) but not for BIM objects, which are now generic `DataObject` instances with semantics in their properties.
</Info>

### Strategy 1: Property-Filtered Traversal ⭐ Recommended for v3

**When to use:** Working with BIM data from connectors (Revit, Rhino, ArchiCAD, etc.).

**How it works:**

* Traverse recursively
* Check if object has a `properties` dictionary
* Filter based on property values (e.g., `category == "Walls"`)
* Collect matching objects

**Best for:**

* v3 BIM objects (DataObject)
* Finding objects by category, family, type
* Filtering by quantities or metadata
* Most common pattern for modern Speckle data

**Example use cases:**

* "Find all walls" → filter by `properties.category == "Walls"`
* "Get steel beams" → filter by `properties.category == "Structural Framing"` AND `properties.material == "Steel"`
* "Objects on Level 1" → filter by `properties.level == "Level 1"`

### Strategy 2: displayValue-Based Traversal ⭐ Recommended for v3

**When to use:** You only want atomic, viewer-selectable objects.

**How it works:**

* Traverse recursively
* Collect only objects with a `displayValue` property
* These are the objects that appear as selectable items in the viewer

**Best for:**

* Getting viewer-clickable objects
* Counting actual BIM elements (not containers/groups)
* Extracting geometry for visualization
* Distinguishing atomic objects from organizational containers

**Key insight:** Objects without displayValue are typically containers, not atomic elements.

### Strategy 3: elements\[] Hierarchy Traversal

**When to use:** Objects are organized in a logical hierarchy using `elements[]` arrays.

<Info>
  The `elements` property is Speckle's standardized convention for organizing hierarchical data. See [The elements Convention](/developers/sdks/python/concepts/data-types#the-elements-convention) for a detailed explanation of this pattern.
</Info>

**How it works:**

* Check if object has an `elements` property (list)
* Recursively traverse only the elements array
* Optionally filter during traversal
* Ignores other properties

**Best for:**

* Collections and Groups
* Organized BIM hierarchies (Level → Room → Elements)
* When you only care about the logical organization, not nested geometry

**Advantage:** Faster than full traversal, follows intentional structure

### Strategy 4: Full Recursive Traversal

**When to use:** You need to visit EVERY object in the graph, regardless of type or depth.

**How it works:**

* Start at root object
* Visit each property on the object
* For each property that's a Base object, list, or dict, recurse into it
* Continue until all branches are explored

**Best for:**

* Finding rare objects that could be anywhere
* Building complete inventories
* Ensuring nothing is missed

**Cost:** Slowest, visits everything

### Strategy 5: Type-Filtered Traversal (Limited Use in v3)

**When to use:** Looking for geometry primitives (Mesh, Point, Line, etc.).

**How it works:**

* Traverse recursively
* Check each object's `speckle_type` property
* Collect only objects matching the target type
* Continue traversing to find all instances

**Best for:**

* Finding geometry objects (Mesh, Line, Point, Arc, Circle, etc.)
* Pure geometry workflows
* Legacy v2 objects with typed BIM classes

<Warning>
  **Limited Use in v3 BIM Data:** Most BIM objects from connectors are now `DataObject` instances with `speckle_type = "Objects.Data.DataObject"`. Filtering by this type returns ALL BIM objects without differentiation. For BIM data, use **Property-Filtered Traversal (Strategy 1)** instead to filter by category, family, type, etc.
</Warning>

## Choosing the Right Pattern

Use this decision tree for v3 data:

```
Working with BIM data from connectors?
├─ YES → Use Property-Filtered Traversal (Strategy 1)
│        Filter by: category, family, type, level, material, etc.
└─ NO → Continue...

Need only viewer-visible objects?
├─ YES → Use displayValue-Based Traversal (Strategy 2)
│        Gets atomic, selectable objects
└─ NO → Continue...

Following organizational hierarchy (Collections, Groups)?
├─ YES → Use elements[] Hierarchy Traversal (Strategy 3)
│        Faster, follows intentional structure
└─ NO → Continue...

Looking for geometry primitives (Mesh, Point, Line)?
├─ YES → Use type-filtered traversal (Pattern 1)
└─ NO → Continue...

Are you looking for v3 BIM objects (walls, columns, etc.)?
├─ YES → Use property-filtered traversal (Pattern 2)
│        or displayValue filtering (Pattern 3b)
└─ NO → Continue...

Is data organized in elements[] arrays?
├─ YES → Use elements[] traversal (Pattern 3a)
└─ NO → Use full recursive traversal
```

## Basic Traversal Techniques

### Technique 1: Direct Property Access

**When to use:** You know the exact structure and property names.

**Advantages:** Fastest, most efficient, no unnecessary traversal.

```python lines icon="python" theme={null}
from specklepy.api import operations

obj = operations.receive(object_id, remote_transport=transport)

# Direct access (if you know the structure)
if hasattr(obj, "name"):
    print(f"Name: {obj.name}")

if hasattr(obj, "measurements"):
    for measurement in obj.measurements:
        print(f"Point: ({measurement.x}, {measurement.y}, {measurement.z})")
```

### Technique 2: Iterate All Members

When you want to explore:

```python lines icon="python" theme={null}
from specklepy.objects import Base

def print_members(obj, indent=0):
    """Print all members of an object."""
    prefix = "  " * indent
    
    if isinstance(obj, Base):
        print(f"{prefix}{obj.speckle_type}")
        
        for name in obj.get_member_names():
            if name.startswith("_"):
                continue
            
            value = getattr(obj, name, None)
            print(f"{prefix}  {name}:")
            print_members(value, indent + 2)
    
    elif isinstance(obj, list):
        print(f"{prefix}[List: {len(obj)} items]")
        if obj:
            print_members(obj[0], indent + 1)  # Show first item
    
    elif isinstance(obj, dict):
        print(f"{prefix}{{Dict: {len(obj)} keys}}")
        for key, value in list(obj.items())[:3]:  # Show first 3
            print(f"{prefix}  {key}:")
            print_members(value, indent + 2)
    
    else:
        # Primitive value
        value_str = str(obj)[:50]  # Limit length
        print(f"{prefix}{value_str}")

# Use it
print_members(obj)
```

### Technique 3: Recursive Search

Find all objects matching criteria:

```python lines icon="python" theme={null}
from specklepy.objects import Base

def find_all(root, predicate):
    """Find all objects matching a predicate function."""
    results = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Check if this object matches
            if predicate(obj):
                results.append(obj)
            
            # Traverse children
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
        
        elif isinstance(obj, dict):
            for value in obj.values():
                traverse(value)
    
    traverse(root)
    return results

# Use it: find all meshes
from specklepy.objects.geometry import Mesh

meshes = find_all(obj, lambda x: isinstance(x, Mesh))
print(f"Found {len(meshes)} meshes")

# Find all walls by checking properties (v3 pattern)
from specklepy.objects.data_objects import DataObject
walls = find_all(obj, lambda x: isinstance(x, DataObject) and x.properties.get("category") == "Walls")
print(f"Found {len(walls)} walls")
```

## Pattern 1: Find by Property ⭐ Primary Pattern for v3

**Purpose:** Search for objects based on their properties (the standard v3 pattern).

**When to use:**

* Working with BIM data from any connector (Revit, Rhino, ArchiCAD, etc.)
* Need to find objects by category, family, type, level, material
* Filtering by quantities, parameters, or metadata
* **This is the primary pattern for modern Speckle data**

**How it works:**

1. Recursively traverse all objects
2. Check if object has `properties` dictionary
3. Filter based on property values
4. Collect matches

**Purpose:** Find objects based on their properties dictionary values (v3 BIM pattern).

**When to use:**

* Working with v3 BIM data (DataObject)
* Need to find objects by category ("Walls", "Columns", "Floors")
* Filtering by BIM properties (loadBearing, fireRating, family, type)
* Need to find objects with specific quantities or metadata

**Advantages:**

* Works with v3 object model
* Can filter on any property value
* Can combine multiple property conditions
* Reflects how BIM data is actually organized

**How it works:**

1. Traverse all objects
2. Check if object is a DataObject
3. Access the `properties` dictionary
4. Filter based on property values
5. Collect matches

```python lines icon="python" theme={null}
from specklepy.objects import Base

def find_by_property(root, property_name, property_value=None):
    """Find objects with a specific property (optionally with value)."""
    results = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Check if property exists
            if hasattr(obj, property_name):
                value = getattr(obj, property_name)
                
                # If checking value too
                if property_value is None or value == property_value:
                    results.append(obj)
            
            # Check properties dictionary
            if hasattr(obj, "properties") and isinstance(obj.properties, dict):
                if property_name in obj.properties:
                    value = obj.properties[property_name]
                    if property_value is None or value == property_value:
                        results.append(obj)
            
            # Traverse children
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    child = getattr(obj, name, None)
                    traverse(child)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return results

# Use it
steel_elements = find_by_property(obj, "Material", "Steel")
exterior_walls = find_by_property(obj, "Function", "Exterior")
all_with_volume = find_by_property(obj, "volume")  # Any object with volume

print(f"Steel elements: {len(steel_elements)}")
print(f"Exterior walls: {len(exterior_walls)}")
print(f"Objects with volume: {len(all_with_volume)}")
```

## Pattern 2: Find by Type (Geometry Only)

**Purpose:** Search for geometry primitives by `speckle_type`.

**When to use:**

* Looking for geometry objects (Mesh, Point, Line, Arc, Circle, etc.)
* Pure geometry workflows without BIM semantics
* Need to find all instances of a specific geometry class

**When NOT to use:**

* ❌ BIM data from connectors (use Pattern 1 - Find by Property instead)
* ❌ Need to differentiate walls from columns (use properties)
* ❌ Any data where semantics are in the properties dictionary

<Warning>
  **Limited Use for BIM Data:** In v3, BIM objects are `DataObject` instances with `speckle_type = "Objects.Data.DataObject"`. All walls, columns, beams, etc. have the same type. To find specific BIM elements, use **Pattern 1 (Find by Property)** to filter by `category`, `family`, `type`, etc.
</Warning>

```python lines icon="python" theme={null}
from specklepy.objects import Base

def find_by_type(root, target_type):
    """Find all objects with a specific speckle_type (for geometry objects)."""
    results = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            if obj.speckle_type == target_type:
                results.append(obj)
            
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
        
        elif isinstance(obj, dict):
            for value in obj.values():
                traverse(value)
    
    traverse(root)
    return results

# Good use cases - geometry primitives
points = find_by_type(obj, "Objects.Geometry.Point")
meshes = find_by_type(obj, "Objects.Geometry.Mesh")
lines = find_by_type(obj, "Objects.Geometry.Line")

print(f"Found {len(points)} points, {len(meshes)} meshes, {len(lines)} lines")

# ❌ Won't work for BIM data:
# data_objects = find_by_type(obj, "Objects.Data.DataObject")  
# # Returns ALL BIM objects - walls, columns, beams, everything!
# # Use Pattern 1 (find_by_property) instead
```

## Pattern 3: Build Flat List

**Purpose:** Convert hierarchical object graphs into flat lists for easier processing.

**When to use:**

* Need to process all objects of a certain type
* Want to use list operations (filter, map, sort)
* Building dataframes or CSV exports
* Running aggregate calculations (sum, average, count)

**Advantages:**

* Easier to work with than nested structures
* Can use standard Python list operations
* Simple to convert to pandas DataFrame
* Good for batch processing

**Disadvantages:**

* Loses hierarchical relationships
* Can be memory-intensive for large models
* May include objects you don't need

**How it works:**

1. Start with empty results list
2. Traverse entire object graph
3. Collect objects matching criteria
4. Return flat list

```python lines icon="python" theme={null}
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh
from specklepy.objects.data_objects import DataObject

def flatten_by_category(root, category_name):
    """Build a flat list of BIM objects by category (v3 pattern)."""
    results = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Check properties for category (v3 pattern)
            if hasattr(obj, "properties") and isinstance(obj.properties, dict):
                if obj.properties.get("category") == category_name:
                    results.append(obj)
            
            # Continue traversing even after match
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
        
        elif isinstance(obj, dict):
            for value in obj.values():
                traverse(value)
    
    traverse(root)
    return results

# For geometry primitives, use isinstance checks
def flatten_geometry(root, geometry_class):
    """Build a flat list of geometry objects by class."""
    results = []
    
    def traverse(obj):
        if isinstance(obj, geometry_class):
            results.append(obj)
        elif isinstance(obj, Base):
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return results

# Use it
from specklepy.objects.geometry import Mesh, Point
meshes = flatten_geometry(obj, Mesh)
points = flatten_geometry(obj, Point)
```

**Alternative: Property-based flattening (most flexible)**

```python lines icon="python" theme={null}
def flatten_by_properties(root, **filters):
    """Build a flat list of DataObjects by category (for v3 BIM objects)."""
    results = []
    
    def traverse(obj):
        if isinstance(obj, DataObject):
            obj_category = obj.properties.get("category", "")
            if obj_category == category:
                results.append(obj)
        
        if isinstance(obj, Base):
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return results

# Use it for geometry
all_meshes = flatten_by_type(obj, "Objects.Geometry.Mesh")
print(f"Flattened to {len(all_meshes)} meshes")

# Use it for BIM objects (v3 pattern)
all_walls = flatten_data_objects_by_category(obj, "Walls")
all_columns = flatten_data_objects_by_category(obj, "Columns")
print(f"Found {len(all_walls)} walls, {len(all_columns)} columns")
```

### Pattern 3a: Traverse elements\[] Arrays

**Purpose:** Navigate object hierarchies by following the organizational structure defined by `elements[]` arrays.

**When to use:**

* Data is organized in Collections or Groups
* Objects have a logical hierarchy (Building → Level → Room → Elements)
* Want to respect the intentional organization
* Need faster traversal by ignoring non-structural properties

**How it works:**

1. Check if object has `elements` property (list)
2. Recursively traverse only through elements arrays
3. Optionally filter during traversal
4. Ignore other properties like displayValue, geometry, etc.

**Key concept:** Many Speckle objects use `elements[]` to define parent-child relationships:

* **Collection** objects have `elements` containing child objects
* **Group** objects organize related objects in `elements`
* **Level** hierarchies nest objects in `elements`

**Advantages:**

* Faster than full traversal
* Follows logical organization
* Respects data structure intent
* Avoids traversing geometry details

```python lines icon="python" theme={null}
from specklepy.objects import Base
from specklepy.objects.data_objects import DataObject

def flatten_via_elements(root, filter_func=None):
    """
    Flatten object hierarchy by following elements[] arrays.
    Collections and groups typically have an elements property containing child objects.
    """
    results = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Check if this object matches filter
            if filter_func is None or filter_func(obj):
                results.append(obj)
            
            # Recursively traverse elements array
            if hasattr(obj, "elements") and isinstance(obj.elements, list):
                for element in obj.elements:
                    traverse(element)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return results

# Use it - get all objects in elements hierarchy
all_objects = flatten_via_elements(obj)
print(f"Total objects in elements tree: {len(all_objects)}")

# Filter by type while traversing
walls = flatten_via_elements(
    obj, 
    lambda o: isinstance(o, DataObject) and o.properties.get("category") == "Walls"
)
print(f"Walls in elements tree: {len(walls)}")

# Filter by property
load_bearing = flatten_via_elements(
    obj,
    lambda o: isinstance(o, DataObject) and o.properties.get("loadBearing") == True
)
print(f"Load bearing elements: {len(load_bearing)}")
```

### Pattern 3b: Find Atomic/Displayable Objects

**Purpose:** Find only atomic, viewer-selectable objects by filtering for `displayValue` presence.

**When to use:**

* Need to count actual BIM elements (not containers or groups)
* Want objects that appear as selectable items in the viewer
* Building element lists for UI selection
* Extracting objects that have visual representation
* Need to match what users see in the 3D viewer

**Key concept:** Not all objects in the graph are "real" elements:

* **Containers** (Collections, Groups) organize but aren't selectable
* **Proxies** (LevelProxy, ColorProxy) reference but aren't rendered
* **Atomic objects** have `displayValue` and ARE selectable in the viewer

<Info>
  **Why displayValue matters:** Objects with a `displayValue` property represent atomic, selectable items in the Speckle viewer. Each object with `displayValue` can be clicked, selected, and queried independently in the 3D view. The `displayValue` typically contains a list of `Mesh` objects that define the visual representation.
</Info>

**How it works:**

1. Traverse the entire graph
2. Check if object has `displayValue` property
3. Verify `displayValue` is not None
4. Collect these objects
5. Optionally filter further by properties

**Result:** A list of objects that exactly matches what users can select in the viewer.

```python lines icon="python" theme={null}
from specklepy.objects import Base
from specklepy.objects.data_objects import DataObject

def find_displayable_objects(root):
    """
    Find all objects with displayValue property.
    These are atomic objects that can be clicked/selected in the Speckle viewer.
    """
    displayable = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Check if this object has displayValue
            if hasattr(obj, "displayValue") and obj.displayValue is not None:
                displayable.append(obj)
            
            # Continue traversing to find nested displayable objects
            if hasattr(obj, "elements") and isinstance(obj.elements, list):
                for element in obj.elements:
                    traverse(element)
            
            # Also traverse other properties
            for name in obj.get_member_names():
                if not name.startswith("_") and name != "displayValue":
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return displayable

# Find all atomic objects
atomic_objects = find_displayable_objects(obj)
print(f"Found {len(atomic_objects)} displayable objects")

# These are the objects that appear as selectable items in the viewer
for atomic_obj in atomic_objects[:5]:  # First 5
    if isinstance(atomic_obj, DataObject):
        category = atomic_obj.properties.get("category", "Unknown")
        print(f"  {atomic_obj.name} ({category})")
        print(f"    Display meshes: {len(atomic_obj.displayValue)}")
```

### Combined Pattern: Elements + DisplayValue

**Purpose:** Efficiently find atomic BIM objects by combining structural traversal with displayValue filtering.

**When to use:**

* Large BIM models where full traversal is slow
* Data is organized in elements\[] hierarchy
* Only want viewer-selectable objects
* Need to skip containers and organizational objects

**Advantages:**

* Faster than full traversal (follows elements\[] only)
* More precise than elements\[] alone (filters to atomic objects)
* Gets you exactly what users see in the viewer
* Skips geometry details and nested references

**Key Optimization:** Objects with `displayValue` are typically **leaf nodes** - they don't have children with displayValues. This means you can stop traversing deeper once you find a displayValue, making traversal much more efficient.

**Strategy:**

1. Traverse through elements\[] arrays only (ignore other properties)
2. At each object, check for displayValue
3. Collect objects that have displayValue and STOP traversing deeper
4. Skip containers and organizational objects automatically

```python lines icon="python" theme={null}
def find_displayable_in_elements(root):
    """
    Traverse elements[] hierarchy and collect objects with displayValue.
    This gives you the atomic BIM objects that are viewer-selectable.
    """
    displayable = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # If it has displayValue, it's an atomic object (leaf node)
            if hasattr(obj, "displayValue") and obj.displayValue is not None:
                displayable.append(obj)
                return  # STOP - leaf nodes rarely have children with displayValue
            
            # Container node - continue down elements hierarchy
            if hasattr(obj, "elements") and isinstance(obj.elements, list):
                for element in obj.elements:
                    traverse(element)
    
    traverse(root)
    return displayable

# Get all viewer-selectable BIM objects
selectable = find_displayable_in_elements(obj)
print(f"Selectable objects: {len(selectable)}")

# Analyze by category
from collections import defaultdict
by_category = defaultdict(int)

for obj in selectable:
    if isinstance(obj, DataObject):
        category = obj.properties.get("category", "Other")
        by_category[category] += 1

print("\nBy category:")
for category, count in sorted(by_category.items()):
    print(f"  {category}: {count}")
```

## Pattern 4: Group by Property

**Purpose:** Organize flat lists of objects into groups based on shared property values.

**When to use:**

* Need to analyze objects by category, type, or level
* Building summaries ("count by type")
* Preparing data for reports or charts
* Want to process each group separately
* Need to find relationships between objects

**Common groupings:**

* By BIM category (Walls, Columns, Floors)
* By level/storey ("Level 1", "Level 2")
* By family or type
* By material
* By any property value

**How it works:**

1. First, flatten objects (Pattern 3)
2. Iterate through flat list
3. Extract grouping property from each object
4. Build dictionary with property value as key
5. Append objects to appropriate group

**Result:** Dictionary where keys are property values and values are lists of objects.

```python lines icon="python" theme={null}
from collections import defaultdict
from specklepy.objects import Base

def group_by_property(root, property_name, type_filter=None):
    """Group objects by a property value."""
    groups = defaultdict(list)
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Apply type filter if specified
            if type_filter and type_filter not in obj.speckle_type:
                # Still traverse children
                for name in obj.get_member_names():
                    if not name.startswith("_"):
                        value = getattr(obj, name, None)
                        traverse(value)
                return
            
            # Try direct property
            if hasattr(obj, property_name):
                key = getattr(obj, property_name)
                groups[key].append(obj)
            
            # Try properties dict
            elif hasattr(obj, "properties") and isinstance(obj.properties, dict):
                if property_name in obj.properties:
                    key = obj.properties[property_name]
                    groups[key].append(obj)
            
            # Traverse children
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return dict(groups)

# Use it
by_material = group_by_property(obj, "Material")
for material, elements in by_material.items():
    print(f"{material}: {len(elements)} elements")

by_level = group_by_property(obj, "level", type_filter="Revit")
for level, elements in by_level.items():
    print(f"{level}: {len(elements)} elements")
```

## Pattern 5: Extract to DataFrame

Convert graph to tabular data:

```python lines icon="python" theme={null}
import pandas as pd
from specklepy.objects import Base

def extract_to_dataframe(root, type_filter=None, properties=None):
    """Extract objects to pandas DataFrame."""
    rows = []
    
    def traverse(obj):
        if isinstance(obj, Base):
            # Apply type filter
            if type_filter and type_filter not in obj.speckle_type:
                # Still traverse children
                for name in obj.get_member_names():
                    if not name.startswith("_"):
                        value = getattr(obj, name, None)
                        traverse(value)
                return
            
            # Build row
            row = {
                "id": obj.id,
                "type": obj.speckle_type,
            }
            
            # Add specified properties
            if properties:
                for prop in properties:
                    # Direct property
                    if hasattr(obj, prop):
                        row[prop] = getattr(obj, prop)
                    # Properties dict
                    elif hasattr(obj, "properties") and isinstance(obj.properties, dict):
                        row[prop] = obj.properties.get(prop)
                    else:
                        row[prop] = None
            else:
                # Include all direct properties (not nested)
                for name in obj.get_typed_member_names():
                    value = getattr(obj, name, None)
                    if not isinstance(value, (Base, list, dict)):
                        row[name] = value
                
                # Include properties dict
                if hasattr(obj, "properties") and isinstance(obj.properties, dict):
                    for key, value in obj.properties.items():
                        if not isinstance(value, (Base, list, dict)):
                            row[key] = value
            
            rows.append(row)
            
            # Traverse children
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
    
    traverse(root)
    return pd.DataFrame(rows)

# Use it - filter by DataObject with specific category
df = extract_to_dataframe(obj, type_filter="Objects.Data.DataObject")
# Then filter by properties
walls_df = df[df["properties"].apply(lambda p: p.get("category") == "Walls" if isinstance(p, dict) else False)]
print(walls_df.head())

# With specific properties
df = extract_to_dataframe(
    obj,
    type_filter="Wall",
    properties=["family", "type", "Volume", "Area", "Material"]
)
print(df[["family", "type", "Volume", "Area"]].head())
```

## Pattern 6: Count by Type

Quick statistics:

```python lines icon="python" theme={null}
from collections import Counter
from specklepy.objects import Base

def count_by_type(root):
    """Count objects by speckle_type."""
    counts = Counter()
    
    def traverse(obj):
        if isinstance(obj, Base):
            counts[obj.speckle_type] += 1
            
            for name in obj.get_member_names():
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                traverse(item)
        
        elif isinstance(obj, dict):
            for value in obj.values():
                traverse(value)
    
    traverse(root)
    return counts

# Use it
counts = count_by_type(obj)

print("Object counts:")
for speckle_type, count in counts.most_common():
    print(f"  {speckle_type}: {count}")

# Summary by category
geometry_count = sum(count for type_name, count in counts.items() if "Geometry" in type_name)
revit_count = sum(count for type_name, count in counts.items() if "Revit" in type_name)

print(f"\nGeometry objects: {geometry_count}")
print(f"Revit objects: {revit_count}")
```

## Handling Unknown Structures

When you don't know the structure ahead of time:

```python lines icon="python" theme={null}
from specklepy.objects import Base

def safe_traverse(obj, callback, max_depth=10, _depth=0):
    """Safely traverse unknown structures with depth limit."""
    if _depth > max_depth:
        return
    
    try:
        if isinstance(obj, Base):
            # Call callback with object
            callback(obj, _depth)
            
            # Traverse members safely
            for name in obj.get_member_names():
                if name.startswith("_"):
                    continue
                
                try:
                    value = getattr(obj, name, None)
                    safe_traverse(value, callback, max_depth, _depth + 1)
                except Exception as e:
                    print(f"Warning: Error accessing {name}: {e}")
        
        elif isinstance(obj, list):
            for i, item in enumerate(obj):
                try:
                    safe_traverse(item, callback, max_depth, _depth + 1)
                except Exception as e:
                    print(f"Warning: Error at list index {i}: {e}")
        
        elif isinstance(obj, dict):
            for key, value in obj.items():
                try:
                    safe_traverse(value, callback, max_depth, _depth + 1)
                except Exception as e:
                    print(f"Warning: Error at dict key {key}: {e}")
    
    except Exception as e:
        print(f"Warning: Traversal error at depth {_depth}: {e}")

# Use it
found_objects = []

def collect_meshes(obj, depth):
    if "Mesh" in obj.speckle_type:
        found_objects.append(obj)

safe_traverse(obj, collect_meshes)
print(f"Safely found {len(found_objects)} meshes")
```

## Performance Optimization

### Memoization

Cache traversal results:

```python lines icon="python" theme={null}
from specklepy.objects import Base

class CachedTraverser:
    """Traverser with result caching."""
    
    def __init__(self):
        self.cache = {}
    
    def find_by_type(self, root, speckle_type):
        """Find objects with caching."""
        cache_key = (id(root), speckle_type)
        
        if cache_key in self.cache:
            return self.cache[cache_key]
        
        results = []
        
        def traverse(obj):
            if isinstance(obj, Base):
                if obj.speckle_type == speckle_type:
                    results.append(obj)
                
                for name in obj.get_member_names():
                    if not name.startswith("_"):
                        value = getattr(obj, name, None)
                        traverse(value)
            
            elif isinstance(obj, list):
                for item in obj:
                    traverse(item)
        
        traverse(root)
        self.cache[cache_key] = results
        return results

# Use it
traverser = CachedTraverser()

# First call - does traversal
meshes = traverser.find_by_type(obj, "Objects.Geometry.Mesh")

# Second call - uses cache
meshes_again = traverser.find_by_type(obj, "Objects.Geometry.Mesh")  # Fast!
```

### Early Termination

Stop when found:

```python lines icon="python" theme={null}
from specklepy.objects import Base

def find_first(root, predicate):
    """Find first object matching predicate (stops early)."""
    result = [None]  # Use list to allow modification in nested function
    
    def traverse(obj):
        if result[0] is not None:
            return  # Already found
        
        if isinstance(obj, Base):
            if predicate(obj):
                result[0] = obj
                return
            
            for name in obj.get_member_names():
                if result[0] is not None:
                    return
                if not name.startswith("_"):
                    value = getattr(obj, name, None)
                    traverse(value)
        
        elif isinstance(obj, list):
            for item in obj:
                if result[0] is not None:
                    return
                traverse(item)
    
    traverse(root)
    return result[0]

# Use it
first_wall = find_first(obj, lambda x: "Wall" in x.speckle_type)
if first_wall:
    print(f"Found wall: {first_wall.speckle_type}")
```

## Complete Example: Building Analysis

```python lines icon="python" theme={null}
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.transports.server import ServerTransport
from collections import defaultdict
import pandas as pd

# 1. Receive building data
client = SpeckleClient(host="app.speckle.systems")
client.authenticate_with_token(token)

version = client.version.get(project_id, version_id)
transport = ServerTransport(stream_id=project_id, client=client)
building = operations.receive(version.referencedObject, remote_transport=transport)

print(f"Received: {building.speckle_type}")

# 2. Count object types
counts = count_by_type(building)
print("\n=== Object Inventory ===")
for obj_type, count in counts.most_common(10):
    print(f"{obj_type}: {count}")

# 3. Find all walls (v3 pattern - by property)
walls = find_by_property(building, "category", "Walls")
print(f"\n=== Walls ===")
print(f"Total walls: {len(walls)}")

# 4. Group walls by material
by_material = group_by_property(building, "Material", type_filter="Wall")
print("\nBy material:")
for material, wall_list in by_material.items():
    print(f"  {material}: {len(wall_list)}")

# 5. Extract to DataFrame for analysis
df = extract_to_dataframe(
    building,
    type_filter="Wall",
    properties=["family", "type", "Volume", "Area", "Material"]
)

print("\n=== Statistical Analysis ===")
print(f"Total volume: {df['Volume'].sum():.2f} m³")
print(f"Total area: {df['Area'].sum():.2f} m²")
print(f"Average wall volume: {df['Volume'].mean():.2f} m³")

# 6. Export results
df.to_csv("building_analysis.csv", index=False)
print("\n✓ Analysis exported to building_analysis.csv")
```

## Best Practices

<AccordionGroup>
  <Accordion title="Always check property existence">
    Use `hasattr()` before accessing:

    ```python theme={null}
    # Good
    if hasattr(obj, "displayValue"):
        mesh = obj.displayValue

    # Bad - can crash
    mesh = obj.displayValue
    ```
  </Accordion>

  <Accordion title="Skip private members">
    Don't traverse `_` prefixed properties:

    ```python theme={null}
    # Good
    for name in obj.get_member_names():
        if not name.startswith("_"):
            value = getattr(obj, name)

    # Bad - includes internals
    for name in dir(obj):
        value = getattr(obj, name)
    ```
  </Accordion>

  <Accordion title="Handle both lists and single values">
    Properties can be lists or single objects:

    ```python theme={null}
    # Good
    display = obj.displayValue
    meshes = display if isinstance(display, list) else [display]

    # Bad - assumes always list
    for mesh in obj.displayValue:  # Crashes if not list
        pass
    ```
  </Accordion>

  <Accordion title="Use type filters to reduce traversal">
    Limit traversal scope when possible:

    ```python theme={null}
    # Good - focused property search (v3 pattern)
    walls = find_by_property(obj, "category", "Walls")

    # Less good - searches everything then filters
    all_objects = find_all(obj, lambda x: True)
    walls = [o for o in all_objects if hasattr(o, "properties") and o.properties.get("category") == "Walls"]
    ```
  </Accordion>
</AccordionGroup>

## Summary

Effective traversal requires:

* ✅ **Understanding the graph** - Know if it's simple, medium, or complex
* ✅ **Choosing the right pattern** - Type, property, flatten, group
* ✅ **Defensive coding** - Check before accessing
* ✅ **Performance awareness** - Cache results, terminate early
* ✅ **Flexibility** - Handle unknown structures gracefully

These patterns work with any Speckle data structure!

## Next Steps

<CardGroup cols={2}>
  <Card title="BIM Data Patterns" icon="building" href="/developers/sdks/python/guides/bim-data-patterns">
    Apply these traversal techniques to complex BIM data
  </Card>

  <Card title="Simple Data Patterns" icon="shapes" href="/developers/sdks/python/guides/simple-data-patterns">
    Working with custom and simple model data
  </Card>

  <Card title="Data Types" icon="table" href="/developers/sdks/python/concepts/data-types">
    Understanding the three types of data
  </Card>

  <Card title="API Reference" icon="code" href="/developers/sdks/python/api-reference/client">
    Client and operations API documentation
  </Card>
</CardGroup>
