Simple python COM server to expose scipy integration to VBA I want to create a COM server to expose scipy to VBA code, as performing integration is kinda difficult and I don't know of any good VBA/COM libraries for this. My objectives are:

As portable as possible
Fast
Maintainable

Here's the python:
# Requires C:\Users\...\anaconda3\Library\bin on the PATH
from scipy import integrate
from win32com.client import Dispatch
from typing import Callable
import comtypes


class ScipyWrapper:
    # pythoncom.CreateGuid()
    _reg_clsid_ = &quot;{A621AB64-9378-4B54-A7D3-20711C0B72DB}&quot;
    _reg_desc_ = &quot;Python Scipy COM Server&quot;
    _reg_progid_ = &quot;PythonCOM.ScipyWrapper&quot;
    _reg_clsctx_ = comtypes.CLSCTX_LOCAL_SERVER # don't want inproc because conda not supported on 64 bit 
                                                # see https://stackoverflow.com/q/67462529/6609896

    _public_methods_ = [&quot;quad&quot;,]
    # _public_attrs_ = []
    # _readonly_attrs_ = []

    def quad(self, vbFunc, a: float, b: float) -&gt; float:
        vb_dispatch = Dispatch(vbFunc)
        func: Callable[[float], float] = vb_dispatch.Evaluate  # Callable[..., float]?
        return integrate.quad(func, a, b)[0]  # only return val, not uncertainty


if __name__ == &quot;__main__&quot;:
    import win32com.server.register

    win32com.server.register.UseCommandLine(ScipyWrapper)


Called from VBA (twinBasic) like:
Module Main
    Public Sub doStuff()
        Dim integrator as Object = CreateObject(&quot;PythonCOM.ScipyWrapper&quot;)
        Dim func as IOneDimensionalFunction = New XSquared 'this is what we'll integrate
        Debug.Print integrator.quad(func, 0., 1.) 'integrate x^2 from 0-&gt;1, returns 0.333333...
    End Sub
End Module

'@Description(&quot;Interface for a f(x) -&gt; y function, with optional parameters&quot;)
Class IOneDimensionalFunction
    Public Function Evaluate(ByVal x As Double, ParamArray parameters() as Variant) As Double
    End Function
End Class

Class XSquared
    Implements IOneDimensionalFunction
    Private Function IOneDimensionalFunction_Evaluate(ByVal x As Double, ParamArray parameters() as Variant) As Double
        Return x^2
    End Function    
End Class

Questions:

Not sure how to Type the python, vbFunc is a PyIDispatch instance but not sure what that means for typing/mypy.
I'd like to use strong typing to protect my python, right now it's duck typed and I could easily pass an Interface without the Evaluate method, or anything else for that matter.
No idea how to handle python errors and turn them into COM errors.
Is this ref-count safe?

OFC any other tips on general style, use of comments and naming etc. are welcome :)
Deferred Imports
Why is import win32com.server.register only imported during __main__? It should be inexpensive and clean to just leave it at the top with everything else. I'd also de-namespace from win32com.server.register import UseCommandLine but that's mostly a matter of taste.
Stubbing
You ask:

vbFunc is a PyIDispatch instance but not sure what that means for typing/mypy

If PyIDispatch is an importable type, then simply
def quad(self, vbFunc: PyIDispatch, a: float, b: float) -&gt; float:

If it's not importable at all (this can happen for some libraries that are aggressively dynamic), then you basically have two options:

Declare an opaque, named NewType to make it clear to callers that this is not just &quot;anything&quot;
Do the work that the library should have done for you, and stub it yourself using a custom .pyi

Call protocols
# Callable[..., float]?

You're right to be confused, and mypy is still fairly terrible at annotating variadic callables. Here you should consider using Protocol:
class DispatchCall(typing.Protocol):
    def __call__(self, x: float, *parameters: COMCompatibleTypes) -&gt; float:
        ... # literally an ellipsis

# Later:

func: DispatchCall = vb_dispatch.Evaluate

Unpacking
Re.
return integrate.quad(func, a, b)[0]  # only return val, not uncertainty

it's worth noting that uncertainty is not the only thing quad returns; there's actually y, abserr, infodict, message and explain.
It's minor, but I typically prefer to do at least a partial unpack to make it a little more clear what's happening:
y, *_ = integrate.quad(func, a, b)

Given the tuple length I don't think I'd do this, but you can if you think it adds clarity:
y, abserr, infodict, message, explain = integrate.quad(func, a, b)