Repository: spyder-ide/spyder-kernels

Issue:
Debugger not working for environments with different Python versions
I think this is a spyder-kernels issue, apologies if not!

Using Spyder 6 alpha 2 and spyder-kernels 3.0.0b2, with a simple .py file with one line `1/0`.
Spyder is installed using mamba following the instructions on https://github.com/spyder-ide/spyder/releases

Two different ways to attempt debugging
1) Debug file (CTRL+F5)
2) Run file, then use `%debug` magic in console

With two different environments, `spyder-env` where Spyder is installed (3.10) and `custom_env` running Python 3.11. 

For 1) Spyder crashes with `TypeError: 'CommsErrorWrapper' object is not subscriptable` using the custom conda environment (`custom_env`). It does not crash when using the same environment as Spyder (`spyder-env`).

```python-traceback
Exception in comms call pdb_input:

  File "C:\Users\username\AppData\Local\mambaforge\envs\spyder-env\lib\site-packages\spyder_kernels\comms\commbase.py", line 323, in _handle_remote_call
	buffer['call_args'],

TypeError: 'CommsErrorWrapper' object is not subscriptable
```

For 2), running the file, then %debug in ipython console raises TypeError below both with custom environment and using the `spyder-env` where Spyder 6a2 is installed.

```python-traceback
Traceback (most recent call last):

  File c:\users\username\desktop\debugtest_6a2.py:8
    1/0

ZeroDivisionError: division by zero


%debug
> c:\users\username\desktop\debugtest_6a2.py(8)<module>()
      7 
----> 8 1/0

Traceback (most recent call last):

  Cell In[2], line 1
    get_ipython().run_line_magic('debug', '')

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\IPython\core\interactiveshell.py:2432 in run_line_magic
    result = fn(*args, **kwargs)

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\IPython\core\magics\execution.py:469 in debug
    self._debug_post_mortem()

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\IPython\core\magics\execution.py:483 in _debug_post_mortem
    self.shell.debugger(force=True)

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\IPython\core\interactiveshell.py:1146 in debugger
    self.InteractiveTB.debugger(force=True)

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\IPython\core\ultratb.py:1253 in debugger
    self.pdb.interaction(None, exc)

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\spyder_kernels\customize\spyderpdb.py:322 in interaction
    return super(SpyderPdb, self).interaction(

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\IPython\core\debugger.py:442 in interaction
    OldPdb.interaction(self, frame, tb)

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\pdb.py:426 in interaction
    self._cmdloop()

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\spyder_kernels\customize\spyderpdb.py:674 in _cmdloop
    self.cmdloop()

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\spyder_kernels\customize\spyderpdb.py:714 in cmdloop
    line = self.cmd_input(self.prompt)

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\spyder_kernels\customize\spyderpdb.py:740 in cmd_input
    prompt, state=self.get_pdb_state())

  File ~\AppData\Local\mambaforge\envs\custom_env\Lib\site-packages\spyder_kernels\customize\spyderpdb.py:864 in get_pdb_state
    pdb_index -= sum(hidden[:pdb_index])

TypeError: unsupported operand type(s) for +: 'int' and 'str'
```

## Versions

* Spyder version: 6.0.0a2  (conda)
* Python version: 3.10.12 64-bit
* Qt version: 5.15.8
* PyQt5 version: 5.15.9
* Operating System: Windows-10-10.0.19045-SP0


### Dependencies

