Every Speckle object inherits from Base. This is the foundation of Speckle’s object model - it provides identity, serialization, type checking, and dynamic properties.
Copy
from specklepy.objects import Basefrom specklepy.objects.geometry import Point, Line# Every geometry object is a Basepoint = Point(x=1.0, y=2.0, z=3.0)print(isinstance(point, Base)) # True# You can create plain Base objectsobj = Base()obj.name = "My Object"obj.value = 42
Base objects support both typed properties (defined in the class) and dynamic properties (added at runtime):
Copy
from specklepy.objects.geometry import Pointpoint = Point(x=1.0, y=2.0, z=3.0)# Static (typed) properties - defined in Point classprint(point.x) # 1.0print(point.y) # 2.0print(point.z) # 3.0# Dynamic properties - added at runtimepoint.color = "red"point.timestamp = "2024-01-15"point.metadata = {"source": "sensor", "accuracy": 0.01}# Get all property namesprint(point.get_member_names())# ['x', 'y', 'z', 'color', 'timestamp', 'metadata', ...]# Get only typed propertiesprint(point.get_typed_member_names())# ['x', 'y', 'z', 'units', ...]# Get only dynamic propertiesprint(point.get_dynamic_member_names())# ['color', 'timestamp', 'metadata']
Why dynamic properties? They let connectors attach application-specific data without defining custom classes. Revit can add parameters, Rhino can add userData, etc.
Base performs runtime type checking on typed properties:
Copy
from specklepy.objects.geometry import Pointpoint = Point(x=1.0, y=2.0, z=3.0)# This works - correct typepoint.x = 5.0# This works - int converts to floatpoint.x = 5# This fails - type checking prevents invalid typestry: point.x = "not a number"except Exception as e: print(f"Error: {e}") # Error: Cannot set 'Point.x': it expects type 'float', # but received type 'str'
Type checking only validates the top-level type. For List[Point], it checks if it’s a list but doesn’t validate each item’s type (for performance).
Every Base object has an id - a unique hash of its content:
Copy
from specklepy.objects.geometry import Pointpoint1 = Point(x=1.0, y=2.0, z=3.0)point2 = Point(x=1.0, y=2.0, z=3.0)point3 = Point(x=1.0, y=2.0, z=4.0)# Same content = same idprint(point1.get_id() == point2.get_id()) # True# Different content = different idprint(point1.get_id() == point3.get_id()) # False
get_id() serializes the entire object - expensive for large objects! The id property is set during send/receive operations, so use that when available.
from specklepy.objects import Baseobj = Base()# These failtry: obj[""] = "value" # Empty stringexcept ValueError as e: print(e) # "Invalid Name: Base member names cannot be empty strings"try: obj["@@invalid"] = "value" # Multiple @except ValueError as e: print(e) # "Invalid Name: Base member names cannot start with more than one '@'"try: obj["has.dot"] = "value" # Contains dotexcept ValueError as e: print(e) # "Invalid Name: Base member names cannot contain characters '.' or '/'"
Properties starting with @ (single) are valid and used for detached references like @displayValue.
from specklepy.objects import Basedef traverse_object(obj, depth=0): """Recursively traverse a Base object tree.""" indent = " " * depth if isinstance(obj, Base): print(f"{indent}{obj.speckle_type}") # Traverse all properties for name in obj.get_member_names(): if name.startswith("_"): continue value = getattr(obj, name, None) print(f"{indent} {name}:") traverse_object(value, depth + 2) elif isinstance(obj, list): print(f"{indent}[List with {len(obj)} items]") for item in obj[:3]: # Show first 3 traverse_object(item, depth + 1) elif isinstance(obj, dict): print(f"{indent}{{Dict with {len(obj)} keys}}") for key, value in list(obj.items())[:3]: # Show first 3 print(f"{indent} {key}:") traverse_object(value, depth + 2) else: # Primitive value print(f"{indent}{repr(obj)}")# Use itfrom specklepy.objects.geometry import Line, Pointline = Line( start=Point(x=0, y=0, z=0), end=Point(x=10, y=10, z=10))line.color = "red"line.thickness = 2.5traverse_object(line)
Large arrays can be chunked for efficient serialization:
Copy
from specklepy.objects import Baseobj = Base()obj.vertices = list(range(100000)) # Large list# Mark vertices as chunkable with chunk size 10000obj.add_chunkable_attrs(vertices=10000)# When sent, vertices will be split into 10 chunks
Large nested objects can be detached and stored separately:
Copy
from specklepy.objects import Basefrom specklepy.objects.geometry import Meshobj = Base()obj.displayValue = Mesh(...) # Large mesh# Mark displayValue as detachableobj.add_detachable_attrs({"displayValue"})# When sent, displayValue is stored separately and referenced by id
Connectors use detachment for displayValue meshes - keeps the main object lightweight while allowing on-demand mesh loading.
from specklepy.objects.geometry import Point# First sendpoint = Point(x=1, y=2, z=3)point.applicationId = "wall-column-base-001"# ... send to Speckle ...# Later update - same applicationIdpoint = Point(x=1.5, y=2, z=3) # Moved slightlypoint.applicationId = "wall-column-base-001" # Same ID# ... send again ...# Receiving applications can track that this is an update to the same object
Connectors use applicationId to map Speckle objects back to their native application objects (like Revit element IDs).
from specklepy.objects import Basefrom specklepy.objects.geometry import Point, Linedef get_objects_by_class(obj, target_class): """Recursively find all objects of a specific class (recommended approach).""" results = [] if isinstance(obj, target_class): results.append(obj) if isinstance(obj, Base): # Search in all properties for name in obj.get_member_names(): if name.startswith("_"): continue value = getattr(obj, name, None) results.extend(get_objects_by_class(value, target_class)) elif isinstance(obj, (list, tuple)): for item in obj: results.extend(get_objects_by_class(item, target_class)) elif isinstance(obj, dict): for value in obj.values(): results.extend(get_objects_by_class(value, target_class)) return results# Use itcollection = Base()collection.items = [ Point(x=1, y=2, z=3), Line(start=Point(x=0, y=0, z=0), end=Point(x=1, y=1, z=1)), Point(x=4, y=5, z=6),]# Recommended: Use isinstance checkspoints = get_objects_by_class(collection, Point)lines = get_objects_by_class(collection, Line)print(f"Found {len(points)} points") # Found 4 points (including nested in Line)
For BIM Data: In v3, most BIM objects are DataObject instances. To differentiate walls from columns, filter by properties (e.g., properties.category == "Walls") rather than by type. See Data Traversal for property-based filtering patterns.
Define class properties with types for validation and documentation:
Copy
# Good - typed propertyclass Wall(Base): height: float# Less good - dynamic propertywall = Base()wall.height = 3.0 # No type checking
Avoid expensive operations in loops
Don’t call get_id() repeatedly:
Copy
# Bad - serializes every timefor obj in objects: if obj.get_id() in seen: continue# Good - use id property or track differentlyfor obj in objects: if obj.id in seen: # Set during send/receive continue
Use applicationId for tracking
Always set applicationId when creating objects from your application:
Copy
from specklepy.objects.geometry import Point# Link to your app's native objectpoint = Point(x=1, y=2, z=3)point.applicationId = f"myapp-{native_object.id}"
Handle unknown types gracefully
When traversing received objects, expect unknown types:
Copy
def process_object(obj): if isinstance(obj, Base): # Check speckle_type string instead of isinstance if obj.speckle_type.startswith("Objects.Geometry"): process_geometry(obj) elif hasattr(obj, "displayValue"): process_display_value(obj.displayValue)