You need to create custom data structures or add metadata to geometry for your Speckle project. Use the Base class - it’s the foundation of all Speckle objects and supports dynamic properties:
Copy
from specklepy.objects import Base# Create a base objectobj = Base()# Add direct attributesobj.name = "My Wall"obj.elementId = "wall-001"obj.height = 3.5obj.isLoadBearing = True# Add nested structureobj.elements = [] # Will hold child objectsprint(f"Created: {obj.name}")
Base objects are Python classes that accept any property name dynamically (no predefined schema needed), serialize automatically for Speckle, support nesting (objects within objects), and work with Python’s native attribute access.When to create Base objects:
Building custom data structures (surveys, analysis results, schedules)
Grouping related objects together
Adding application-specific metadata
Don’t use reserved names: Avoid property names starting with _ or matching Base methods like id, speckle_type, get_member_names(). These have special meanings.
Copy
# ❌ Bad - overwrites internal propertyobj.id = "custom-123" # This sets the Speckle hash ID# ✅ Good - use your own namingobj.elementId = "custom-123"
How do I add metadata: direct attributes vs. properties dictionary?
You need to attach metadata to objects, but you’re unsure whether to use direct attributes (obj.name) or the properties dictionary (obj.properties["name"]).Use direct attributes for structure and organization:
Copy
from specklepy.objects import Base# Direct attributes define YOUR data structurebuilding = Base()building.name = "Building A"building.address = "123 Main St"building.levels = [] # Your hierarchybuilding.elements = [] # Your organization# Add a levellevel = Base()level.name = "Level 1"level.elevation = 0.0level.elements = [] # Objects on this levelbuilding.levels.append(level)
Use the properties dictionary for queryable metadata:
Copy
from specklepy.objects import Base# Properties dict for metadata that other apps should findwall = Base()wall.name = "W-101"# Standard BIM metadatawall.properties = { "category": "Walls", "material": "Concrete", "thickness": 200, "fireRating": "2 hour", "isLoadBearing": True, "cost": 1500.00}# Access safely with defaultsmaterial = wall.properties.get("material", "Unknown")thickness = wall.properties.get("thickness", 0)
Direct attributes (obj.name) are part of your object’s structure, provide Python-level property access, and are good for relationships and hierarchies like obj.levels, obj.elements, obj.children.Properties dictionary (obj.properties) is a standardized metadata container, follows BIM connector conventions, makes data searchable across applications, and commonly uses keys like category, family, type, material.
Which to use? Both are valid Speckle patterns! The properties dictionary is a convention that makes metadata more discoverable. If Revit sends a wall, its category will be in properties["category"] - following this pattern makes your data easier to work with across platforms.
Combining both approaches:
Copy
from specklepy.objects import Basefrom specklepy.objects.geometry import Point, Mesh# Create a beam with both structure and metadatabeam = Base()# Direct attributes - your structurebeam.name = "B-205"beam.startPoint = Point(x=0, y=0, z=3.5)beam.endPoint = Point(x=5, y=0, z=3.5)beam.displayValue = mesh # Geometry reference# Properties dict - queryable metadatabeam.properties = { "category": "Structural Framing", "family": "Steel Beam", "type": "W12x26", "material": "Steel", "length": 5.0, "weight": 130.0}
You have a Speckle object and need to read its properties, but you don’t know what properties it has or how to access them safely.For direct attributes:
Copy
# Direct access (if you know the property exists)name = obj.nameheight = obj.height# Safe access with hasattr()if hasattr(obj, "name"): print(f"Name: {obj.name}")else: print("No name property")# Safe access with getattr() and defaultname = getattr(obj, "name", "Unnamed")height = getattr(obj, "height", 0.0)
For properties dictionary:
Copy
# Direct access (might raise KeyError)category = obj.properties["category"] # KeyError if missing!# ✅ Safe access with .get() and defaultcategory = obj.properties.get("category", "Unknown")material = obj.properties.get("material", "Not specified")# Check if key existsif "category" in obj.properties: print(f"Category: {obj.properties['category']}")
Discovering all properties - use get_member_names() to discover what properties an object has:
Copy
from specklepy.objects import Baseobj = Base()obj.name = "Widget"obj.value = 42obj.tags = ["important", "new"]# Get all property namesproperty_names = obj.get_member_names()print(property_names)# ['name', 'value', 'tags', 'id', 'speckle_type', ...]# Loop through all propertiesfor prop_name in obj.get_member_names(): value = getattr(obj, prop_name, None) print(f"{prop_name}: {value}")
What does get_member_names() return?
get_member_names() returns all property names on the object, automatically filtering out:
Private members (starting with _)
Methods and functions
Class attributes
This means you get a clean list of just the data properties. No need to check if not name.startswith("_") - it’s already filtered!
Copy
# get_member_names() already excludes private membersfor name in obj.get_member_names(): value = getattr(obj, name, None) # Clean, no filtering needed
Safe property access pattern:
Copy
def safely_read_object(obj): """Read all properties from an object safely.""" # Read known direct attributes with defaults name = getattr(obj, "name", "Unnamed") # Read properties dictionary safely properties = {} if hasattr(obj, "properties") and obj.properties: category = obj.properties.get("category", "Unknown") material = obj.properties.get("material", "Not specified") properties = { "category": category, "material": material } return { "name": name, "properties": properties }# Use itinfo = safely_read_object(wall)print(f"Wall: {info['name']}, Category: {info['properties']['category']}")
Don’t assume properties exist! Speckle objects can come from different sources with different schemas. Always use safe access patterns:
Copy
# ❌ Bad - will crash if property doesn't existcategory = obj.properties["category"]name = obj.name# ✅ Good - handles missing properties gracefullycategory = obj.properties.get("category", "Unknown")name = getattr(obj, "name", "Unnamed")
You need to work with multiple objects - filtering them, counting them, or processing them in bulk.Looping through lists:
Copy
from specklepy.objects import Base# Create a collection of objectsbuilding = Base()building.elements = []for i in range(5): element = Base() element.name = f"Element-{i}" element.properties = {"category": "Walls" if i % 2 == 0 else "Floors"} building.elements.append(element)# Loop through elementsfor element in building.elements: name = getattr(element, "name", "unnamed") category = element.properties.get("category", "Unknown") print(f"{name}: {category}")
Filtering lists:
Copy
# Filter with list comprehensionwalls = [ obj for obj in building.elements if obj.properties.get("category") == "Walls"]print(f"Found {len(walls)} walls")# Filter with multiple criteriaload_bearing_walls = [ obj for obj in building.elements if obj.properties.get("category") == "Walls" and obj.properties.get("isLoadBearing", False)]
Counting and aggregating:
Copy
# Count by categoryfrom collections import Countercategories = Counter( obj.properties.get("category", "Unknown") for obj in building.elements)print(categories)# Counter({'Walls': 3, 'Floors': 2})# Sum numeric propertiestotal_area = sum( obj.properties.get("area", 0.0) for obj in building.elements)print(f"Total area: {total_area}")
Working with nested collections - many Speckle objects use elements arrays for hierarchical structures:
Terminology Note: Examples here use terms like “building”, “level”, “room” to illustrate hierarchical concepts. Real BIM data from connectors (Revit, Rhino, etc.) uses proxy structures rather than direct nesting - for example, Revit levels are LevelProxy objects that reference elements by ID. See Proxification and BIM Data Patterns for actual BIM structures.
Copy
from specklepy.objects import Base# Create hierarchical structure (illustrative example)building = Base()building.name = "Building A"building.elements = []# Add levelsfor level_num in range(3): level = Base() level.name = f"Level {level_num}" level.properties = {"category": "Levels", "elevation": level_num * 3.5} level.elements = [] # Objects on this level # Add rooms to this level for room_num in range(4): room = Base() room.name = f"Room {level_num}{room_num}" room.properties = {"category": "Rooms", "area": 25.0} level.elements.append(room) building.elements.append(level)# Process hierarchyfor level in building.elements: level_name = getattr(level, "name", "unnamed") room_count = len(level.elements) if hasattr(level, "elements") else 0 print(f"{level_name}: {room_count} rooms")
Common collection patterns:
Copy
# Check if object has elementsif hasattr(obj, "elements") and obj.elements: print(f"Object has {len(obj.elements)} elements") for element in obj.elements: process(element)# Safe iteration (handles missing elements)elements = getattr(obj, "elements", [])for element in elements: process(element)# Find first matchdef find_first(objects, category): """Find first object with matching category.""" for obj in objects: if obj.properties.get("category") == category: return obj return Nonewall = find_first(building.elements, "Walls")
Check if elements exists before looping:
Copy
# ❌ Bad - crashes if no elements propertyfor element in obj.elements: process(element)# ✅ Good - safe check firstif hasattr(obj, "elements"): for element in obj.elements: process(element)# ✅ Also good - use getattr with defaultfor element in getattr(obj, "elements", []): process(element)
from collections import Counterdef filter_by_property(objects, key, value): """Filter objects by property key-value pair.""" return [ obj for obj in objects if obj.properties.get(key) == value ]def summarize_objects(objects): """Create summary statistics for objects.""" if not objects: return "No objects to summarize" # Count by category categories = Counter( obj.properties.get("category", "Unknown") for obj in objects ) # Calculate total area (if present) total_area = sum( obj.properties.get("area", 0.0) for obj in objects ) return { "count": len(objects), "categories": dict(categories), "total_area": total_area }# Use the functionselements = building.elementswalls = filter_by_property(elements, "category", "Walls")summary = summarize_objects(walls)print(f"Found {summary['count']} walls")print(f"Categories: {summary['categories']}")print(f"Total area: {summary['total_area']} m²")