```
# Mandatory:
atomicwrites >=1.2.0                                                        :  1.4.1 (OK)
chardet >=2.0.0                                                             :  5.2.0 (OK)
cloudpickle >=0.5.0                                                         :  2.2.1 (OK)
cookiecutter >=1.6.0                                                        :  2.3.0 (OK)
diff_match_patch >=20181111                                                 :  20230430 (OK)
intervaltree >=3.0.2                                                        :  3.1.0 (OK)
IPython >=7.31.1,<9.0.0,!=8.8.0,!=8.9.0,!=8.10.0,!=8.11.0,!=8.12.0,!=8.12.1 :  8.15.0 (OK)
jedi >=0.17.2,<0.19.0                                                       :  0.18.2 (OK)
jellyfish >=0.7                                                             :  1.0.0 (OK)
jsonschema >=3.2.0                                                          :  4.19.0 (OK)
keyring >=17.0.0                                                            :  24.2.0 (OK)
nbconvert >=4.0                                                             :  7.8.0 (OK)
numpydoc >=0.6.0                                                            :  1.5.0 (OK)
paramiko >=2.4.0                                                            :  3.3.1 (OK)
parso >=0.7.0,<0.9.0                                                        :  0.8.3 (OK)
pexpect >=4.4.0                                                             :  4.8.0 (OK)
pickleshare >=0.4                                                           :  0.7.5 (OK)
psutil >=5.3                                                                :  5.9.5 (OK)
pygments >=2.0                                                              :  2.16.1 (OK)
pylint >=2.5.0,<3.0                                                         :  2.17.5 (OK)
pylint_venv >=3.0.2                                                         :  3.0.2 (OK)
pyls_spyder >=0.4.0                                                         :  0.4.0 (OK)
pylsp >=1.7.4,<1.8.0                                                        :  1.7.4 (OK)
pylsp_black >=1.2.0,<3.0.0                                                  :  1.3.0 (OK)
pyuca >=1.2                                                                 :  1.2 (OK)
qdarkstyle >=3.0.2,<3.2.0                                                   :  3.1 (OK)
qstylizer >=0.2.2                                                           :  0.2.2 (OK)
qtawesome >=1.2.1                                                           :  1.2.3 (OK)
qtconsole >=5.4.2,<5.5.0                                                    :  5.4.4 (OK)
qtpy >=2.1.0                                                                :  2.4.0 (OK)
rtree >=0.9.7                                                               :  1.0.1 (OK)
setuptools >=49.6.0                                                         :  68.2.2 (OK)
sphinx >=0.6.6                                                              :  7.2.6 (OK)
spyder_kernels >=3.0.0b2,<3.0.0b3                                           :  3.0.0b2 (OK)
textdistance >=4.2.0                                                        :  4.5.0 (OK)
three_merge >=0.1.1                                                         :  0.1.1 (OK)
watchdog >=0.10.3                                                           :  3.0.0 (OK)
zmq >=22.1.0                                                                :  25.1.1 (OK)

# Optional:
cython >=0.21                                                               :  None (NOK)
matplotlib >=3.0.0                                                          :  None (NOK)
numpy >=1.7                                                                 :  None (NOK)
pandas >=1.1.1                                                              :  None (NOK)
scipy >=0.17.0                                                              :  None (NOK)
sympy >=0.7.3                                                               :  None (NOK)

```

Discussion:

Patch:
diff --git a/spyder_kernels/comms/commbase.py b/spyder_kernels/comms/commbase.py
index 20e3a8a..800078f 100644
--- a/spyder_kernels/comms/commbase.py
+++ b/spyder_kernels/comms/commbase.py
@@ -8,7 +8,8 @@
 Class that handles communications between Spyder kernel and frontend.
 
 Comms transmit data in a list of buffers, and in a json-able dictionnary.
-Here, we only support a buffer list with a single element.
+Here, we only support json to avoid issues of compatibility between Python
+versions. In the abstraction below, buffers is used to send bytes.
 
 The messages exchanged have the following msg_dict:
 
