Skip to main content

What You’ll Learn

By the end of this guide, you’ll understand:
  • ✅ How to recursively traverse nested object structures
  • ✅ How to filter objects by property values while traversing
  • ✅ How to extract geometry from BIM objects
  • ✅ How to handle hierarchical collections and preserve context

Prerequisites

Before starting this guide, you should:
This guide builds on traversal fundamentals from the Core Concepts. We focus on practical patterns for finding and extracting data from real-world Speckle projects.

How do I traverse nested Speckle data?

Speckle data is often deeply nested - buildings contain levels, levels contain rooms, rooms contain elements. You need to visit every object in the tree to process or analyze them.
Terminology Note: When we use terms like “levels”, “rooms”, or reference property names in examples, these are for illustrative purposes. Real BIM data from connectors (Revit, Rhino, etc.) uses proxy structures - for example, Revit levels are represented as LevelProxy objects in dedicated collections, not as direct hierarchy. See the Proxification guide and BIM Data Patterns for actual BIM data structures.
Basic manual traversal - here’s the fundamental pattern:
from specklepy.objects import Base

def traverse(obj):
    """Visit every object in the tree."""
    if not isinstance(obj, Base):
        return

    # Process current object
    name = getattr(obj, "name", "unnamed")
    print(f"Visiting: {name}")

    # Recurse through all properties
    for prop_name in obj.get_member_names():
        value = getattr(obj, prop_name, None)

        if isinstance(value, Base):
            # Single nested object
            traverse(value)
        elif isinstance(value, list):
            # List of objects
            for item in value:
                if isinstance(item, Base):
                    traverse(item)

# Use it
traverse(root)
SDK’s built-in traversal utility ⭐ - SpecklePy provides GraphTraversal for robust traversal:
The SDK provides three key components for traversal:
  1. GraphTraversal - The main traversal engine
    • Creates the traversal instance: GraphTraversal([rules])
    • Executes traversal: .traverse(root) returns an iterator
    • Pass [] for default behavior (traverse everything)
  2. TraversalContext - Information about each visited object
    • .current - The current Base object being visited
    • .member_name - Property name from parent (e.g., “elements”, “displayValue”)
    • .parent - Parent TraversalContext (or None if root)
  3. TraversalRule - Optional rules to control behavior
    • _conditions - When does this rule apply? (list of predicates)
    • _members_to_traverse - What properties to traverse? (function returning list)
    • _should_return_to_output - Include objects in results? (boolean)
In the examples below, look for comments showing where each component is used.
from specklepy.objects.graph_traversal.traversal import GraphTraversal

# GraphTraversal: Create traversal engine with
# default rules (traverse everything)
traversal = GraphTraversal([])

# Traverse and visit every object
# Returns Iterator[TraversalContext]
for context in traversal.traverse(root):
    # TraversalContext: current Base object
    obj = context.current
    name = getattr(obj, "name", "unnamed")
    print(f"Visiting: {name}")
Understanding the traversal flow:
# Visual Flow:
#
#   GraphTraversal([])  ←  You create the engine
#          ↓
#   .traverse(root)     ←  Call traverse on root object
#          ↓
#   Iterator[TraversalContext]  ←  Returns iterator of contexts
#          ↓
#   for context in ...:  ←  Loop through each context
#       context.current     ←  Access the Base object
#       context.parent      ←  Access parent context (optional)
#       context.member_name ←  Access property name (optional)
Why use GraphTraversal?
  • ✅ Handles all edge cases (dicts, lists, nested Base objects)
  • ✅ Provides context (parent object, property name)
  • ✅ Supports custom rules for filtering during traversal
  • ✅ Memory efficient (uses iterators, not lists)
  • ✅ Battle-tested in production SDK code
The traversal pattern has three parts:
  1. Base case: Check if object is a Base instance
  2. Process: Do something with the current object
  3. Recurse: Visit all child objects via get_member_names()
Why get_member_names()? It returns all property names on the object, already filters out private members (_) and methods, and works with both typed and dynamic properties.
# get_member_names() handles the filtering
for name in obj.get_member_names():
    value = getattr(obj, name, None)
    # Value is always a data property, never a method
