Skip to main content

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.
Prerequisites: This guide assumes familiarity with the Base class and data types. If you’re new to Speckle objects, start with those concepts first.
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
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)

# Linear structure
project
├── name: "Building A"
├── date: "2024-01-15"
└── measurements: [Point, Point, Point]

Medium Graph (Simple Model)

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

Complex Graph (BIM Data)

# 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 TypeStructureBest StrategyWhy
Custom DataFlat or simpleDirect accessProperties are known
Geometry CollectionsMedium depthType-based traversalFind specific geometry types
BIM ModelsDeep hierarchyProperty/displayValue filteringBIM semantics in properties
Large ModelsVery deepelements[] traversalFollows 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

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.
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"
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.
The elements property is Speckle’s standardized convention for organizing hierarchical data. See The elements Convention for a detailed explanation of this pattern.
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
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.

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.
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:
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)
Find all objects matching criteria:
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
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
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.
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
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)
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
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
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.
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.
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
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.
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:
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:
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:
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:
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:
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

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

Use hasattr() before accessing:
# Good
if hasattr(obj, "displayValue"):
    mesh = obj.displayValue

# Bad - can crash
mesh = obj.displayValue
Don’t traverse _ prefixed properties:
# 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)
Properties can be lists or single objects:
# 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
Limit traversal scope when possible:
# 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"]

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