Repository: caketop/python-starlark-go (language: python)

Issue:
Can't tell what starlark Builtin cannot be converted to Python
I can't tell which property of the final output of my Starlark script is not convertible.

```
$ python3 test.py test.star
Traceback (most recent call last):
  File "/Users/Z003XC4/Source/Target/TargetOSS/python-poetry/test.py", line 12, in <module>
    print(s.eval("main([])"))
starlark_go.errors.ConversionError: Don't know how to convert *starlark.Builtin to Python value

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/Z003XC4/Source/Target/TargetOSS/python-poetry/test.py", line 14, in <module>
    print(e.backtrace)
AttributeError: 'ConversionError' object has no attribute 'backtrace'
```

Test code:

```python
import sys

from starlark_go import Starlark
from starlark_go.errors import EvalError, ConversionError

s = Starlark()

with open(sys.argv[1], 'r') as input_file:
    s.exec(input_file.read())

try:
    print(s.eval("main([])"))
except (EvalError, ConversionError) as e:
    print(e.backtrace)
```

The test code is working with simple dictionaries so it might be something else fancier I'm (unknowingly) doing that's not resolving to something convertible.


Discussion / hints:
Can you share the Starlark that triggers this as well?
It looks like you're trying to return a reference to a function somehow? A [Builtin](https://pkg.go.dev/go.starlark.net@v0.0.0-20230122040757-066229b0515d/starlark#Builtin) is a Go function that is callable from Starlark. I'd like for this module to support returning Callables back to Python but it doesn't at this time.
I introduce a `print()` of the final output and tracked it down to basically this:

```
"slack": {"needs": <built-in method keys of dict value>, 
```

and this is the minimum reproduction:

```starlark
def stage_slack_notify(stage_names):
    return {"name": "slack", "needs": stage_names}

def main(ctx):
    build_stages = {"something": [], "something_else": []}

    stages = [build_stages] + [stage_slack_notify(build_stages.keys)]
    final = {
        'version': '1',
        'stages': stages,
    }

    print(final)

    return final
```

Can you spot the error?

`Dict.keys()` is a function and I forgot `()`. 🙃 PEBKAC.

But I still feel like there's a better way to communicate the error, perhaps by telling exactly what wasn't able to be converted, with some path to it if possible.
Keeping track of the path to the value would be doable but maybe kinda ugly. I think we'd have to add a `path` parameter to every function in [starlark_to_python.go](https://github.com/caketop/python-starlark-go/blob/main/starlark_to_python.go) and have `starlark[Dict|Tuple|Set|List]ToPython` append keys to it before passing it along to the next function.

Fix patch:
diff --git a/python_exceptions.go b/python_exceptions.go
index ab6adc4..0c963cc 100644
--- a/python_exceptions.go
+++ b/python_exceptions.go
@@ -20,6 +20,39 @@ import (
 	"go.starlark.net/syntax"
 )
 