Collecting objects during traversal using GraphTraversal: Instead of just visiting objects, you often want to collect them into a list. Here are two approaches - collecting all at once or processing as you go:
from specklepy.objects.graph_traversal.traversal import GraphTraversal

traversal = GraphTraversal([])

# Collect all objects
all_objects = [
    context.current
    for context in traversal.traverse(root)
]
print(f"Found {len(all_objects)} objects")

# Or process while traversing (memory efficient)
for context in traversal.traverse(root):
    obj = context.current
    if hasattr(obj, "properties"):
        category = obj.properties.get("category")
        if category:
            obj_name = getattr(obj, 'name', 'unnamed')
            print(f"Found {category}: {obj_name}")
Understanding TraversalContext: Each context provides information about where you are in the object tree - the current object, what property it came from, and its parent:
from specklepy.objects.graph_traversal.traversal import GraphTraversal

traversal = GraphTraversal([])

for context in traversal.traverse(root):
    # TraversalContext provides three properties:
    obj = context.current           # Current object
    prop_name = context.member_name # Property name
    parent_context = context.parent # Parent context

    if parent_context:
        parent_obj = parent_context.current
        print(f"{obj.name} is in {parent_obj.name}.{prop_name}")
Using TraversalRule to control traversal: Rules give you fine-grained control over what gets traversed and returned. They’re useful when you want to skip certain properties or limit results during traversal:
from specklepy.objects.graph_traversal.traversal import (
    GraphTraversal,
    TraversalRule  # Controls what gets traversed
)

# Define function to skip displayValue property
def get_members_without_display(obj):
    return [
        name for name in obj.get_member_names()
        if name != "displayValue"
    ]

# TraversalRule: Create rule with three parameters
skip_display_rule = TraversalRule(
    # When: applies to all objects
    _conditions=[lambda obj: True],
    # What: which properties to traverse
    _members_to_traverse=get_members_without_display,
    # Whether: include in results
    _should_return_to_output=True
)

# Use the rule in GraphTraversal
traversal = GraphTraversal([skip_display_rule])

for context in traversal.traverse(root):
    obj = context.current
    print(f"Visiting: {getattr(obj, 'name', 'unnamed')}")
    # displayValue won't be traversed due to our rule
Simpler approach - filter after traversal: If you just need to skip certain objects, filtering after traversal is often clearer than writing a custom rule:
# Simpler approach: Filter after traversal instead of using rules
traversal = GraphTraversal([])

for context in traversal.traverse(root):
    obj = context.current

    # Skip if this came from displayValue property
    if context.member_name == "displayValue":
        continue

    print(f"Visiting: {getattr(obj, 'name', 'unnamed')}")
Comprehensive example - all three components together:
from specklepy.objects.graph_traversal.traversal import (
    GraphTraversal,
    TraversalRule,
    TraversalContext
)

# Define a custom rule to skip geometry properties
def get_non_geometry_members(obj):
    """Skip geometry properties to focus on structure."""
    skip_props = [
        "displayValue",
        "renderMaterial",
        "@displayValue"
    ]
    return [
        name for name in obj.get_member_names()
        if name not in skip_props
    ]

structure_only_rule = TraversalRule(
    _conditions=[lambda obj: True],
    _members_to_traverse=get_non_geometry_members,
    _should_return_to_output=True
)

# Create traversal with the custom rule
traversal = GraphTraversal([structure_only_rule])

# Traverse and collect context information
results = []
for context in traversal.traverse(root):
    obj = context.current

    # Get parent information
    parent_name = "root"
    if context.parent:
        parent_obj = context.parent.current
        parent_name = getattr(parent_obj, "name", "unnamed")

    came_from = context.member_name or "root"

    results.append({
        "object": obj,
        "name": getattr(obj, "name", "unnamed"),
        "parent": parent_name,
        "property": came_from
    })

# Display results showing the traversal path
for item in results[:5]:  # Show first 5
    parent = item['parent']
    prop = item['property']
    print(f"{item['name']} (from {parent}.{prop})")
