When receiving data from BIM authoring tools (Revit, Rhino, ArchiCAD, etc.) via Speckle Connectors, you encounter rich, deeply nested structures with application-specific semantics.
Prerequisites: This guide builds on traversal techniques. If you’re new to navigating Speckle object graphs, start with Traversing Objects to learn the foundational patterns used throughout this guide.
Deep NestingBIM data often has nested structures with properties and sub-objects:
Copy
# v3 structure with properties dictionaryobj.properties["volume"] # Direct property accessobj.properties["materials"]["Steel"]["area"] # Nested material data# └─1st level──┘ └──2nd level──┘ └─3rd─┘
Understanding v3 Object TypesIn v3, there are no typed BIM classes like Wall or Column. Everything is a generic container:
Copy
# All BIM objects are DataObject instancesfrom specklepy.objects.data_objects import DataObjectprint(type(obj)) # <class 'DataObject'>print(obj.speckle_type) # "Objects.Data.DataObject"# BIM semantics come from the properties dictionaryprint(obj.name) # "Basic Wall - 200mm"print(obj.properties["category"]) # "Walls"print(obj.properties["family"]) # "Basic Wall"print(obj.properties["type"]) # "200mm"
Check properties, not types:
Copy
# Good - check properties for BIM semanticsif obj.properties.get("category") == "Walls": print(f"Found a wall: {obj.name}")# Don't check speckle_type - it's always "Objects.Data.DataObject"# Bad - won't work in v3if "Wall" in obj.speckle_type: # This won't match anything useful pass
Reference-Based ArchitectureLarge nested objects are stored separately:
Copy
# Display value is a reference, not the actual objectobj["@displayValue"] # Returns: "hash123abc..."# To get the actual mesh, it's fetched from the transport# This happens automatically during receive
from specklepy.objects import Basedef extract_revit_parameters(obj): """Extract all Revit parameters as a flat dictionary.""" params = {} # Get the parameters object if not hasattr(obj, "parameters"): return params parameters = obj.parameters if not isinstance(parameters, (dict, Base)): return params # Iterate through parameter categories param_dict = parameters if isinstance(parameters, dict) else parameters.__dict__ for category, category_params in param_dict.items(): if category.startswith("_"): continue # Get parameters in this category if isinstance(category_params, dict): param_source = category_params elif isinstance(category_params, Base): param_source = category_params.__dict__ else: continue for param_name, param_value in param_source.items(): if param_name.startswith("_"): continue # Extract value from parameter object if isinstance(param_value, dict): value = param_value.get("value") units = param_value.get("units") # Create compound key key = f"{category}.{param_name}" params[key] = { "value": value, "units": units, "category": category } elif isinstance(param_value, Base): value = getattr(param_value, "value", param_value) key = f"{category}.{param_name}" params[key] = {"value": value, "category": category} return params# Use itwall = operations.receive(wall_id, remote_transport=transport)params = extract_revit_parameters(wall)for key, data in params.items(): value = data["value"] units = data.get("units", "") print(f"{key}: {value} {units}")
Filtering by Parameter Value
Copy
def filter_by_parameter(objects, param_name, param_value): """Find all objects with a specific parameter value.""" results = [] for obj in objects: params = extract_revit_parameters(obj) # Check all parameter categories for key, data in params.items(): if param_name in key and data["value"] == param_value: results.append(obj) break return results# Find all walls with Function = "Exterior"exterior_walls = filter_by_parameter(walls, "Function", "Exterior")print(f"Found {len(exterior_walls)} exterior walls")
Converting Parameters to DataFrame
Copy
import pandas as pddef revit_objects_to_dataframe(objects): """Convert Revit objects with parameters to DataFrame.""" rows = [] for obj in objects: row = { "id": obj.id, "type": obj.speckle_type, "family": getattr(obj, "family", ""), "type_name": getattr(obj, "type", ""), } # Add all parameters as columns params = extract_revit_parameters(obj) for key, data in params.items(): # Flatten the key (remove category prefix if desired) column_name = key.split(".")[-1] # Just the parameter name row[column_name] = data["value"] rows.append(row) return pd.DataFrame(rows)# Use itwalls = [...] # Your Revit wallsdf = revit_objects_to_dataframe(walls)# 4. Analyze resultsprint("\nWall Analysis:")print(df[["name", "speckle_type", "volume", "area"]].head())print(f"\nTotal volume: {df['volume'].sum():.2f} m³")print(f"Average area: {df['area'].mean():.2f} m²")# 5. Material summary# Compute material summary (assuming 'material' column exists in df)material_summary = ( df.groupby("material") .agg(volume=("volume", "sum"), area=("area", "sum"), count=("material", "count")) .reset_index())print("\nMaterial Summary:")for _, row in material_summary.iterrows(): print(f"\n{row['material']}:") print(f" Volume: {row['volume']:.2f} m³") print(f" Area: {row['area']:.2f} m²") print(f" Used in: {row['count']} walls")## Pattern 2: DisplayValue ProxiesDisplay geometry is often detached for performance.<Note>**Deep Dive:** For a comprehensive understanding of display values, including how to create them, best practices, and cross-platform visualization strategies, see [Display Values](/developers/sdks/python/concepts/display-values).</Note><Note>**Proxified Display Values:** In many BIM models, display values are stored as **instance references** that need to be resolved using applicationId lookups. When `displayValue` contains references instead of direct meshes, see [Extracting Display Values and Transform Instances](/developers/sdks/python/guides/working-with-proxies) for the complete workflow on building applicationId indexes and resolving these references.</Note>**Understanding Display Values**BIM objects (like `DataObject`) have a `displayValue` property containing visualization geometry as a list of `Mesh` objects or other geometry.<Info>**Atomic Viewer Objects:** 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. When traversing objects, filtering by `displayValue` presence gives you all viewer-interactive elements.</Info>```python lines icon="python"# After receive, displayValue is the actual object (not a reference)wall = operations.receive(wall_id, remote_transport=transport)if hasattr(wall, "displayValue"): display = wall.displayValue # Can be a single mesh if hasattr(display, "vertices"): print(f"Single mesh: {display.vertices_count} vertices") # Or a list of meshes elif isinstance(display, list): print(f"Multiple meshes: {len(display)}") for mesh in display: print(f" - {mesh.vertices_count} vertices")
Extracting All Display Geometry
Copy
from specklepy.objects.geometry import Meshdef extract_display_meshes(obj): """Recursively extract all display meshes from an object.""" meshes = [] def traverse(current): if isinstance(current, Mesh): meshes.append(current) elif isinstance(current, Base): # Check for displayValue if hasattr(current, "displayValue"): display = current.displayValue if isinstance(display, list): for item in display: traverse(item) else: traverse(display) # Traverse other members for name in current.get_member_names(): if not name.startswith("_") and name != "displayValue": value = getattr(current, name, None) traverse(value) elif isinstance(current, list): for item in current: traverse(item) traverse(obj) return meshes# Use itall_meshes = extract_display_meshes(building)total_vertices = sum(m.vertices_count for m in all_meshes)print(f"Total display geometry: {len(all_meshes)} meshes, {total_vertices} vertices")
Finding Atomic Displayable ObjectsExtract all objects with displayValue - these are the atomic, viewer-selectable BIM elements:
Copy
from specklepy.objects import Basefrom specklepy.objects.data_objects import DataObjectdef 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 (atomic/leaf node) if hasattr(obj, "displayValue") and obj.displayValue is not None: displayable.append(obj) # Optimization: displayValue objects are typically leaf nodes # They rarely have children with displayValue, so we can stop here return # Container node - continue traversing via elements array if hasattr(obj, "elements") and isinstance(obj.elements, list): for element in obj.elements: traverse(element) # 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 BIM objectsatomic_objects = find_displayable_objects(building)print(f"Found {len(atomic_objects)} atomic/displayable objects")# Analyze by categoryfrom collections import defaultdictby_category = defaultdict(int)for obj in atomic_objects: if isinstance(obj, DataObject): category = obj.properties.get("category", "Other") by_category[category] += 1print("\nViewer-selectable objects by category:")for category, count in sorted(by_category.items()): print(f" {category}: {count}")
Computing Bounding Box from Display
Copy
from specklepy.objects.geometry import Pointdef compute_bounding_box(meshes): """Compute bounding box from a list of meshes.""" if not meshes: return None all_x, all_y, all_z = [], [], [] for mesh in meshes: # Extract all coordinates for i in range(0, len(mesh.vertices), 3): all_x.append(mesh.vertices[i]) all_y.append(mesh.vertices[i + 1]) all_z.append(mesh.vertices[i + 2]) if not all_x: return None bbox = { "min": Point(x=min(all_x), y=min(all_y), z=min(all_z)), "max": Point(x=max(all_x), y=max(all_y), z=max(all_z)), "center": Point( x=(min(all_x) + max(all_x)) / 2, y=(min(all_y) + max(all_y)) / 2, z=(min(all_z) + max(all_z)) / 2 ), "size": [ max(all_x) - min(all_x), max(all_y) - min(all_y), max(all_z) - min(all_z) ] } return bbox# Use itdisplay_meshes = extract_display_meshes(building)bbox = compute_bounding_box(display_meshes)print(f"Building bounds: {bbox['size']}")print(f"Center: ({bbox['center'].x}, {bbox['center'].y}, {bbox['center'].z})")
Rhino objects may include native binary data for Rhino-to-Rhino workflows.Understanding EncodedValue
Copy
# Rhino extrusion with encodedValue{ "speckle_type": "Objects.Geometry.Extrusion", "displayValue": [...], # Mesh for visualization "encodedValue": "base64_encoded_3dm_data...", # Native Rhino format}
Checking for EncodedValue
Copy
def has_native_rhino_data(obj): """Check if object has Rhino native data.""" return hasattr(obj, "encodedValue") and obj.encodedValue is not Nonedef extract_encoded_objects(root): """Find all objects with encodedValue.""" results = [] def traverse(obj): if isinstance(obj, Base): if has_native_rhino_data(obj): results.append({ "object": obj, "type": obj.speckle_type, "encoded_size": len(str(obj.encodedValue)) }) # 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 results# Use itencoded_objects = extract_encoded_objects(rhino_data)print(f"Found {len(encoded_objects)} objects with native Rhino data")
encodedValue is primarily for Rhino-to-Rhino workflows. For other platforms, use displayValue instead.
Complex models use instance/definition patterns for efficiency (like Revit families or Rhino blocks).
Instance Definitions Location: Instance definitions are stored in the instanceDefinitionProxies collection on the root object, not nested in the hierarchy. You need to build an applicationId index to resolve instance references, similar to how level proxies work (see Pattern 5).
Understanding InstancesInstance objects reference their definition by applicationId:
Copy
# Root object has instanceDefinitionProxiesroot.instanceDefinitionProxies = [ { "value": { # The actual definition with geometry "applicationId": "block_def_123", "speckle_type": "Objects.Geometry.Mesh", # ... geometry data } }]# Instance objects reference the definition{ "speckle_type": "Objects.Other.Instance", "applicationId": "block_def_123", # References the definition above "transform": [...], # Position/rotation/scale matrix}
Extracting Instances and Definitions
Copy
def extract_instance_definitions(root): """Extract instance definitions from instanceDefinitionProxies.""" definitions = {} # Get instanceDefinitionProxies from root instance_def_proxies = getattr(root, "instanceDefinitionProxies", []) for proxy in instance_def_proxies: if hasattr(proxy, "value"): definition = proxy.value app_id = getattr(definition, "applicationId", None) if app_id: definitions[app_id] = definition return definitionsdef find_instances(root): """Find all instance objects in the hierarchy.""" instances = [] def traverse(obj): if isinstance(obj, Base): if obj.speckle_type == "Objects.Other.Instance": instances.append(obj) # Traverse children for name in obj.get_member_names(): if name not in ["instanceDefinitionProxies"]: # Skip proxies value = getattr(obj, name, None) traverse(value) elif isinstance(obj, list): for item in obj: traverse(item) traverse(root) return instances# Use itdefinitions = extract_instance_definitions(root)instances = find_instances(root)print(f"Found {len(definitions)} unique definitions")print(f"Found {len(instances)} instances")# Match instances to definitionsfor instance in instances[:5]: # Show first 5 app_id = getattr(instance, "applicationId", None) if app_id and app_id in definitions: definition = definitions[app_id] print(f"Instance {instance.id} uses definition: {definition.speckle_type}")
Pattern 5: Level and Location Data with Proxy Collections
BIM models use proxy collections at the root level to organize objects by levels, colors, groups, and materials. Understanding this pattern is essential for working with BIM hierarchies.Key Insight: Proxies enable multiple overlapping organizational hierarchies. A single wall can simultaneously belong to:
A building level (architectural hierarchy)
A functional group (organizational grouping)
A layer (CAD hierarchy)
A material assignment (material hierarchy)
An instance type (blocks/families)
This models real-world CAD/BIM organization where groups, layers, levels, and blocks all coexist without forcing objects into a single parent-child hierarchy.
See Also: For a comprehensive explanation of proxification, why overlapping hierarchies matter, and intersection queries across multiple organizational systems, see Proxification.
The Proxy Pattern Workflow:
Proxies live at root level - Look for levelProxies, colorProxies, etc. on the root object
Proxies contain applicationId lists - Each proxy has an objects array of applicationId strings
Build an applicationId index - Map applicationIds to actual objects in the elements[] hierarchy
Resolve references - Match proxy applicationIds against your index to find actual objects
Reverse lookup for object level - To find an object’s level, search which proxy’s list contains its applicationId
This is a two-step lookup process, not direct nesting!
Understanding Proxy CollectionsProxy collections are stored as properties at the root object level:
levelProxies - Building levels/storeys organization
Key Concept: These are reference collections, not nested hierarchies. They contain lists of applicationId strings that point to objects in the elements[] hierarchy.LevelProxy Structure
Copy
from specklepy.objects.proxies import LevelProxy# Root object has levelProxies collectionroot = operations.receive(object_id, remote_transport=transport)# Access the levelProxies collectionlevel_proxies = getattr(root, "levelProxies", [])# Each LevelProxy has:for level_proxy in level_proxies: # 1. value: DataObject with level metadata print(level_proxy.value.name) # "Level 1" print(level_proxy.value.properties.get("elevation")) # 0.0 # 2. objects: List of applicationId strings print(level_proxy.objects) # ["guid-1", "guid-2", "guid-3"] # 3. applicationId: The level's own GUID print(level_proxy.applicationId) # "level-guid"
Finding LevelProxy Collections at Root
Copy
from specklepy.objects.proxies import LevelProxydef get_level_proxies(root): """ Extract level proxies from root object. These are stored at the root level. """ level_proxies = [] # Check for levelProxies property if hasattr(root, "levelProxies"): proxies = getattr(root, "levelProxies") if isinstance(proxies, list): level_proxies = [p for p in proxies if isinstance(p, LevelProxy)] return level_proxies# Get all levelslevel_proxies = get_level_proxies(root)for level in level_proxies: level_name = level.value.name object_count = len(level.objects) elevation = level.value.properties.get("elevation", 0) print(f"{level_name}: {object_count} objects at {elevation}m")
Resolving Objects by applicationIdThe challenge: LevelProxy contains applicationId strings, but you need to find the actual objects in the elements[] hierarchy.
Copy
from specklepy.objects import Basefrom specklepy.objects.data_objects import DataObjectdef build_applicationid_index(root): """ Build an index mapping applicationId to objects. This allows fast lookup when resolving proxy references. """ index = {} def traverse(obj): # Index any object with an applicationId if isinstance(obj, Base) and hasattr(obj, "applicationId"): if obj.applicationId: index[obj.applicationId] = obj # Traverse elements hierarchy if hasattr(obj, "elements") and isinstance(obj.elements, list): for element in obj.elements: traverse(element) # Traverse other properties if isinstance(obj, Base): for name in obj.get_member_names(): if not name.startswith("_") and name != "elements": value = getattr(obj, name, None) traverse(value) elif isinstance(obj, list): for item in obj: traverse(item) traverse(root) return index# Build the index onceapp_id_index = build_applicationid_index(root)print(f"Indexed {len(app_id_index)} objects by applicationId")# Now you can quickly resolve any applicationIdapp_id = "some-guid-123"if app_id in app_id_index: obj = app_id_index[app_id] print(f"Found: {obj.name}")
Grouping Objects by LevelCombine proxy collections with applicationId index to organize objects:
Copy
from collections import defaultdictdef group_objects_by_level(root): """ Group objects by their level using LevelProxy references. Returns a dictionary: {level_name: [list of objects]} """ # Step 1: Get level proxies from root level_proxies = get_level_proxies(root) # Step 2: Build applicationId index app_id_index = build_applicationid_index(root) # Step 3: Group objects by level by_level = defaultdict(list) for level_proxy in level_proxies: level_name = level_proxy.value.name # Resolve each applicationId to actual object for app_id in level_proxy.objects: if app_id in app_id_index: obj = app_id_index[app_id] by_level[level_name].append(obj) return dict(by_level)# Use itlevels = group_objects_by_level(root)print("\nObjects by Level:")for level_name, objects in levels.items(): print(f"\n{level_name}: {len(objects)} objects") # Analyze by category categories = defaultdict(int) for obj in objects: if isinstance(obj, DataObject): category = obj.properties.get("category", "Other") categories[category] += 1 for category, count in categories.items(): print(f" {category}: {count}")
Complete Proxy Pattern HelperA comprehensive helper for working with all proxy types:
Copy
from specklepy.objects.proxies import ( LevelProxy, ColorProxy, GroupProxy, RenderMaterialProxy)def extract_all_proxies(root): """ Extract all proxy collections from root object. Returns a dictionary of proxy type to list of proxies. """ proxies = { "levels": [], "colors": [], "groups": [], "materials": [] } # Level proxies if hasattr(root, "levelProxies"): level_proxies = getattr(root, "levelProxies") if isinstance(level_proxies, list): proxies["levels"] = [p for p in level_proxies if isinstance(p, LevelProxy)] # Color proxies if hasattr(root, "colorProxies"): color_proxies = getattr(root, "colorProxies") if isinstance(color_proxies, list): proxies["colors"] = [p for p in color_proxies if isinstance(p, ColorProxy)] # Group proxies if hasattr(root, "groupProxies"): group_proxies = getattr(root, "groupProxies") if isinstance(group_proxies, list): proxies["groups"] = [p for p in group_proxies if isinstance(p, GroupProxy)] # Render material proxies if hasattr(root, "renderMaterialProxies"): mat_proxies = getattr(root, "renderMaterialProxies") if isinstance(mat_proxies, list): proxies["materials"] = [p for p in mat_proxies if isinstance(p, RenderMaterialProxy)] return proxies# Use itproxies = extract_all_proxies(root)print(f"Levels: {len(proxies['levels'])}")print(f"Colors: {len(proxies['colors'])}")print(f"Groups: {len(proxies['groups'])}")print(f"Materials: {len(proxies['materials'])}")# List all levelsfor level_proxy in proxies["levels"]: level_name = level_proxy.value.name print(f" {level_name}: {len(level_proxy.objects)} objects")
Finding an Object’s LevelTo find which level an object belongs to, you need to reverse lookup from the proxy collections:
Copy
def find_object_level(root, target_obj): """ Find which level an object belongs to. Returns the level name or None if not found. """ # Get level proxies level_proxies = get_level_proxies(root) # Get the object's applicationId target_app_id = getattr(target_obj, "applicationId", None) if not target_app_id: return None # Search through level proxies for level_proxy in level_proxies: if target_app_id in level_proxy.objects: return level_proxy.value.name return None# Use itwall = walls[0] # Some wall objectlevel_name = find_object_level(root, wall)print(f"Wall is on: {level_name}")
Revit objects can contain detailed material quantity takeoffs stored in properties["Material Quantities"]. These quantities exist only on individual objects - there is no pre-aggregated total at the model level. To get project-wide material totals, you need to traverse all objects and aggregate their material quantities.Understanding Material QuantitiesEach object with materials contains a Material Quantities dictionary with material breakdowns:
Extracting Material Quantities from Individual Objects
Copy
def extract_material_quantities(obj): """Extract material quantities from a single object's properties.""" if not hasattr(obj, "properties") or not isinstance(obj.properties, dict): return {} return obj.properties.get("Material Quantities", {})# Use it on a single objectdesk = objects[0]mat_quantities = extract_material_quantities(desk)print(f"Materials in {desk.name}:")for material_name, mat_data in mat_quantities.items(): volume = mat_data.get("volume", {}).get("value", 0) units = mat_data.get("volume", {}).get("units", "m³") print(f" {material_name}: {volume} {units}")
Aggregating Model-Wide Material TotalsTo get total material quantities across the entire model version, traverse all objects and aggregate:
Copy
from specklepy.api import operationsfrom specklepy.transports.server import ServerTransportfrom specklepy.objects.graph_traversal.default_traversal import create_default_traversal_functiondef collect_all_objects_with_materials(root): """Traverse and collect all objects that have Material Quantities.""" objects_with_materials = [] traversal = create_default_traversal_function() for context in traversal.traverse(root): obj = context.current if hasattr(obj, "properties") and isinstance(obj.properties, dict): if "Material Quantities" in obj.properties: objects_with_materials.append(obj) return objects_with_materials# Receive the model versionroot = operations.receive(version_object_id, remote_transport=transport)# Collect all objects with materialsobjects_with_materials = collect_all_objects_with_materials(root)print(f"Found {len(objects_with_materials)} objects with material quantities")def aggregate_materials_by_type(objects): """Aggregate material quantities across multiple objects.""" material_totals = {} for obj in objects: mat_quantities = extract_material_quantities(obj) for material_name, mat_data in mat_quantities.items(): if material_name not in material_totals: material_totals[material_name] = { "total_area": 0, "total_volume": 0, "count": 0, "type": mat_data.get("materialType", "Unknown"), "category": mat_data.get("materialCategory", "Unknown"), "density": mat_data.get("density", {}).get("value", 0) } # Aggregate area if "area" in mat_data and "value" in mat_data["area"]: material_totals[material_name]["total_area"] += mat_data["area"]["value"] # Aggregate volume if "volume" in mat_data and "value" in mat_data["volume"]: material_totals[material_name]["total_volume"] += mat_data["volume"]["value"] material_totals[material_name]["count"] += 1 return material_totals# Use itall_objects = []# ... collect objects from traversalmaterials = aggregate_materials_by_type(all_objects)print("Material Takeoff Summary:")for material, data in sorted(materials.items()): print(f"{material} ({data['type']}):") print(f" Total Area: {data['total_area']:.2f} m²") print(f" Total Volume: {data['total_volume']:.4f} m³") print(f" Used in: {data['count']} elements") if data['density'] > 0: weight = data['total_volume'] * data['density'] print(f" Estimated Weight: {weight:.2f} kg")
Converting to DataFrame for Analysis
Copy
import pandas as pddef materials_to_dataframe(objects): """Convert material quantities to a detailed DataFrame.""" rows = [] for obj in objects: obj_name = getattr(obj, "name", "Unknown") obj_id = getattr(obj, "id", "") mat_quantities = extract_material_quantities(obj) for material_name, mat_data in mat_quantities.items(): row = { "object_name": obj_name, "object_id": obj_id, "material_name": material_name, "material_type": mat_data.get("materialType", ""), "material_category": mat_data.get("materialCategory", ""), "area_m2": mat_data.get("area", {}).get("value", 0), "volume_m3": mat_data.get("volume", {}).get("value", 0), "density_kg_m3": mat_data.get("density", {}).get("value", 0), } # Calculate weight if density is available if row["density_kg_m3"] > 0: row["weight_kg"] = row["volume_m3"] * row["density_kg_m3"] else: row["weight_kg"] = 0 rows.append(row) return pd.DataFrame(rows)# Use itdf = materials_to_dataframe(all_objects)# Analyze by material typeprint("Material Summary by Type:")summary = df.groupby("material_type").agg({ "area_m2": "sum", "volume_m3": "sum", "weight_kg": "sum", "object_id": "count"}).rename(columns={"object_id": "element_count"})print(summary)# Export for further analysisdf.to_csv("material_quantities.csv", index=False)
Always check if properties exist before accessing:
Copy
# Good - safe accessparams = getattr(obj, "parameters", {})instance_params = params.get("Instance Parameters", {})volume = instance_params.get("Volume", {}).get("value", 0)# Bad - will crash if structure differsvolume = obj.parameters["Instance Parameters"]["Volume"]["value"]
Handle both dict and Base for parameters
Parameters can be dict or Base objects:
Copy
def safe_get_params(obj): if not hasattr(obj, "parameters"): return {} params = obj.parameters if isinstance(params, dict): return params elif isinstance(params, Base): return params.__dict__ return {}
Use displayValue for visualization
Don’t rely on encodedValue for cross-platform work:
Copy
# Good - works everywhereif hasattr(obj, "displayValue"): meshes = obj.displayValue if isinstance(obj.displayValue, list) else [obj.displayValue] # Process meshes...# Bad - only works in Rhinoif hasattr(obj, "encodedValue"): # Can't use this in other platforms
Cache extracted data for performance
Don’t re-extract parameters repeatedly:
Copy
# Good - extract onceparams_cache = {obj.id: extract_revit_parameters(obj) for obj in objects}# Use cached datafor obj in objects: params = params_cache[obj.id] # Process...# Bad - extracts every timefor obj in objects: params = extract_revit_parameters(obj) # Slow!