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

# Finding and Extracting Data

> Learn to traverse, filter, and extract data from nested Speckle structures

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

* Understand [how to work with Speckle objects](/developers/sdks/python/guides/how-to-work-with-objects)
* Be familiar with [data traversal concepts](/developers/sdks/python/concepts/data-traversal)
* Know basic Python recursion

<Info>
  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.
</Info>

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

<Note>
  **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](/developers/sdks/python/concepts/proxification) and [BIM Data Patterns](/developers/sdks/python/guides/bim-data-patterns) for actual BIM data structures.
</Note>

**Basic manual traversal** - here's the fundamental pattern:

```python lines icon="python" expandable theme={null}
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:

<Accordion title="Understanding GraphTraversal Components">
  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.**
</Accordion>

```python lines icon="python" theme={null}
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:**

```python lines icon="python" theme={null}
# 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.

```python lines icon="python" theme={null}
# 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:

```python lines icon="python" theme={null}
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:

```python lines icon="python" theme={null}
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:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" theme={null}
# 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:**

```python lines icon="python" expandable theme={null}
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)
```

<Warning>
  **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:

  ```python theme={null}
  # ❌ 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)
  ```
</Warning>

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

```python lines icon="python" expandable theme={null}
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")
```

<Note>
  **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](/developers/sdks/python/guides/bim-data-patterns)
  for production patterns.
</Note>

**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:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" expandable theme={null}
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
```

<Warning>
  **Don't recurse into displayValue!** The displayValue property often contains the same geometry as the object, leading to duplicates:

  ```python theme={null}
  # ❌ 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
  ```
</Warning>

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

<Warning>
  **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:

  * [Proxification guide](/developers/sdks/python/concepts/proxification) - Understanding proxy structures
  * [BIM Data Patterns](/developers/sdks/python/guides/bim-data-patterns) - Working with real connector data
</Warning>

Track hierarchy levels during traversal:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" expandable theme={null}
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:

```python lines icon="python" expandable theme={null}
# 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

```python lines icon="python" expandable theme={null}
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

```python lines icon="python" expandable theme={null}
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

```python lines icon="python" expandable theme={null}
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:**

* [Data Traversal](/developers/sdks/python/concepts/data-traversal) - Deep dive into traversal patterns
* [Display Values](/developers/sdks/python/concepts/display-values) - Understanding geometry representation

**Guides:**

* [BIM Data Patterns](/developers/sdks/python/guides/bim-data-patterns) - Advanced BIM-specific patterns
* [Working with Geometry](/developers/sdks/python/guides/working-with-geometry) - Geometry manipulation
* [Understanding Speckle Mesh](/developers/sdks/python/guides/understanding-speckle-mesh) - Mesh structure details

**Next Steps:**

* [Advanced: Performance and Complex Patterns](/developers/sdks/python/guides/how-to-optimize-and-handle-complexity) - Learn optimization techniques

## 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](/developers/sdks/python/guides/how-to-optimize-and-handle-complexity) →