Output example:
Building A (from root.root)
Level 1 (from Building A.elements)
Room 101 (from Level 1.elements)
Wall-001 (from Room 101.elements)
Don’t process the same property twice! When handling elements arrays specifically, make sure you don’t also process them in the general get_member_names() loop:
# ❌ Bad - processes elements twice
def traverse(obj):
    # First time: explicit elements handling
    if hasattr(obj, "elements"):
        for element in obj.elements:
            traverse(element)

    # Second time: get_member_names includes "elements"!
    for name in obj.get_member_names():
        value = getattr(obj, name)
        traverse(value)

# ✅ Good - process each property once
def traverse(obj):
    for name in obj.get_member_names():
        value = getattr(obj, name, None)
        if isinstance(value, Base):
            traverse(value)
        elif isinstance(value, list):
            for item in value:
                if isinstance(item, Base):
                    traverse(item)

How do I find specific objects?

You need to find all objects matching certain criteria - for example, all walls, all objects on a specific level, or all elements with a particular property value. Filter while traversing by checking conditions and collecting matches:
from specklepy.objects.graph_traversal.traversal import GraphTraversal

def find_by_category(obj, category_name):
    """Find all objects with matching category."""
    traversal = GraphTraversal([])
    results = []

    for context in traversal.traverse(obj):
        current = context.current

        has_props = (hasattr(current, "properties")
                     and current.properties)
        if has_props:
            cat = current.properties.get("category")
            if cat == category_name:
                results.append(current)

    return results

# Find all walls
walls = find_by_category(root, "Walls")
print(f"Found {len(walls)} walls")
About “category” property: When we reference properties["category"] in examples, this demonstrates the pattern. Real BIM data may organize categories differently - Revit data, for instance, uses both properties["category"] on individual objects AND category-based proxy collections. See BIM Data Patterns for production patterns.
Multiple filter criteria: When you need to match objects on several properties at once (e.g., walls that are also concrete), pass multiple key-value pairs to check all conditions:
from specklepy.objects.graph_traversal.traversal import GraphTraversal

def find_by_properties(obj, **criteria):
    """Find objects matching multiple property criteria.

    Example:
        find_by_properties(
            root,
            category="Walls",
            material="Concrete"
        )
    """
    traversal = GraphTraversal([])
    results = []

    for context in traversal.traverse(obj):
        current = context.current

        # Check if all criteria match
        has_props = (hasattr(current, "properties")
                     and current.properties)
        if has_props:
            matches = all(
                current.properties.get(key) == value
                for key, value in criteria.items()
            )
            if matches:
                results.append(current)

    return results

# Find concrete walls
concrete_walls = find_by_properties(
    root,
    category="Walls",
    material="Concrete"
)
Using custom filter functions: For complex filtering logic (numeric comparisons, nested properties, combined conditions), pass a custom function that returns True for objects you want to keep:
from specklepy.objects.graph_traversal.traversal import GraphTraversal

def find_by_filter(obj, filter_func):
    """Find objects matching custom filter function.

    Args:
        obj: Root object to search
        filter_func: Function that takes an object
                     and returns True/False
    """
    traversal = GraphTraversal([])

    results = [
        context.current
        for context in traversal.traverse(obj)
        if filter_func(context.current)
    ]

    return results

# Find large walls (area > 10 m²)
def is_large_wall(obj):
    if not hasattr(obj, "properties"):
        return False
    is_wall = obj.properties.get("category") == "Walls"
    is_large = obj.properties.get("area", 0) > 10
    return is_wall and is_large

large_walls = find_by_filter(root, is_large_wall)

# Find load-bearing elements
def is_load_bearing(obj):
    if not hasattr(obj, "properties"):
        return False
    return obj.properties.get("isLoadBearing", False)

load_bearing = find_by_filter(root, is_load_bearing)
The filtering pattern: (1) Traverse the entire tree recursively, (2) Check each object against your criteria, (3) Collect matching objects in a results list, (4) Return the accumulated results. This pattern works for any filtering criteria - category, property values, object types, etc.

How do I extract geometry from BIM objects?

BIM objects from connectors (Revit, Rhino, etc.) contain geometry in the displayValue property. You need to extract these meshes for visualization or analysis. Check for displayValue and collect geometry objects:
from specklepy.objects.geometry import Mesh
from specklepy.objects.graph_traversal.traversal import GraphTraversal

