PyScript - Why Do We Need create_proxy()?
Published October 24, 2022
The Problem
Pyodide has an almost-magical ability to proxy objects and functions between Python and JavaScript in both directions... except when sometimes it seems to mysteriously break. Consider this stumbling block that new users often hit:
|
|
This seems like a perfectly reasonable thing to do, but upon clicking the button, an error pops up in the developer console:
The usual band-aid is wrap the Python Function in create_proxy()
like so:
|
|
Which seems to just make things work... but why?
Why create_proxy()
?
When you call something like button.addEventListener("click", hello)
(without create_proxy), Pyodide needs to briefly proxy the Python function hello
so the JS function addEventListener
knows how to interact with it. But once addEventListener
terminates, that proxy is no longer needed, it gets destroyed... and then when an event comes around to trigger your function, the proxy it should be using is gone. Which is why you'll see the error above talking about a "borrowed proxy being automatically destroyed".
The two functions that the Error mentions (create_proxy()
and create_once_callable()
) create a PyProxy (a JS object) of your Python object that you, the user, are supposed to manage the lifetime of, by calling PyProxy.destroy()
on it when you're done with it. Or, if you use create_once_callable()
, the proxy will destroy() itself after the first time it's called.
In practical terms, for something like an event listener, you may never want to destroy the proxy for the lifetime of your page, so you can just leave it hanging around. But it's worth noting that if you remove that event listener or button (maybe in a 'single-page-app' where you're manipulating what's on the page quite a bit), you should plan to track and destroy the PyProxy, otherwise it just hangs around taking up memory.
A Better Solution
Keeping track of the proxies that wrap each of our Python functions sounds like a real pain, no? Thankfully, there's a better way, thanks to some new features in the Pyodide runtime.
Since Pyodide 21.0 (PyScript 2022.09.1), there are now wrappers built into pyodide for adding event listeners: pyodide.ffi.wrappers.add_event_listener() and pyodide.ffi.wrappers.remove_event_listener() which, if you use them in conjunction, will handle proxy creation and destruction for you.
For example, here is the entirety of pyodide.ffi.wrappers.add_event_listener
:
|
|
You can see that this:
- Creates a proxy of the listener function using
create_proxy()
- Adds a reference to that proxy in an internal dictionary for later reference
- Adds the event listener using the browser's
addEventListener()
remove_event_listener
simply undoes this process - it removes the event listener using JavaScript's removeEventListener
, looks up the appropriate proxy in the internal dictionary, and destroy()
s it.
So now, our code above would look like:
|
|
I personally recommend using these wrapper methods for all new code where possible, instead of using create_proxy()
and addEventListener()
manually.