Skip to content

COM API Pitfalls for LLM Agents

This page documents hard-won lessons from debugging the SolidWorks COM bridge. If you are an AI coding assistant (Claude, GPT-4, Gemini, Copilot, etc.) working on this codebase, read this page before touching any COM-related code. Every entry below caused a real runtime failure that required hours to diagnose.

For LLM agents

The patterns here are not obvious from the SolidWorks API docs alone. They are the delta between "what the documentation says" and "what actually works" under pywin32 late-binding on SW 2025/2026.


1. SelectByID2Callout must be VT_DISPATCH null, not None

Symptom: (-2147352571, 'Type mismatch.', None, 8) when calling SelectByID2.

Root cause: The Callout parameter (8th argument, type VT_DISPATCH) expects a COM null pointer, not a Python None. Python None marshals as VT_NULL which SolidWorks rejects with DISP_E_TYPEMISMATCH.

Fix:

import pythoncom
import win32com.client as _win32com

null_callout = _win32com.VARIANT(pythoncom.VT_DISPATCH, None)
model.Extension.SelectByID2(
    "", "EDGE", x, y, z,
    append, mark,
    null_callout,   # <- NOT plain None
    0,
)

Applies to: Every call to SelectByID2 or SelectByID that doesn't need a real callout object. The same issue occurs for FACE, EDGE, VERTEX, and other entity types.


2. InsertFeatureChamfer lives on IFeatureManager, not IModelDocExtension

Symptom: <unknown>.InsertFeatureChamfer error when calling through model.Extension.

Root cause: InsertFeatureChamfer (DISPID 83) is a method of IFeatureManager, not IModelDocExtension. Routing it through model.Extension causes DISP_E_MEMBERNOTFOUND.

Fix:

import math

fm = model.FeatureManager          # IFeatureManager (property access, no parens)
feature = fm.InsertFeatureChamfer(
    1,                             # Options
    1,                             # ChamferType = swChamferEqualDistance
    distance_m,                    # Width in metres
    math.pi / 4,                   # Angle (45 degrees)
    0.0,                           # OtherDist (unused for equal-distance)
    0.0, 0.0, 0.0,                 # VertexChamDist1, 2, 3 (unused)
)

Applies to: Any chamfer feature creation. Do not use model.Extension.InsertFeatureChamfer.


3. ForceRebuild3 must run before coordinate-based edge/face selection

Symptom: SelectByID2("", "EDGE", x, y, z, ...) returns False even when the coordinate is geometrically on an edge.

Root cause: After creating a feature (revolve, cut, extrude), the new edges are not tessellated until the model is explicitly rebuilt. SelectByID2 uses the tessellated mesh to find nearby entities; without it, the edge does not exist in the selection index.

Fix: Call ForceRebuild3(True) before the first coordinate-based selection in any feature operation:

model.ForceRebuild3(True)   # True = top-level only (faster); False = deep rebuild
model.Extension.SelectByID2("", "EDGE", x, y, z, False, 0, null_callout, 0)

Only needed once per feature-creation sequence, before the first SelectByID2 call.


4. GetTessellation is on IFace2, not on IEdge

Symptom: <source>.GetTessellation error when iterating body.GetEdges() and calling edge.GetTessellation(tol).

Root cause: GetTessellation is a method of IFace2 (face objects), not IEdge. Edge objects do not have a tessellation method accessible via late binding.

What to use instead for edge sampling: IEdge.GetCurveParams2 (property, no parens) returns [t0, t1, ...]; then use the ICurve returned by IEdge.GetCurve and call curve.Evaluate2(t, 0) to sample points.

params = edge.GetCurveParams2    # property — no ()
curve  = edge.GetCurve           # property — no ()
t0, t1 = float(params[0]), float(params[1])
pt = curve.Evaluate2((t0 + t1) / 2, 0)   # midpoint

But prefer SelectByID2 with coordinates over body traversal — it is faster and more reliable.


5. Zero-arg COM methods accessed without () in late binding

Symptom: TypeError: 'str' object is not callable or TypeError: 'tuple' object is not callable.

Root cause: Under pywin32 late binding, zero-argument COM methods are returned as their result value (property-style), not as callable objects. Calling () on the result tries to invoke the returned value (a string, tuple, etc.) as a function.