def extract_display_meshes(obj):
    """Extract all displayValue meshes from object tree."""
    traversal = GraphTraversal([])
    meshes = []

    for context in traversal.traverse(obj):
        current = context.current

        # Check for displayValue
        if hasattr(current, "displayValue"):
            display = current.displayValue

            # displayValue can be a single mesh or a list
            if isinstance(display, Mesh):
                meshes.append(display)
            elif isinstance(display, list):
                # Filter list for Mesh objects
                meshes.extend([d for d in display if isinstance(d, Mesh)])

    return meshes

# Extract all meshes
meshes = extract_display_meshes(root)
print(f"Found {len(meshes)} meshes")
Extracting geometry with metadata: Often you need to know which object each mesh came from. Use TraversalContext to track source objects and their properties:
from specklepy.objects.geometry import Mesh
from specklepy.objects.graph_traversal.traversal import GraphTraversal

def extract_meshes_with_metadata(obj):
    """Extract meshes with source object metadata."""
    traversal = GraphTraversal([])
    results = []

    for context in traversal.traverse(obj):
        current = context.current

        # Extract displayValue
        if hasattr(current, "displayValue"):
            display = current.displayValue

            meshes = []
            if isinstance(display, Mesh):
                meshes = [display]
            elif isinstance(display, list):
                meshes = [
                    d for d in display
                    if isinstance(d, Mesh)
                ]

            # Add metadata for each mesh
            for mesh in meshes:
                results.append({
                    "mesh": mesh,
                    "source_name": getattr(
                        current, "name", "unnamed"
                    ),
                    "category": (
                        current.properties.get("category")
                        if hasattr(current, "properties")
                        else None
                    ),
                    "properties": (
                        current.properties
                        if hasattr(current, "properties")
                        else {}
                    ),
                    "parent": context.parent.current if context.parent else None
                })

    return results

# Extract with context
mesh_data = extract_meshes_with_metadata(root)

for item in mesh_data:
    print(f"Mesh from: {item['source_name']}")
    print(f"  Category: {item['category']}")
    vert_count = len(item['mesh'].vertices) / 3
    print(f"  Vertices: {vert_count}")
Handling different geometry types: Not all geometry is meshes - you might also encounter points, lines, and polylines. Check for all geometry types you’re interested in:
from specklepy.objects.geometry import (
    Mesh, Point, Line, Polyline
)

def extract_all_geometry(obj):
    """Extract all geometry types from displayValue."""
    geometry = []

    if not isinstance(obj, Base):
        return geometry

    # Check for displayValue
    if hasattr(obj, "displayValue"):
        display = obj.displayValue

        # Handle single object
        geom_types = (Mesh, Point, Line, Polyline)
        if isinstance(display, geom_types):
            geometry.append(display)
        # Handle list
        elif isinstance(display, list):
            geometry.extend([
                d for d in display
                if isinstance(d, geom_types)
            ])

    # Check if object itself is geometry
    if isinstance(obj, geom_types):
        geometry.append(obj)

    # Recurse
    for name in obj.get_member_names():
        if name != "displayValue":
            value = getattr(obj, name, None)

            if isinstance(value, Base):
                geometry.extend(extract_all_geometry(value))
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, Base):
                        geometry.extend(extract_all_geometry(item))

    return geometry
Don’t recurse into displayValue! The displayValue property often contains the same geometry as the object, leading to duplicates:
# ❌ Bad - processes displayValue twice
for name in obj.get_member_names():  # includes "displayValue"
    value = getattr(obj, name)
    if isinstance(value, Mesh):
        meshes.append(value)  # Adds mesh
    if isinstance(value, Base):
        extract_meshes(value)  # Adds same mesh again!

# ✅ Good - skip displayValue in recursion
if name != "displayValue":
    # Only recurse into non-geometry properties

How do I work with hierarchical collections?

BIM data often has hierarchical structures: Building → Levels → Rooms → Elements. You need to process these hierarchies while maintaining context about where each object came from.
Real BIM structures use proxies! When working with actual connector data (Revit, Rhino, ArchiCAD), “Levels” aren’t nested hierarchies - they’re represented as LevelProxy collections that reference objects by ID. The examples here show conceptual hierarchies for learning. For production code with real BIM data, see:
Track hierarchy levels during traversal:
from specklepy.objects.graph_traversal.traversal import GraphTraversal