+func getCurrentPythonException() (*C.PyObject, *C.PyObject, *C.PyObject) {
+	var ptype *C.PyObject = nil
+	var pvalue *C.PyObject = nil
+	var ptraceback *C.PyObject = nil
+
+	C.PyErr_Fetch(&ptype, &pvalue, &ptraceback)
+	C.PyErr_NormalizeException(&ptype, &pvalue, &ptraceback)
+	if ptraceback != nil {
+		C.PyException_SetTraceback(pvalue, ptraceback)
+		C.Py_DecRef(ptraceback)
+	}
+
+	return ptype, pvalue, ptraceback
+}
+
+func setPythonExceptionCause(cause *C.PyObject) {
+	ptype, pvalue, ptraceback := getCurrentPythonException()
+	C.PyException_SetCause(pvalue, cause)
+	C.PyErr_Restore(ptype, pvalue, ptraceback)
+}
+
+func handleConversionError(err error, pytype *C.PyObject) {
+	_, pvalue, _ := getCurrentPythonException()
+
+	if pvalue != nil {
+		pvalue = C.cgoPy_NewRef(pvalue)
+		defer setPythonExceptionCause(pvalue)
+	}
+	errmsg := C.CString(err.Error())
+	defer C.free(unsafe.Pointer(errmsg))
+	C.PyErr_SetString(pytype, errmsg)
+}
+
 func raisePythonException(err error) {
 	var (
 		exc_args   *C.PyObject
diff --git a/python_globals.go b/python_globals.go
index 2211ef9..ee784b2 100644
--- a/python_globals.go
+++ b/python_globals.go
@@ -2,8 +2,6 @@ package main
 
 /*
 #include "starlark.h"
-
-extern PyObject *ConversionError;
 */
 import "C"
 
diff --git a/python_to_starlark.go b/python_to_starlark.go
index 60ec47a..51e7541 100644
--- a/python_to_starlark.go
+++ b/python_to_starlark.go
@@ -3,14 +3,13 @@ package main
 /*
 #include "starlark.h"
 
-extern PyObject *ConversionError;
+extern PyObject *ConversionToStarlarkFailed;
 */
 import "C"
 
 import (
 	"fmt"
 	"math/big"
-	"unsafe"
 
 	"go.starlark.net/starlark"
 )
@@ -19,19 +18,25 @@ func pythonToStarlarkTuple(obj *C.PyObject) (starlark.Tuple, error) {
 	var elems []starlark.Value
 	pyiter := C.PyObject_GetIter(obj)
 	if pyiter == nil {
-		return starlark.Tuple{}, fmt.Errorf("List: couldn't get iterator")
+		return starlark.Tuple{}, fmt.Errorf("Couldn't get iterator for Python tuple")
 	}
 	defer C.Py_DecRef(pyiter)
 
+	index := 0
 	for pyvalue := C.PyIter_Next(pyiter); pyvalue != nil; pyvalue = C.PyIter_Next(pyiter) {
 		defer C.Py_DecRef(pyvalue)
 
-		value, err := pythonToStarlarkValue(pyvalue)
+		value, err := innerPythonToStarlarkValue(pyvalue)
 		if err != nil {
-			return starlark.Tuple{}, err
+			return starlark.Tuple{}, fmt.Errorf("While converting value at index %v in Python tuple: %v", index, err)
 		}
 
 		elems = append(elems, value)
+		index += 1
+	}
+
+	if C.PyErr_Occurred() != nil {
+		return starlark.Tuple{}, fmt.Errorf("Python exception while converting value at index %v in Python tuple", index)
 	}
 
 	return starlark.Tuple(elems), nil
@@ -40,7 +45,7 @@ func pythonToStarlarkTuple(obj *C.PyObject) (starlark.Tuple, error) {
 func pythonToStarlarkBytes(obj *C.PyObject) (starlark.Bytes, error) {
 	cbytes := C.PyBytes_AsString(obj)
 	if cbytes == nil {
-		return starlark.Bytes(""), fmt.Errorf("Bytes: couldn't get pointer")
+		return starlark.Bytes(""), fmt.Errorf("Couldn't get pointer to Python bytes")
 	}
 
 	return starlark.Bytes(C.GoString(cbytes)), nil
@@ -49,99 +54,112 @@ func pythonToStarlarkBytes(obj *C.PyObject) (starlark.Bytes, error) {
 func pythonToStarlarkList(obj *C.PyObject) (*starlark.List, error) {
 	len := C.PyObject_Length(obj)
 	if len < 0 {
-		return &starlark.List{}, fmt.Errorf("List: couldn't get length of object")
+		return &starlark.List{}, fmt.Errorf("Couldn't get size of Python list")
 	}
 
 	var elems []starlark.Value
 	pyiter := C.PyObject_GetIter(obj)
 	if pyiter == nil {
-		return &starlark.List{}, fmt.Errorf("List: couldn't get iterator")
+		return &starlark.List{}, fmt.Errorf("Couldn't get iterator for Python list")
 	}
 	defer C.Py_DecRef(pyiter)
 
+	index := 0
 	for pyvalue := C.PyIter_Next(pyiter); pyvalue != nil; pyvalue = C.PyIter_Next(pyiter) {
 		defer C.Py_DecRef(pyvalue)
-
-		value, err := pythonToStarlarkValue(pyvalue)
+		value, err := innerPythonToStarlarkValue(pyvalue)
 		if err != nil {
-			return &starlark.List{}, err
+			return &starlark.List{}, fmt.Errorf("While converting value at index %v in Python list: %v", index, err)
 		}
 
 		elems = append(elems, value)
+		index += 1
+	}
+
+	if C.PyErr_Occurred() != nil {
+		return &starlark.List{}, fmt.Errorf("Python exception while converting value at index %v in Python list", index)
 	}
 
 	return starlark.NewList(elems), nil
 }
 
 func pythonToStarlarkDict(obj *C.PyObject) (*starlark.Dict, error) {
-	len := C.PyObject_Length(obj)
-	if len < 0 {
-		return &starlark.Dict{}, fmt.Errorf("Dict: couldn't get length of object")
+	size := C.PyObject_Length(obj)
+	if size < 0 {
+		return &starlark.Dict{}, fmt.Errorf("Couldn't get size of Python dict")
 	}
 
-	dict := starlark.NewDict(int(len))
+	dict := starlark.NewDict(int(size))
 	pyiter := C.PyObject_GetIter(obj)
 	if pyiter == nil {
-		return &starlark.Dict{}, fmt.Errorf("Dict: couldn't get iterator")
+		return &starlark.Dict{}, fmt.Errorf("Couldn't get iterator for Python dict")
 	}
 	defer C.Py_DecRef(pyiter)
 
 	for pykey := C.PyIter_Next(pyiter); pykey != nil; pykey = C.PyIter_Next(pyiter) {
 		defer C.Py_DecRef(pykey)
 
+		key, err := innerPythonToStarlarkValue(pykey)
+		if err != nil {
+			return &starlark.Dict{}, fmt.Errorf("While converting key in Python dict: %v", err)
+		}
+
 		pyvalue := C.PyObject_GetItem(obj, pykey)
 		if pyvalue == nil {
-			return &starlark.Dict{}, fmt.Errorf("Dict: couldn't get value")
+			return &starlark.Dict{}, fmt.Errorf("Couldn't get value of key %v in Python dict", key)
 		}
 		defer C.Py_DecRef(pyvalue)
 
-		key, err := pythonToStarlarkValue(pykey)
-		if err != nil {
-			return &starlark.Dict{}, err
-		}
-
-		value, err := pythonToStarlarkValue(pyvalue)
+		value, err := innerPythonToStarlarkValue(pyvalue)
 		if err != nil {
-			return &starlark.Dict{}, err
+			return &starlark.Dict{}, fmt.Errorf("While converting value of key %v in Python dict: %v", key, err)
 		}
 
 		err = dict.SetKey(key, value)
 		if err != nil {
-			return &starlark.Dict{}, err
+			return &starlark.Dict{}, fmt.Errorf("While setting %v to %v in Starlark dict: %v", key, value, err)
 		}
 	}
 
+	if C.PyErr_Occurred() != nil {
+		return &starlark.Dict{}, fmt.Errorf("Python exception while iterating through Python dict")
+	}
+
 	return dict, nil
 }
 
 func pythonToStarlarkSet(obj *C.PyObject) (*starlark.Set, error) {
-	len := C.PyObject_Length(obj)
-	if len < 0 {
-		return &starlark.Set{}, fmt.Errorf("Set: couldn't get length of object")
+	size := C.PyObject_Length(obj)
+	if size < 0 {
+		return &starlark.Set{}, fmt.Errorf("Couldn't get size of Python set")
 	}
 
-	set := starlark.NewSet(int(len))
+	set := starlark.NewSet(int(size))
 	pyiter := C.PyObject_GetIter(obj)
 	if pyiter == nil {
-		return &starlark.Set{}, fmt.Errorf("Set: couldn't get iterator")
+		return &starlark.Set{}, fmt.Errorf("Couldn't get iterator for Python set")
 	}
 	defer C.Py_DecRef(pyiter)
 
 	for pyvalue := C.PyIter_Next(pyiter); pyvalue != nil; pyvalue = C.PyIter_Next(pyiter) {
 		defer C.Py_DecRef(pyvalue)
 
-		value, err := pythonToStarlarkValue(pyvalue)
+		value, err := innerPythonToStarlarkValue(pyvalue)
 		if err != nil {
-			return &starlark.Set{}, err
+			return &starlark.Set{}, fmt.Errorf("While converting value in Python set: %v", err)
 		}
 
 		err = set.Insert(value)
 		if err != nil {
 			raisePythonException(err)
-			return &starlark.Set{}, err
+			return &starlark.Set{}, fmt.Errorf("While inserting %v into Starlark set: %v", value, err)
 		}
 	}
 
+	if C.PyErr_Occurred() != nil {
+		return &starlark.Set{}, fmt.Errorf("Python exception while converting value in Python set to Starlark")
+	}
+
 	return set, nil
 }
 
@@ -149,7 +167,7 @@ func pythonToStarlarkString(obj *C.PyObject) (starlark.String, error) {
 	var size C.Py_ssize_t
 	cstr := C.PyUnicode_AsUTF8AndSize(obj, &size)
 	if cstr == nil {
-		return starlark.String(""), fmt.Errorf("Int: couldn't convert to C string")
+		return starlark.String(""), fmt.Errorf("Couldn't convert Python string to C string")
 	}
 
 	return starlark.String(C.GoString(cstr)), nil
@@ -159,7 +177,7 @@ func pythonToStarlarkInt(obj *C.PyObject) (starlark.Int, error) {
 	overflow := C.int(0)
 	longlong := int64(C.PyLong_AsLongLongAndOverflow(obj, &overflow)) // https://youtu.be/6-1Ue0FFrHY
 	if C.PyErr_Occurred() != nil {
-		return starlark.Int{}, fmt.Errorf("Int: couldn't convert to long")
+		return starlark.Int{}, fmt.Errorf("Couldn't convert Python int to long")
 	}
 
 	if overflow == 0 {
@@ -168,14 +186,14 @@ func pythonToStarlarkInt(obj *C.PyObject) (starlark.Int, error) {
 
 	pystr := C.PyObject_Str(obj)
 	if pystr == nil {
-		return starlark.Int{}, fmt.Errorf("Int: couldn't convert to Python string")
+		return starlark.Int{}, fmt.Errorf("Couldn't convert Python int to string")
 	}
 	defer C.Py_DecRef(pystr)
 
 	var size C.Py_ssize_t
 	cstr := C.PyUnicode_AsUTF8AndSize(pystr, &size)
 	if cstr == nil {
-		return starlark.Int{}, fmt.Errorf("Int: couldn't convert to C string")
+		return starlark.Int{}, fmt.Errorf("Couldn't convert Python int to C string")
 	}
 
 	i := new(big.Int)
@@ -186,13 +204,13 @@ func pythonToStarlarkInt(obj *C.PyObject) (starlark.Int, error) {
 func pythonToStarlarkFloat(obj *C.PyObject) (starlark.Float, error) {
 	cvalue := C.PyFloat_AsDouble(obj)
 	if C.PyErr_Occurred() != nil {
-		return starlark.Float(0), fmt.Errorf("Float: couldn't conver to double")
+		return starlark.Float(0), fmt.Errorf("Couldn't convert Python float to double")
 	}
 
 	return starlark.Float(cvalue), nil
 }
 
-func pythonToStarlarkValue(obj *C.PyObject) (starlark.Value, error) {
+func innerPythonToStarlarkValue(obj *C.PyObject) (starlark.Value, error) {
 	var value starlark.Value = nil
 	var err error = nil
 
@@ -219,25 +237,29 @@ func pythonToStarlarkValue(obj *C.PyObject) (starlark.Value, error) {
 		value, err = pythonToStarlarkList(obj)
 	case C.cgoPyTuple_Check(obj) == 1:
 		value, err = pythonToStarlarkTuple(obj)
-	case C.PyMapping_Check(obj) == 1:
-		value, err = pythonToStarlarkDict(obj)
 	case C.PySequence_Check(obj) == 1:
 		value, err = pythonToStarlarkList(obj)
+	case C.PyMapping_Check(obj) == 1:
+		value, err = pythonToStarlarkDict(obj)
+	default:
+		err = fmt.Errorf("Don't know how to convert Python %s to Starlark", C.GoString(obj.ob_type.tp_name))
 	}
 
-	if value == nil {
-		if err == nil {
-			err = fmt.Errorf("Can't convert Python %s to Starlark", C.GoString(obj.ob_type.tp_name))
+	if err == nil {
+		if C.PyErr_Occurred() != nil {
+			err = fmt.Errorf("Python exception while converting to Starlark")
 		}
 	}
 
+	return value, err
+}
+
+func pythonToStarlarkValue(obj *C.PyObject) (starlark.Value, error) {
+	value, err := innerPythonToStarlarkValue(obj)
 	if err != nil {
-		if C.PyErr_Occurred() == nil {
-			errmsg := C.CString(err.Error())
-			defer C.free(unsafe.Pointer(errmsg))
-			C.PyErr_SetString(C.ConversionError, errmsg)
-		}
+		handleConversionError(err, C.ConversionToStarlarkFailed)
+		return starlark.None, err
 	}
 
-	return value, err
+	return value, nil
 }
diff --git a/src/starlark_go/__init__.py b/src/starlark_go/__init__.py
index d54af8e..7a3084f 100644
--- a/src/starlark_go/__init__.py
+++ b/src/starlark_go/__init__.py
@@ -1,5 +1,7 @@
 from starlark_go.errors import (
     ConversionError,
+    ConversionToPythonFailed,
+    ConversionToStarlarkFailed,
     EvalError,
     ResolveError,
     ResolveErrorItem,
@@ -16,6 +18,8 @@
     "Starlark",
     "StarlarkError",
     "ConversionError",
+    "ConversionToPythonFailed",
+    "ConversionToStarlarkFailed",
     "EvalError",
     "ResolveError",
     "ResolveErrorItem",
diff --git a/src/starlark_go/errors.py b/src/starlark_go/errors.py
index d311d46..5d3da73 100644
--- a/src/starlark_go/errors.py
+++ b/src/starlark_go/errors.py
@@ -185,10 +185,24 @@ def __init__(
 
 class ConversionError(StarlarkError):
     """
-    A Starlark conversion error.
+    A base class for conversion errors.
+    """
+
+
+class ConversionToPythonFailed(ConversionError):
+    """
+    An error when converting a Starlark value to a Python value.
+
+    This exception is raied by :py:meth:`starlark_go.Starlark.eval`
+    and :py:meth:`starlark_go.Starlark.get` when a Starlark value can
+    not be converted to a Python value.
+    """
+
+
+class ConversionToStarlarkFailed(ConversionError):
+    """
+    An error when converting a Python value to a Starlark value.
 
-    This exception is raied by :py:meth:`starlark_go.Starlark.eval`,
-    :py:meth:`starlark_go.Starlark.get`, and :py:meth:`starlark_go.Starlark.set`
-    when a Starlark value can not be converted to a Python value, or when a
-    Python value can not be converted to a Starlark value.
+    This exception is raied by :py:meth:`starlark_go.Starlark.set`
+    when a Python value can not be converted to a Starlark value.
     """
diff --git a/starlark.c b/starlark.c
index efbebe6..6c06afd 100644
--- a/starlark.c
+++ b/starlark.c
@@ -22,7 +22,8 @@ PyObject *SyntaxError;
 PyObject *EvalError;
 PyObject *ResolveError;
 PyObject *ResolveErrorItem;
-PyObject *ConversionError;
+PyObject *ConversionToPythonFailed;
+PyObject *ConversionToStarlarkFailed;
 
 /* Wrapper for setting Starlark configuration options */
 static char *configure_keywords[] = {
@@ -99,7 +100,7 @@ PyDoc_STRVAR(
     "eval(self, expr, *, filename=None, convert=True, print=None)\n--\n\n"
     "Evaluate a Starlark expression. The expression passed to ``eval`` must evaluate "
     "to a value. Function definitions, variable assignments, and control structures "
-    "are not allowed by ``eval``. To use those, please use func:`exec`.\n\n"
+    "are not allowed by ``eval``. To use those, please use :meth:`exec`.\n\n"
     ":param expr: A string containing a Starlark expression to evaluate\n"
     ":type expr: str\n"
     ":param filename: An optional filename to use in exceptions, if evaluting the "
@@ -113,6 +114,8 @@ PyDoc_STRVAR(
     "unspecified, Starlark's ``print()`` function will be forwarded to Python's "
     "built-in :py:func:`python:print`.\n"
     ":type print: typing.Callable[[str], typing.Any]\n"
+    ":raises ConversionToPythonFailed: if the value is of an unsupported type for "
+    "conversion.\n"
     ":raises EvalError: if there is a Starlark evaluation error\n"
     ":raises ResolveError: if there is a Starlark resolution error\n"
     ":raises SyntaxError: if there is a Starlark syntax error\n"
@@ -138,7 +141,6 @@ PyDoc_STRVAR(
     "unspecified, Starlark's ``print()`` function will be forwarded to Python's "
     "built-in :py:func:`python:print`.\n"
     ":type print: typing.Callable[[str], typing.Any]\n"
-    ":raises ConversionError: if the value is of an unsupported type for conversion.\n"
     ":raises EvalError: if there is a Starlark evaluation error\n"
     ":raises ResolveError: if there is a Starlark resolution error\n"
     ":raises SyntaxError: if there is a Starlark syntax error\n"
@@ -177,14 +179,15 @@ PyDoc_STRVAR(
     "For the aggregate types (``dict``, ``list``, ``set``, and ``tuple``,) all keys "
     "and/or values must also be one of the supported types.\n\n"
     "Attempting to get the value of any other Starlark type will raise a "
-    ":py:class:`ConversionError`.\n\n"
+    ":py:class:`ConversionToPythonFailed`.\n\n"
     ":param name: The name of the global variable.\n"
     ":type name: str\n"
     ":param default_value: A default value to return, if no global variable named "
     "``name`` is defined.\n"
     ":type default_value: typing.Any\n"
     ":raises KeyError: if there is no global value named ``name`` defined.\n"
-    ":raises ConversionError: if the value is of an unsupported type for conversion.\n"
+    ":raises ConversionToPythonFailed: if the value is of an unsupported type for "
+    "conversion.\n"
     ":rtype: typing.Any\n"
 );
 
@@ -226,8 +229,9 @@ PyDoc_STRVAR(
     "For the aggregate types (``dict``, ``list``, ``set``, and ``tuple``,) all keys "
     "and/or values must also be one of the supported types.\n\n"
     "Attempting to set a value of any other Python type will raise a "
-    ":py:class:`ConversionError`.\n\n"
-    ":raises ConversionError: if a value is of an unsupported type for conversion.\n"
+    ":py:class:`ConversionToStarlarkFailed`.\n\n"
+    ":raises ConversionToStarlarkFailed: if a value is of an unsupported type for "
+    "conversion.\n"
 );
 
 PyDoc_STRVAR(
@@ -243,7 +247,8 @@ PyDoc_STRVAR(
     "``name`` is defined.\n"
     ":type default_value: typing.Any\n"
     ":raises KeyError: if there is no global value named ``name`` defined.\n"
-    ":raises ConversionError: if the value is of an unsupported type for conversion.\n"
+    ":raises ConversionToPythonFailed: if the value is of an unsupported type for "
+    "conversion.\n"
     ":rtype: typing.Any\n"
 );
 
@@ -555,8 +560,12 @@ PyMODINIT_FUNC PyInit_starlark_go(void)
   ResolveErrorItem = get_exception_class(errors, "ResolveErrorItem");
   if (ResolveErrorItem == NULL) return NULL;
 
-  ConversionError = get_exception_class(errors, "ConversionError");
-  if (ConversionError == NULL) return NULL;
+  ConversionToPythonFailed = get_exception_class(errors, "ConversionToPythonFailed");
+  if (ConversionToPythonFailed == NULL) return NULL;
+
+  ConversionToStarlarkFailed =
+      get_exception_class(errors, "ConversionToStarlarkFailed");
+  if (ConversionToStarlarkFailed == NULL) return NULL;
 
   PyObject *m;
   if (PyType_Ready(&StarlarkType) < 0) return NULL;
diff --git a/starlark_to_python.go b/starlark_to_python.go
index 0d23d88..359008a 100644
--- a/starlark_to_python.go
+++ b/starlark_to_python.go
@@ -3,7 +3,7 @@ package main
 /*
 #include "starlark.h"
 
-extern PyObject *ConversionError;
+extern PyObject *ConversionToPythonFailed;
 */
 import "C"
 
@@ -39,20 +39,24 @@ func starlarkDictToPython(x starlark.IterableMapping) (*C.PyObject, error) {
 	dict := C.PyDict_New()
 
 	for _, item := range items {
-		key, err := starlarkValueToPython(item[0])
-		defer C.Py_DecRef(key)
+		key, err := innerStarlarkValueToPython(item[0])
+		if key != nil {
+			defer C.Py_DecRef(key)
+		}
 
 		if err != nil {
 			C.Py_DecRef(dict)
-			return nil, err
+			return nil, fmt.Errorf("While converting key %v in Starlark dict: %v", item[0], err)
 		}
 
-		value, err := starlarkValueToPython((item[1]))
-		defer C.Py_DecRef(value)
+		value, err := innerStarlarkValueToPython((item[1]))
+		if value != nil {
+			defer C.Py_DecRef(value)
+		}
 
 		if err != nil {
 			C.Py_DecRef(dict)
-			return nil, err
+			return nil, fmt.Errorf("While converting value %v of key %v in Starlark dict: %v", item[1], item[0], err)
 		}
 
 		// This does not steal references
@@ -69,18 +73,20 @@ func starlarkTupleToPython(x starlark.Tuple) (*C.PyObject, error) {
 
 	var elem starlark.Value
 	for i := 0; iter.Next(&elem); i++ {
-		value, err := starlarkValueToPython(elem)
+		value, err := innerStarlarkValueToPython(elem)
 		if err != nil {
-			C.Py_DecRef(value)
+			if value != nil {
+				C.Py_DecRef(value)
+			}
 			C.Py_DecRef(tuple)
-			return nil, err
+			return nil, fmt.Errorf("While converting value %v at index %v in Starlark tuple: %v", elem, i, err)
 		}
 
 		// This "steals" the ref to value so we don't need to DecRef after
 		if C.PyTuple_SetItem(tuple, C.Py_ssize_t(i), value) != 0 {
 			C.Py_DecRef(value)
 			C.Py_DecRef(tuple)
-			return nil, fmt.Errorf("Tuple: setitem failed")
+			return nil, fmt.Errorf("Couldn't store converted value of %v at index %v in Python tuple: %v", elem, i, err)
 		}
 	}
 
@@ -94,17 +100,17 @@ func starlarkListToPython(x starlark.Iterable) (*C.PyObject, error) {
 
 	var elem starlark.Value
 	for i := 0; iter.Next(&elem); i++ {
-		value, err := starlarkValueToPython(elem)
+		value, err := innerStarlarkValueToPython(elem)
 		if err != nil {
 			C.Py_DecRef(list)
-			return nil, err
+			return nil, fmt.Errorf("While converting value %v at index %v in Starlark list: %v", elem, i, err)
 		}
 
 		// This "steals" the ref to value so we don't need to DecRef after
 		if C.PyList_Append(list, value) != 0 {
 			C.Py_DecRef(value)
 			C.Py_DecRef(list)
-			return nil, fmt.Errorf("List: append failed")
+			return nil, fmt.Errorf("Couldn't store converted value of %v at index %v in Python list: %v", elem, i, err)
 		}
 	}
 
@@ -118,12 +124,14 @@ func starlarkSetToPython(x starlark.Set) (*C.PyObject, error) {
 
 	var elem starlark.Value
 	for i := 0; iter.Next(&elem); i++ {
-		value, err := starlarkValueToPython(elem)
-		defer C.Py_DecRef(value)
+		value, err := innerStarlarkValueToPython(elem)
+		if value != nil {
+			defer C.Py_DecRef(value)
+		}
 
 		if err != nil {
 			C.Py_DecRef(set)
-			return nil, err
+			return nil, fmt.Errorf("While converting value %v in Starlark set: %v", elem, err)
 		}
 
 		// This does not steal references
@@ -139,7 +147,7 @@ func starlarkBytesToPython(x starlark.Bytes) (*C.PyObject, error) {
 	return C.PyBytes_FromStringAndSize(cstr, C.Py_ssize_t(x.Len())), nil
 }
 
-func starlarkValueToPython(x starlark.Value) (*C.PyObject, error) {
+func innerStarlarkValueToPython(x starlark.Value) (*C.PyObject, error) {
 	var value *C.PyObject = nil
 	var err error = nil
 
@@ -168,21 +176,25 @@ func starlarkValueToPython(x starlark.Value) (*C.PyObject, error) {
 		value, err = starlarkTupleToPython(x)
 	case starlark.Iterable:
 		value, err = starlarkListToPython(x)
+	default:
+		err = fmt.Errorf("Don't know how to convert Starlark %s to Python", reflect.TypeOf(x).String())
 	}
 
-	if value == nil {
-		if err == nil {
-			err = fmt.Errorf("Can't to convert Starlark %s to Python", reflect.TypeOf(x).String())
+	if err == nil {
+		if C.PyErr_Occurred() != nil {
+			err = fmt.Errorf("Python exception while converting from Starlark")
 		}
 	}
 
+	return value, err
+}
+
+func starlarkValueToPython(x starlark.Value) (*C.PyObject, error) {
+	value, err := innerStarlarkValueToPython(x)
 	if err != nil {
-		if C.PyErr_Occurred() == nil {
-			errmsg := C.CString(err.Error())
-			defer C.free(unsafe.Pointer(errmsg))
-			C.PyErr_SetString(C.ConversionError, errmsg)
-		}
+		handleConversionError(err, C.ConversionToPythonFailed)
+		return nil, err
 	}
 
-	return value, err
+	return value, nil
 }


Test patch:
diff --git a/tests/test_conversion_to_python_failed.py b/tests/test_conversion_to_python_failed.py
new file mode 100644
index 0000000..eaf3bd2
--- /dev/null
+++ b/tests/test_conversion_to_python_failed.py
@@ -0,0 +1,49 @@
+import pytest
+
+from starlark_go import ConversionToPythonFailed, Starlark
+
+STARLARK_SRC = """
+foo = print
+bar = [1, print, 2]
+baz = {"c": print}
+"""
+
+
+DONT_KNOW = "Don't know how to convert Starlark *starlark.Builtin to Python"
+LIST_INDEX_1 = (
+    "While converting value <built-in function print> at index 1 in Starlark list: "
+)
+DICT_KEY_C = (
+    'While converting value <built-in function print> of key "c" in Starlark dict: '
+)
+
+
+@pytest.fixture
+def s() -> Starlark:
+    starlark = Starlark()
+    starlark.exec(STARLARK_SRC, filename="fake.star")
+    return starlark
+
+
+def test_ConversionToPythonFailed(s: Starlark):
+    with pytest.raises(ConversionToPythonFailed) as e:
+        s.eval("print")
+    assert str(e.value) == DONT_KNOW
+
+    s.exec("foo = print")
+
+    with pytest.raises(ConversionToPythonFailed) as e:
+        s.get("foo")
+    assert str(e.value) == DONT_KNOW
+
+
+def test_ConversionToPythonFailed_bar(s: Starlark):
+    with pytest.raises(ConversionToPythonFailed) as e:
+        s.eval("bar")
+    assert str(e.value) == LIST_INDEX_1 + DONT_KNOW
+
+
+def test_ConversionToPythonFailed_baz(s: Starlark):
+    with pytest.raises(ConversionToPythonFailed) as e:
+        s.eval("baz")
+    assert str(e.value) == DICT_KEY_C + DONT_KNOW
diff --git a/tests/test_conversion_to_starlark_failed.py b/tests/test_conversion_to_starlark_failed.py
new file mode 100644
index 0000000..34cc2ce
--- /dev/null
+++ b/tests/test_conversion_to_starlark_failed.py
@@ -0,0 +1,64 @@
+from typing import Sequence
+
+import pytest
+
+from starlark_go import ConversionToStarlarkFailed, Starlark
+
+foo = print
+bar = [1, print, 2]
+baz = {"c": print}
+
+
+DONT_KNOW = "Don't know how to convert Python builtin_function_or_method to Starlark"
+LIST_INDEX_1 = "While converting value at index 1 in Python list: "
+DICT_KEY_C = 'While converting value of key "c" in Python dict: '
+
+
+class ExplodingException(Exception):
+    pass
+
+
+class ExplodingSequence(Sequence[str]):
+    def __len__(self):
+        return 3
+
+    def __getitem__(self, key: int):
+        if key == 1:
+            raise ExplodingException("Surprise!")
+        return key * 100
+
+
+@pytest.fixture
+def s() -> Starlark:
+    starlark = Starlark()
+    return starlark
+
+
+def test_ConversionToStarlarkFailed(s: Starlark):
+    with pytest.raises(ConversionToStarlarkFailed) as e:
+        s.set(foo=foo)
+    assert str(e.value) == DONT_KNOW
+    assert getattr(e.value, "__cause__", None) is None
+
+
+def test_ConversionToStarlarkFailed_bar(s: Starlark):
+    with pytest.raises(ConversionToStarlarkFailed) as e:
+        s.set(bar=bar)
+    assert str(e.value) == LIST_INDEX_1 + DONT_KNOW
+    assert getattr(e.value, "__cause__", None) is None
+
+
+def test_ConversionToStarlarkFailed_baz(s: Starlark):
+    with pytest.raises(ConversionToStarlarkFailed) as e:
+        s.set(baz=baz)
+    assert str(e.value) == DICT_KEY_C + DONT_KNOW
+    assert getattr(e.value, "__cause__", None) is None
+
+
+def test_exploding_sequence(s: Starlark):
+    with pytest.raises(ConversionToStarlarkFailed) as e:
+        s.set(surprise=ExplodingSequence())
+    print(str(e.value))
+    assert isinstance(e.value, ConversionToStarlarkFailed)
+    assert hasattr(e.value, "__cause__")
+    assert isinstance(getattr(e.value, "__cause__"), ExplodingException)