@@ -19,8 +20,6 @@ The messages exchanged have the following msg_dict:
     }
     ```
 
-The buffer is generated by cloudpickle using `PICKLE_PROTOCOL = 2`.
-
 To simplify the usage of messaging, we use a higher level function calling
 mechanism:
     - The `remote_call` method returns a RemoteCallHandler object
@@ -35,33 +34,32 @@ The messages exchanged are:
             'call_name': The name of the function to be called,
             'call_id': uuid to match the request to a potential reply,
             'settings': A dictionnary of settings,
-            }
-        - The buffer encodes a dictionnary {
             'call_args': The function args,
             'call_kwargs': The function kwargs,
-            }
+            'buffered_args': The args index that are in the buffers,
+            'buffered_kwargs': the kwargs keys that are in the buffers
+          }
+        - The buffer contains any bytes in the arguments
     - If the 'settings' has `'blocking' =  True`, a reply is sent.
       (spyder_msg_type = 'remote_call_reply'):
-        - The buffer contains the return value of the function.
         - The 'content' is a dict with: {
-                'is_error': a boolean indicating if the return value is an
-                            exception to be raised.
-                'call_id': The uuid from above,
-                'call_name': The function name (mostly for debugging)
-                }
+            'is_error': a boolean indicating if the return value is an
+                        exception to be raised.
+            'call_id': The uuid from above,
+            'call_name': The function name (mostly for debugging),
+            'call_return_value': The return value of the function
+           }
+        - The buffer contains the return value if it is bytes
 """
-import cloudpickle
-import pickle
 import logging
 import sys
 import uuid
 import traceback
+import builtins
 
 
 logger = logging.getLogger(__name__)
 
-DEFAULT_PICKLE_PROTOCOL = 4
-
 # Max timeout (in secs) for blocking calls
 TIMEOUT = 3
 
@@ -70,6 +68,32 @@ class CommError(RuntimeError):
     pass
 
 
+def stacksummary_to_json(stack):
+    """StackSummary to json."""
+    return [
+        {
+            "filename": frame.filename,
+            "lineno": frame.lineno,
+            "name": frame.name,
+            "line": frame.line
+        }
+        for frame in stack
+    ]
+
+
+def staksummary_from_json(stack):
+    """StackSummary from json."""
+    traceback.StackSummary.from_list([
+        (
+            frame["filename"],
+            frame["lineno"],
+            frame["name"],
+            frame["line"]
+        )
+        for frame in stack
+    ])
+
+
 class CommsErrorWrapper():
     def __init__(self, call_name, call_id):
         self.call_name = call_name
@@ -77,6 +101,32 @@ class CommsErrorWrapper():
         self.etype, self.error, tb = sys.exc_info()
         self.tb = traceback.extract_tb(tb)
 
+    def to_json(self):
+        """Create JSON representation."""
+        return {
+            "call_name": self.call_name,
+            "call_id": self.call_id,
+            "etype": self.etype.__name__,
+            "args": self.error.args,
+            "tb": stacksummary_to_json(self.tb)
+        }
+
+    @classmethod
+    def from_json(cls, json_data):
+        """Get a CommsErrorWrapper from a JSON representation."""
+        instance = cls.__new__(cls)
+        instance.call_name = json_data["call_name"]
+        instance.call_id = json_data["call_id"]
+        etype = json_data["etype"]
+        instance.etype = getattr(
+            builtins,
+            etype,
+            type(etype, (Exception,), {})
+        )
+        instance.error = instance.etype(*json_data["args"])
+        instance.tb = staksummary_from_json(json_data["tb"])
+        return instance
+
     def raise_error(self):
         """
         Raise the error while adding informations on the callback.
@@ -204,8 +254,9 @@ class CommBase:
         return RemoteCallFactory(self, comm_id, callback, **settings)
 
     # ---- Private -----
-    def _send_message(self, spyder_msg_type, content=None, data=None,
-                      comm_id=None):
+    def _send_message(
+        self, spyder_msg_type, content=None, comm_id=None, buffers=None
+    ):
         """
         Publish custom messages to the other side.
 
@@ -215,11 +266,10 @@ class CommBase:
             The spyder message type
         content: dict
             The (JSONable) content of the message
-        data: any
-            Any object that is serializable by cloudpickle (should be most
-            things). Will arrive as cloudpickled bytes in `.buffers[0]`.
         comm_id: int
             the comm to send to. If None sends to all comms.
+        buffers: list(bytes)
+            a list of bytes to send.
         """
         if not self.is_open(comm_id):
             raise CommError("The comm is not connected.")
@@ -228,17 +278,9 @@ class CommBase:
             msg_dict = {
                 'spyder_msg_type': spyder_msg_type,
                 'content': content,
-                'pickle_protocol': self._comms[comm_id]['pickle_protocol'],
-                'python_version': sys.version,
-                }
-            buffers = [cloudpickle.dumps(
-                data, protocol=self._comms[comm_id]['pickle_protocol'])]
-            self._comms[comm_id]['comm'].send(msg_dict, buffers=buffers)
+            }
 
-    def _set_pickle_protocol(self, protocol):
-        """Set the pickle protocol used to send data."""
-        protocol = min(protocol, pickle.HIGHEST_PROTOCOL)
-        self._comms[self.calling_comm_id]['pickle_protocol'] = protocol
+            self._comms[comm_id]['comm'].send(msg_dict, buffers=buffers)
 
     @property
     def _comm_name(self):
@@ -256,9 +298,8 @@ class CommBase:
         message_id : str
             The identifier for the message
         handler : callback
-            A function to handle the message. This is called with 3 arguments:
+            A function to handle the message. This is called with:
                 - msg_dict: A dictionary with message information.
-                - buffer: The data transmitted in the buffer
             Pass None to unregister the message_id
         """
         if handler is None:
@@ -275,9 +316,8 @@ class CommBase:
         comm.on_close(self._comm_close)
         self._comms[comm.comm_id] = {
             'comm': comm,
-            'pickle_protocol': DEFAULT_PICKLE_PROTOCOL,
             'status': 'opening',
-            }
+        }
 
     def _comm_close(self, msg):
         """Close comm."""
@@ -292,40 +332,35 @@ class CommBase:
 
         # Get message dict
         msg_dict = msg['content']['data']
-
-        # Load the buffer. Only one is supported.
-        try:
-            buffer = cloudpickle.loads(msg['buffers'][0])
-        except Exception as e:
-            logger.debug(
-                "Exception in cloudpickle.loads : %s" % str(e))
-            buffer = CommsErrorWrapper(
-                msg_dict['content']['call_name'],
-                msg_dict['content']['call_id'])
-
-            msg_dict['content']['is_error'] = True
-
         spyder_msg_type = msg_dict['spyder_msg_type']
+        buffers = msg['buffers']
 
         if spyder_msg_type in self._message_handlers:
-            self._message_handlers[spyder_msg_type](
-                msg_dict, buffer)
+            self._message_handlers[spyder_msg_type](msg_dict, buffers)
         else:
             logger.debug("No such spyder message type: %s" % spyder_msg_type)
 
-    def _handle_remote_call(self, msg, buffer):
+    def _handle_remote_call(self, msg, buffers):
         """Handle a remote call."""
         msg_dict = msg['content']
         self.on_incoming_call(msg_dict)
-        if msg['content'].get('is_error', False):
-            # could not open the pickle
-            self._set_call_return_value(msg, buffer, is_error=True)
-            return
         try:
+            # read buffers
+            args = msg_dict['call_args']
+            kwargs = msg_dict['call_kwargs']
+
+            if buffers:
+                for idx in msg_dict['buffered_args']:
+                    args[idx] = buffers.pop(0)
+                for name in msg_dict['buffered_kwargs']:
+                    kwargs[name] = buffers.pop(0)
+                assert len(buffers) == 0
+
             return_value = self._remote_callback(
-                    msg_dict['call_name'],
-                    buffer['call_args'],
-                    buffer['call_kwargs'])
+                msg_dict['call_name'],
+                args,
+                kwargs
+            )
             self._set_call_return_value(msg_dict, return_value)
         except Exception:
             exc_infos = CommsErrorWrapper(
@@ -340,7 +375,7 @@ class CommBase:
 
         raise CommError("No such spyder call type: %s" % call_name)
 
-    def _set_call_return_value(self, call_dict, data, is_error=False):
+    def _set_call_return_value(self, call_dict, return_value, is_error=False):
         """
         A remote call has just been processed.
 
@@ -350,21 +385,34 @@ class CommBase:
 
         display_error = ('display_error' in settings and
                          settings['display_error'])
-        if is_error and display_error:
-            data.print_error()
+        if is_error:
+            if display_error:
+                return_value.print_error()
+            return_value = return_value.to_json()
 
         send_reply = 'send_reply' in settings and settings['send_reply']
         if not send_reply:
             # Nothing to send back
             return
+
+        buffers = None
+        if isinstance(return_value, bytes):
+            buffers = [return_value]
+            return_value = None
+
         content = {
             'is_error': is_error,
             'call_id': call_dict['call_id'],
-            'call_name': call_dict['call_name']
+            'call_name': call_dict['call_name'],
+            'call_return_value': return_value
         }
 
-        self._send_message('remote_call_reply', content=content, data=data,
-                           comm_id=self.calling_comm_id)
+        self._send_message(
+            'remote_call_reply',
+            content=content,
+            comm_id=self.calling_comm_id,
+            buffers=buffers
+        )
 
     def _register_call(self, call_dict, callback=None):
         """
@@ -378,20 +426,18 @@ class CommBase:
 
     def on_outgoing_call(self, call_dict):
         """A message is about to be sent"""
-        call_dict["pickle_highest_protocol"] = pickle.HIGHEST_PROTOCOL
         return call_dict
 
     def on_incoming_call(self, call_dict):
         """A call was received"""
-        if "pickle_highest_protocol" in call_dict:
-            self._set_pickle_protocol(call_dict["pickle_highest_protocol"])
+        pass
 
-    def _send_call(self, call_dict, call_data, comm_id):
+    def _send_call(self, call_dict, comm_id, buffers=None):
         """Send call."""
         call_dict = self.on_outgoing_call(call_dict)
         self._send_message(
-            'remote_call', content=call_dict, data=call_data,
-            comm_id=comm_id)
+            'remote_call', content=call_dict, comm_id=comm_id, buffers=buffers
+        )
 
     def _get_call_return_value(self, call_dict, comm_id):
         """
@@ -403,7 +449,6 @@ class CommBase:
         settings = call_dict['settings']
 
         blocking = 'blocking' in settings and settings['blocking']
-
         if not blocking:
             return
 
@@ -418,12 +463,12 @@ class CommBase:
 
         self._wait_reply(comm_id, call_id, call_name, timeout)
 
-        reply = self._reply_inbox.pop(call_id)
+        content = self._reply_inbox.pop(call_id)
+        return_value = content['call_return_value']
 
-        if reply['is_error']:
-            return self._sync_error(reply['value'])
-
-        return reply['value']
+        if content['is_error']:
+            return self._sync_error(return_value)
+        return return_value
 
     def _wait_reply(self, comm_id, call_id, call_name, timeout):
         """
@@ -431,7 +476,7 @@ class CommBase:
         """
         raise NotImplementedError
 
-    def _handle_remote_call_reply(self, msg_dict, buffer):
+    def _handle_remote_call_reply(self, msg_dict, buffers):
         """
         A blocking call received a reply.
         """
@@ -439,11 +484,20 @@ class CommBase:
         call_id = content['call_id']
         call_name = content['call_name']
         is_error = content['is_error']
+        return_value = content['call_return_value']
+
+        # Prepare return value
+        if is_error:
+            return_value = CommsErrorWrapper.from_json(return_value)
+        elif buffers:
+            assert len(buffers) == 1
+            return_value = buffers[0]
+        content['call_return_value'] = return_value
 
         # Unexpected reply
         if call_id not in self._reply_waitlist:
             if is_error:
-                return self._async_error(buffer)
+                return self._async_error(return_value)
             else:
                 logger.debug('Got an unexpected reply {}, id:{}'.format(
                     call_name, call_id))
@@ -453,19 +507,15 @@ class CommBase:
 
         # Async error
         if is_error and not blocking:
-            return self._async_error(buffer)
+            return self._async_error(return_value)
 
         # Callback
         if callback is not None and not is_error:
-            callback(buffer)
+            callback(return_value)
 
         # Blocking inbox
         if blocking:
-            self._reply_inbox[call_id] = {
-                    'is_error': is_error,
-                    'value': buffer,
-                    'content': content
-                    }
+            self._reply_inbox[call_id] = content
 
     def _async_error(self, error_wrapper):
         """
@@ -515,21 +565,40 @@ class RemoteCall():
         """
         Transmit the call to the other side of the tunnel.
 
-        The args and kwargs have to be picklable.
+        The args and kwargs have to be JSON-serializable or bytes.
         """
         blocking = 'blocking' in self._settings and self._settings['blocking']
         self._settings['send_reply'] = blocking or self._callback is not None
 
+        # The call will be serialized with json. The bytes are sent separately.
+        buffers = []
+        buffered_args = []
+        buffered_kwargs = []
+        args = list(args)
+
+        for i, arg in enumerate(args):
+            if isinstance(arg, bytes):
+                buffers.append(arg)
+                buffered_args.append(i)
+                args[i] = None
+
+        for name in kwargs:
+            arg = kwargs[name]
+            if isinstance(arg, bytes):
+                buffers.append(arg)
+                buffered_kwargs.append(name)
+                kwargs[name] = None
+
         call_id = uuid.uuid4().hex
         call_dict = {
             'call_name': self._name,
             'call_id': call_id,
             'settings': self._settings,
-            }
-        call_data = {
             'call_args': args,
             'call_kwargs': kwargs,
-            }
+            'buffered_args': buffered_args,
+            'buffered_kwargs': buffered_kwargs
+        }
 
         if not self._comms_wrapper.is_open(self._comm_id):
             # Only an error if the call is blocking.
@@ -538,6 +607,6 @@ class RemoteCall():
             logger.debug("Call to unconnected comm: %s" % self._name)
             return
         self._comms_wrapper._register_call(call_dict, self._callback)
-        self._comms_wrapper._send_call(call_dict, call_data, self._comm_id)
+        self._comms_wrapper._send_call(call_dict, self._comm_id, buffers)
         return self._comms_wrapper._get_call_return_value(
             call_dict, self._comm_id)
diff --git a/spyder_kernels/comms/frontendcomm.py b/spyder_kernels/comms/frontendcomm.py
index 9eaf2ee..514d540 100644
--- a/spyder_kernels/comms/frontendcomm.py
+++ b/spyder_kernels/comms/frontendcomm.py
@@ -176,8 +176,6 @@ class FrontendComm(CommBase):
         """
         self.calling_comm_id = comm.comm_id
         self._register_comm(comm)
-        self._set_pickle_protocol(
-            msg['content']['data']['pickle_highest_protocol'])
 
         # IOPub might not be connected yet, keep sending messages until a
         # reply is received.
@@ -196,7 +194,7 @@ class FrontendComm(CommBase):
         """
         Send an async error back to the frontend to be displayed.
         """
-        self.remote_call()._async_error(error_wrapper)
+        self.remote_call()._async_error(error_wrapper.to_json())
 
     def _register_comm(self, comm):
         """
diff --git a/spyder_kernels/console/kernel.py b/spyder_kernels/console/kernel.py
index 15a407a..da59537 100644
--- a/spyder_kernels/console/kernel.py
+++ b/spyder_kernels/console/kernel.py
@@ -20,6 +20,7 @@ import sys
 import traceback
 import tempfile
 import threading
+import cloudpickle
 
 # Third-party imports
 from ipykernel.ipkernel import IPythonKernel
@@ -30,6 +31,7 @@ from zmq.utils.garbage import gc
 
 # Local imports
 import spyder_kernels
+from spyder_kernels.comms.commbase import stacksummary_to_json
 from spyder_kernels.comms.frontendcomm import FrontendComm
 from spyder_kernels.comms.decorators import (
     register_comm_handlers, comm_handler)
@@ -257,7 +259,7 @@ class SpyderKernel(IPythonKernel):
         """Get the current frames."""
         ignore_list = self.get_system_threads_id()
         main_id = threading.main_thread().ident
-        frames = {}
+        stack_dict = {}
         thread_names = {thread.ident: thread.name
                         for thread in threading.enumerate()}
 
@@ -274,8 +276,12 @@ class SpyderKernel(IPythonKernel):
                     thread_name = thread_names[thread_id]
                 else:
                     thread_name = str(thread_id)
-                frames[thread_name] = stack
-        return frames
+
+                # Transform stack in a dict because FrameSummary objects
+                # are not compatible between versions of Python
+                stack_dict[thread_name] = stacksummary_to_json(stack)
+
+        return stack_dict
 
     # --- For the Variable Explorer
     @comm_handler
@@ -346,14 +352,22 @@ class SpyderKernel(IPythonKernel):
             return None
 
     @comm_handler
-    def get_value(self, name):
+    def get_value(self, name, encoded=False):
         """Get the value of a variable"""
         ns = self.shell._get_current_namespace()
-        return ns[name]
+        value = ns[name]
+        if encoded:
+            # Encode with cloudpickle
+            value = cloudpickle.dumps(value)
+        return value
 
     @comm_handler
-    def set_value(self, name, value):
+    def set_value(self, name, value, encoded=False):
         """Set the value of a variable"""
+        if encoded:
+            # Decode_value
+            value = cloudpickle.loads(value)
+
         ns = self.shell._get_reference_namespace(name)
         ns[name] = value
         self.log.debug(ns)
@@ -705,7 +719,7 @@ class SpyderKernel(IPythonKernel):
             exec("from pylab import *", self.shell.user_ns)
             self.shell.special = special
             return
-           
+
         if special == "sympy":
             import sympy  # noqa
             sympy_init = "\n".join([
diff --git a/spyder_kernels/console/shell.py b/spyder_kernels/console/shell.py
index 48cecaf..5a9c1a2 100644
--- a/spyder_kernels/console/shell.py
+++ b/spyder_kernels/console/shell.py
@@ -26,6 +26,7 @@ from ipykernel.zmqshell import ZMQInteractiveShell
 from spyder_kernels.customize.namespace_manager import NamespaceManager
 from spyder_kernels.customize.spyderpdb import SpyderPdb
 from spyder_kernels.customize.code_runner import SpyderCodeRunner
+from spyder_kernels.comms.commbase import stacksummary_to_json
 from spyder_kernels.comms.decorators import comm_handler
 from spyder_kernels.utils.mpl import automatic_backend
 
@@ -271,7 +272,9 @@ class SpyderShell(ZMQInteractiveShell):
         if not exception_only:
             try:
                 etype, value, tb = self._get_exc_info(exc_tuple)
-                stack = traceback.extract_tb(tb.tb_next)
+                etype = etype.__name__
+                value = value.args
+                stack = stacksummary_to_json(traceback.extract_tb(tb.tb_next))
                 self.kernel.frontend_call(blocking=False).show_traceback(
                     etype, value, stack)
             except Exception:
diff --git a/spyder_kernels/customize/namespace_manager.py b/spyder_kernels/customize/namespace_manager.py
index f758cf2..c3fcbcb 100755
--- a/spyder_kernels/customize/namespace_manager.py
+++ b/spyder_kernels/customize/namespace_manager.py
@@ -76,7 +76,6 @@ class NamespaceManager:
             self.ns_globals = main_mod.__dict__
             self.ns_locals = None
 
-            # Needed to allow pickle to reference main
             if '__main__' in sys.modules:
                 self._previous_main = sys.modules['__main__']
             sys.modules['__main__'] = main_mod
diff --git a/spyder_kernels/customize/spyderpdb.py b/spyder_kernels/customize/spyderpdb.py
index 8fd4e97..a522afa 100755
--- a/spyder_kernels/customize/spyderpdb.py
+++ b/spyder_kernels/customize/spyderpdb.py
@@ -24,6 +24,7 @@ from IPython.core.debugger import Pdb as ipyPdb
 from IPython.core.inputtransformer2 import TransformerManager
 
 import spyder_kernels
+from spyder_kernels.comms.commbase import stacksummary_to_json
 from spyder_kernels.comms.frontendcomm import CommError, frontend_request
 from spyder_kernels.customize.utils import (
     path_is_library, capture_last_Expr, exec_encapsulate_locals
@@ -829,9 +830,10 @@ class SpyderPdb(ipyPdb):
 
         if self.pdb_publish_stack:
             # Publish Pdb stack so we can update the Debugger plugin on Spyder
-            pdb_stack = traceback.StackSummary.extract(self.stack)
+            pdb_stack = stacksummary_to_json(
+                traceback.StackSummary.extract(self.stack)
+            )
             pdb_index = self.curindex
-
             skip_hidden = getattr(self, 'skip_hidden', False)
 
             if skip_hidden:


Test patch:
diff --git a/spyder_kernels/console/tests/test_console_kernel.py b/spyder_kernels/console/tests/test_console_kernel.py
index b7bbbc6..60f240d 100644
--- a/spyder_kernels/console/tests/test_console_kernel.py
+++ b/spyder_kernels/console/tests/test_console_kernel.py
@@ -1272,13 +1272,12 @@ def test_interrupt():
     """
     # Command to start the kernel
     cmd = "from spyder_kernels.console import start; start.main()"
-    import pickle
     with setup_kernel(cmd) as client:
         kernel_comm = CommBase()
 
         # Create new comm and send the highest protocol
         comm = Comm(kernel_comm._comm_name, client)
-        comm.open(data={'pickle_highest_protocol': pickle.HIGHEST_PROTOCOL})
+        comm.open(data={})
         comm._send_channel = client.control_channel
         kernel_comm._register_comm(comm)
 
@@ -1328,13 +1327,12 @@ def test_enter_debug_after_interruption():
     """
     # Command to start the kernel
     cmd = "from spyder_kernels.console import start; start.main()"
-    import pickle
     with setup_kernel(cmd) as client:
         kernel_comm = CommBase()
 
         # Create new comm and send the highest protocol
         comm = Comm(kernel_comm._comm_name, client)
-        comm.open(data={'pickle_highest_protocol': pickle.HIGHEST_PROTOCOL})
+        comm.open(data={})
         comm._send_channel = client.control_channel
         kernel_comm._register_comm(comm)
 
@@ -1393,7 +1391,7 @@ def test_django_settings(kernel):
 
 def test_hard_link_pdb(tmpdir):
     """
-    Test that breakpoints on a file are recognised even when the path is 
+    Test that breakpoints on a file are recognised even when the path is
     different.
     """
     # Create a file and a hard link
@@ -1403,10 +1401,10 @@ def test_hard_link_pdb(tmpdir):
     os.mkdir(folder)
     hard_link = folder.join("file.py")
     os.link(d, hard_link)
-    
+
     # Make sure both paths point to the same file
     assert os.path.samefile(d, hard_link)
-    
+
     # Make sure canonic returns the same path for a single file
     pdb_obj = SpyderPdb()
     assert pdb_obj.canonic(str(d)) == pdb_obj.canonic(str(hard_link))