def process_hierarchy(obj):
    """Process hierarchical structure with context information."""
    traversal = GraphTraversal([])

    for context in traversal.traverse(obj):
        current = context.current

        # Calculate depth from parent chain
        depth = 0
        parent_ctx = context.parent
        while parent_ctx:
            depth += 1
            parent_ctx = parent_ctx.parent

        indent = "  " * depth
        name = getattr(current, "name", "unnamed")
        category = current.properties.get("category", "") if hasattr(current, "properties") else ""

        # Print with indentation showing hierarchy
        print(f"{indent}{name} ({category})")

# Use it
process_hierarchy(root)
Output:
Building A (Buildings)
  Level 1 (Levels)
    Room 101 (Rooms)
      Wall W-101 (Walls)
      Wall W-102 (Walls)
    Room 102 (Rooms)
  Level 2 (Levels)
Collecting objects by level: To analyze your data by depth in the tree (e.g., root objects vs. deeply nested objects), organize objects by their hierarchy level:
def collect_by_hierarchy_level(obj):
    """Collect objects organized by hierarchy level."""
    levels = {}

    def traverse(current_obj, level=0):
        if not isinstance(current_obj, Base):
            return

        # Add to level dictionary
        if level not in levels:
            levels[level] = []
        levels[level].append(current_obj)

        # Recurse
        for name in current_obj.get_member_names():
            value = getattr(current_obj, name, None)

            if isinstance(value, Base):
                traverse(value, level + 1)
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, Base):
                        traverse(item, level + 1)

    traverse(obj)
    return levels

# Collect by level
by_level = collect_by_hierarchy_level(root)

for level, objects in sorted(by_level.items()):
    print(f"Level {level}: {len(objects)} objects")
Flattening vs. preserving hierarchy: Choose between flattening (all objects in one list) or preserving structure (nested dictionaries). Flatten when you just need to process objects; preserve when hierarchy matters:
# Flatten to a simple list:
def flatten_hierarchy(obj):
    """Flatten entire hierarchy to a single list."""
    results = []

    def collect(current_obj):
        if not isinstance(current_obj, Base):
            return

        results.append(current_obj)

        for name in current_obj.get_member_names():
            value = getattr(current_obj, name, None)

            if isinstance(value, Base):
                collect(value)
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, Base):
                        collect(item)

    collect(obj)
    return results

# Get flat list
all_objects = flatten_hierarchy(root)

# Preserve hierarchy as nested structure:
def preserve_hierarchy(obj):
    """Convert to nested dictionary preserving hierarchy."""
    if not isinstance(obj, Base):
        return None

    result = {
        "name": getattr(obj, "name", "unnamed"),
        "properties": obj.properties if hasattr(obj, "properties") else {},
        "children": []
    }

    # Collect children
    if hasattr(obj, "elements"):
        for element in obj.elements:
            if isinstance(element, Base):
                child = preserve_hierarchy(element)
                if child:
                    result["children"].append(child)

    return result

# Get hierarchical structure
hierarchy = preserve_hierarchy(root)

Practical Examples

Example 1: Find All Walls on a Specific Level

def find_walls_on_level(root, level_name):
    """Find all walls on a specific level."""
    walls = []
    current_level = None

    def traverse(obj):
        nonlocal current_level

        if not isinstance(obj, Base):
            return

        # Track when we enter/exit levels
        has_props = hasattr(obj, "properties")
        is_level = (has_props and
                   obj.properties.get("category") == "Levels")

        if is_level:
            obj_name = getattr(obj, "name", "")
            if obj_name == level_name:
                current_level = obj_name

        # Collect walls when in target level
        if current_level == level_name:
            has_props = hasattr(obj, "properties")
            is_wall = (has_props and
                      obj.properties.get("category") == "Walls")
            if is_wall:
                walls.append(obj)

        # Recurse
        for name in obj.get_member_names():
            value = getattr(obj, name, None)

            if isinstance(value, Base):
                traverse(value)
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, Base):
                        traverse(item)

        # Reset level when exiting
        if is_level and obj_name == level_name:
            current_level = None

    traverse(root)
    return walls