Examples:

# WRONG
title = model.GetTitle()     # TypeError: 'str' is not callable
params = edge.GetCurveParams2()  # TypeError: 'tuple' is not callable

# CORRECT
title = model.GetTitle       # returns the string directly
params = edge.GetCurveParams2    # returns the tuple directly

Rule: Any SW method with no parameters must be accessed without (). The sw_type_info.flag_methods system handles which names are treated as methods vs properties. Check gen_py (IEdge, IModelDoc2, etc.) to see whether a name is a def (method) or in _prop_map_get_ (property).


6. FeatureChamfer(Width, Angle, Flip) returns int, not IFeature

Symptom: Checking if not feature: fails even on success; feature.Name crashes.

Root cause: IModelDoc2.FeatureChamfer(Width, Angle, Flip) (DISPID 65583) returns VT_I4 (an integer): 1 on success, 0 on failure. It does not return an IFeature. Code that treats the return value as a COM object will fail.

Fix: Use IFeatureManager.InsertFeatureChamfer (see pitfall #2) which returns a proper IFeature. If you must use the IModelDoc2 variant as a fallback, check the int:

result_int = model.FeatureChamfer(width_m, math.pi / 4, False)
if not result_int:
    raise Exception("FeatureChamfer returned 0 (failure)")
# result_int is now 1 (truthy), not an IFeature

7. IModelDoc2.FeatureFillet3 on SW 2025+ returns int, not IFeature

Symptom: feature.Name raises AttributeError: int object has no attribute Name.

Root cause: Starting with SW 2025 (major version ≥ 33), IModelDoc2.FeatureFillet3 returns VT_I4 (1 = success, 0 = failure), not an IFeature dispatch object. The older IFeatureManager.FeatureFillet3 still returns IFeature on older builds.

Fix: Branch on SW major version:

rev = adapter.swApp.RevisionNumber
major = int(str(rev).split(".")[0])

if major >= 33:
    result_code = model.FeatureFillet3(radius_m, True, 0, False, 0, 0, None, False, False)
    if not result_code:
        raise Exception("FeatureFillet3 returned 0")
    # Feature exists but IFeature reference is not available; name defaults to "Fillet"
else:
    feature = model.FeatureManager.FeatureFillet3(radius_m, 0, 0, 0, 0, ...)
    if not feature:
        raise Exception("FeatureFillet3 returned None")
    name = feature.Name

8. The <unknown>.<Method> error pattern

Symptom: Error message like <unknown>.InsertFeatureChamfer or GetEdges.GetTessellation.

Root cause: This is pywin32's error format when IDispatch.GetIdsOfNames returns DISP_E_MEMBERNOTFOUND for a method name. It means you are calling a method on the wrong COM interface — the COM object does not know that method name.

The format: <ProgId>.<MethodName> where <ProgId> is how pywin32 identified the object (<unknown> if it has no registered ProgId, or a method name if the object was returned from a call).

How to diagnose: 1. Check the gen_py file for which class owns the method (search for def MethodName). 2. Confirm the class name printed near the def is the interface you are calling through. 3. If they differ, navigate through the correct property chain to reach the right interface.


9. InsertRefPlane for face sketches is more reliable than SelectByID2 FACE

Symptom: Sketch placed via SelectByID2 FACE fails after a parametric cut operation because the face topology name has changed and the new face isn't found.

Fix: Create an offset reference plane from a named plane (e.g. Top Plane) and open a sketch on that instead:

top_plane = model.FeatureByName("Top Plane") or model.FeatureByName("Planta")
top_plane.Select2(False, 0)
# swRefPlaneReferenceConstraints_Distance = 8
offset_feat = model.FeatureManager.InsertRefPlane(8, offset_m, 0, 0.0, 0, 0.0)
offset_feat.Select2(False, 0)
sketch = model.SketchManager.InsertSketch(True)

See build_yoke_female_artifact.py for a full working implementation.


10. ThroughAll vs ThroughAllBoth for mid-plane sketches

Symptom: A cut extruded from a mid-plane sketch (e.g. Top plane at Y=0) only cuts in one direction, leaving half the material untouched.

Root cause: swEndCondThroughAll (value 1) cuts only in Direction 1 from the sketch plane. When the sketch is at Y=0 and the body extends from Y=-10 to Y=+10, Direction 1 goes to Y=+10 but Direction 2 is not cut.

Fix: Use swEndCondThroughAllBoth (value 9) or pass both_directions=True:

await adapter.create_cut_extrude(
    ExtrusionParameters(end_condition="ThroughAllBoth")
)

11. GetType() resolves as a property, not a method, on a freshly-fetched ActiveDoc

Symptom: TypeError: 'int' object is not callable when calling document.GetType() — but the identical call on a document just returned by OpenDoc6/NewDocument works fine. If this exception is caught by a broad except Exception: return default (e.g. this codebase's _attempt helper), the failure is silent: the caller just sees doc_type == 0 and takes the wrong branch (e.g. treating an Assembly as unrecognized).

Root cause: Late-bound COM dispatch resolves an unknown member name speculatively — sometimes as a callable method wrapper, sometimes by eagerly invoking it and returning the value (property semantics). Which one you get depends on whether the specific dispatch object has been flagged for its real interface via sw_type_info.flag_doc/flag_methods (_FlagAsMethod). A document object returned directly by OpenDoc6 in this codebase's adapter is flagged immediately after open. A document re-fetched via swApp.ActiveDoc in a later call (e.g. a resync-before-traversal step) is a new, unflagged dispatch wrapping the same underlying document — GetType on it resolves as a property, and calling it with () tries to call the returned int.

Fix: Never call a zero-arg accessor on a document/feature/component with bare parentheses unless you know it has just been flagged. Route through the existing _get_attr_or_call(obj, "MethodName") helper, which handles both property and method resolution:

doc_type = adapter._attempt(
    lambda: int(adapter._get_attr_or_call(document, "GetType") or 0), default=0
)

Found and fixed 2026-08-11 while verifying assembly-aware list_features (_FeatureSelectionService.list_features in pywin32_adapter.py) against a live SolidWorks session — see openspec/changes/assembly-aware-list-features/.

12. IComponent2.GetModelDoc2 raises "Member not found" unless IComponent2 is flagged first

Symptom: pywintypes.com_error: (-2147352573, 'Member not found.', None, None) when calling component.GetModelDoc2() on an IComponent2 object returned by IAssemblyDoc.GetComponents, even though getattr(component, "GetModelDoc2", None) returns something that looks callable.

Root cause: Same late-binding ambiguity as #11 and #5, but manifesting as a COM error instead of a Python TypeError: dynamic dispatch returns a speculative callable wrapper for an attribute name it hasn't resolved a real DISPID for yet, and that wrapper only fails once actually invoked. IComponent2 is never flagged anywhere else in the traversal — components come from GetComponents, not from flag_doc (which only knows about document-level interfaces: IPartDoc/IAssemblyDoc/IDrawingDoc).

Fix: Flag the component for IComponent2 before calling any of its methods:

sw_type_info.flag_methods(component, "IComponent2")
resolved_doc = adapter._get_attr_or_call(component, "GetModelDoc2")

Found and fixed 2026-08-11 alongside #11, in the same live-verification pass. Before this fix, every component in a real assembly resolved as UnresolvedComponent — the traversal logic itself was correct (confirmed by unit tests against mock COM fakes), but nothing had ever exercised it against a real, unflagged IComponent2 dispatch until then. A reminder that mock-adapter tests validate the shape of a fix, not COM binding behavior itself.


Reference: Where to look things up

Question Where to look
Which class owns a method? gen_py/3.13/83A33D31-*x0x34x0.py — search def MethodName and note the class above it
Is a name a method or property? In gen_py class: def Name(...) = method; "Name": (...) in _prop_map_get_ = property
What DISPID does a method have? InvokeTypes(DISPID, ...) line in the gen_py method body
Does a method return IFeature? Check return type: (9, 0) = VT_DISPATCH (object); (24, 0) = VT_I4 (integer)
Full COM threading rules See "COM threading architecture" in CLAUDE.md
Runbook for live debugging See "Troubleshooting Runbook" section in CLAUDE.md