# Find walls on Level 2
level_2_walls = find_walls_on_level(root, "Level 2")
print(f"Found {len(level_2_walls)} walls on Level 2")

Example 2: Extract Geometry by Category

from specklepy.objects.geometry import Mesh

def extract_geometry_by_category(root, category):
    """Extract all meshes for objects in a category."""
    results = []

    def traverse(obj):
        if not isinstance(obj, Base):
            return

        # Check if object is in target category
        has_props = hasattr(obj, "properties")
        is_match = (has_props and
                   obj.properties.get("category") == category)

        # Extract geometry if category matches
        if is_match and hasattr(obj, "displayValue"):
            display = obj.displayValue
            obj_name = getattr(obj, "name", "unnamed")

            meshes = []
            if isinstance(display, Mesh):
                meshes = [display]
            elif isinstance(display, list):
                meshes = [d for d in display if isinstance(d, Mesh)]

            for mesh in meshes:
                results.append({
                    "name": obj_name,
                    "mesh": mesh,
                    "properties": obj.properties
                })

        # Recurse (skip displayValue)
        for name in obj.get_member_names():
            if name != "displayValue":
                value = getattr(obj, name, None)

                if isinstance(value, Base):
                    traverse(value)
                elif isinstance(value, list):
                    for item in value:
                        if isinstance(item, Base):
                            traverse(item)

    traverse(root)
    return results

# Extract all wall meshes
wall_meshes = extract_geometry_by_category(root, "Walls")

for item in wall_meshes:
    print(f"Wall: {item['name']}")
    print(f"  Material: {item['properties'].get('material', 'Unknown')}")
    print(f"  Vertices: {len(item['mesh'].vertices) / 3}")

Example 3: Build a Category Summary Report

from collections import defaultdict
from specklepy.objects.geometry import Mesh

def generate_category_report(root):
    """Generate detailed report by category."""

    # Collect data by category
    by_category = defaultdict(lambda: {
        "count": 0,
        "objects": [],
        "total_area": 0.0,
        "mesh_count": 0
    })

    def traverse(obj):
        if not isinstance(obj, Base):
            return

        # Get category
        category = "Unknown"
        has_props = (hasattr(obj, "properties")
                     and obj.properties)
        if has_props:
            category = obj.properties.get("category", "Unknown")

        # Update statistics
        by_category[category]["count"] += 1
        by_category[category]["objects"].append(obj)

        # Add area if present
        if hasattr(obj, "properties"):
            area = obj.properties.get("area", 0.0)
            by_category[category]["total_area"] += area

        # Count meshes
        if hasattr(obj, "displayValue"):
            display = obj.displayValue
            if isinstance(display, Mesh):
                by_category[category]["mesh_count"] += 1
            elif isinstance(display, list):
                mesh_count = sum(
                    1 for d in display
                    if isinstance(d, Mesh)
                )
                by_category[category]["mesh_count"] += mesh_count

        # Recurse
        for name in obj.get_member_names():
            value = getattr(obj, name, None)

            if isinstance(value, Base):
                traverse(value)
            elif isinstance(value, list):
                for item in value:
                    if isinstance(item, Base):
                        traverse(item)

    traverse(root)

    # Generate report
    print("=" * 60)
    print("CATEGORY SUMMARY REPORT")
    print("=" * 60)

    for category in sorted(by_category.keys()):
        data = by_category[category]
        print(f"\n{category}:")
        print(f"  Count: {data['count']}")
        print(f"  Meshes: {data['mesh_count']}")
        if data['total_area'] > 0:
            print(f"  Total Area: {data['total_area']:.2f} m²")

    return dict(by_category)

# Generate report
report = generate_category_report(root)

Learn More

Core Concepts: Guides: Next Steps:

Next Steps

Now that you can find and extract data, you’re ready to:
  1. Optimize for performance - Build indexes for large datasets
  2. Handle complexity - Work with detached objects and references
  3. Extract Revit parameters - Access nested BIM metadata efficiently
Continue to Advanced: Performance and Complex Patterns