I'm working on a development environment for
Python based on Emacs' SLIME mode for Common Lisp.
In this talk I'll demonstrate some of its
features, like an object inspector, interactive
backtrace buffer, thread and async task viewer,
and function tracer. I'll also discuss its
implementation and limitations, along with future
directions for the project.
This project aims to bring a Lisp and Smalltalk
inspired style of development to Python. You get a
faster feedback loop by developing inside a
running python process without needing to restart
your program and lose state on changes, allowing
you to immediately inspect the results of code you
write. We can also provide more advanced tooling
based on runtime introspection, as we have more
information available at runtime than is available
to traditional tools based on static analysis of
source code, mainly we have the actual values of
variables rather than just their types.
About the speaker:
Python is eating the world. Emacs is eating my computing environment. I'm
attempting to get them working together.
Discussion / notes
Q: Does swanky-python work with Sly?
A: It doesn't, Sly is great but I went with slime for a few
reasons:
I wanted to use some cool stuff from slime-star
I actually think there's good potential with slime's presentations that sly removed.
The main feature of sly missing from slime is stickers.
slime-star provides something similar in being able to
recompile a function with an expression traced, but I
think for python it'll be better to integrate with dape
for debugging
In recent years slime has been more actively maintained
in terms of bug fixes and such.
Though even when the swanky python backend was written in Hy, it
was still targeted at editing python code, not Hy. Implementing
it in Hy just made the implementation a bit easier, as the slime
"protocol" is just slime sending lisp forms to the swank
backend to evaluate, so to write the backend in python we need
to implement a lisp interpreter (swank_eval in server.py), which
we already have in Hy.
To make it work for editing Hy code would require some changes
on the backend, around evaling code, returning source locations,
and autocomplete. But most would stay the same, so I think it
could be made to support both without needing to fork a separate
project. I don't plan to use Hy or work on it. When writing
lisp I'd rather write CL, and when writing python I'd rather
use standard python syntax. But if someone wants to add Hy
support I'd be happy to merge it and assist a bit.
Q: Where can I find a list of Slime-like interfaces for other
languages?
Q: Is there an IRC channel for swanky-python? If not, are you
interested in creating one?
A: Good idea to have, I just made #swankypython on libera
Q:How would this integrate with python notebooks such as marimo?
A: I've never used marimo, just jupyter, but it looks nicer so
I'd like to try it out sometime. The most basic integration
would be to just run swanky python within the notebook. That way
you would use the notebook as normal, but get the interactive
backtrace buffer in emacs on any exception, and be able to use
the inspector, nicer repl, etc. A more complete integration
would probably be based on emacs-jupyter but I haven't looked
into it yet.
Q: Why not org babel as well? +1 for org-babel with this, would be
awesome
A: That'd be great and probably not much work at all. I just
tried evaling python code as a "lisp" block, since babel for
lisp calls slime-eval, and it dies with an exception because I
haven't implemented swank:eval-and-grab-output in swanky python
yet. Maybe it's just needed to implement that and then
configure babel for python src blocks to use slime-eval rather
than running with org-babel-python-command.
Tangentially, did you see Kent Pitman's recent moves to introduce
his common lisp condition system to python? E.g. about resuming
execution after an exception. He showed it some sunday lispy gopher
climate. In my opinion, reach out to
https://climatejustice.social/@kentpitman
since you asked for contact about lisp-style python exception
restarts which he has worked on recently.
But a condition system is a bit of a separate issue from the
exception restarts I'd like to have. A condition system can be
implemented without any changes to the runtime, in any language with
dynamic scope and first class functions. And dynamic scope can be
emulated in any language with global variables, so people have
implemented Common Lisp (CL) style condition systems as libraries
for many languages. If this was used universally in place of the
language's native exceptions, it would give the ability to drop
into a repl at the point of an otherwise uncaught exception, but not
the ability to restart execution from any stack frame. Smalltalk has
traditional exceptions and not a CL like condition system, but its
debugger does provide this ability, as does the JVM and v8
debuggers. In CL this ability (sldb-restart-frame in slime) isn't
provided by the condition system, but in SBCL for example by
internal functions in the sb-debug and sb-di packages.
It'd be interesting to experiment with a condition system in
Python, but what I'm more interested in is the ability on any
runtime error, to be able to fix the bug and restart execution from
any stack frame.
This is really cool, I am amazed how much functionality you have
implemented! I hope I can start using this in my day job!
Very very impressive. I will definitely try to use this in my
workflow. I love the Lisp development style.
very impressive. I am also working on a Python IDE with a python
process and a webview to host the python runtime and display the
IDE, but I am very far behind in terms of features. I just made the
reload system work and the code(AST)->html renderer
Neat, if you publish it send me a link!
Definitely going to give it a try! I've been missing interactive
since learning python many years ago, even before I knew Common Lisp
existed and one of the primary reasons why Common Lisp replaced
python as my go-to language
Such a package alone would automatically make Emacs a much better option than something like PyCharm.
I found it very funny how he showed M-x doctor But very interesting talk!
Transcript
Hello everyone, I'm Scottand I'll be talking about Swanky Python,which is a development environment for Pythonbased on Emacs' Slime package.So what is that and why might you find it interesting?SLIME is the Superior Lisp Interaction Mode for Emacs.It's an Emacs package for developing Common Lisp,and it's a bit different from the way we develop most languagesin that you're always connectedto a running instance of your application,and you kind of build up your application, piece by piece,modifying one expression at a timewithout ever having to restart your application.So why might you want to develop this way?One advantage is that you can get a faster feedback loop.For some kinds of software, it doesn't make a big difference.Like, if you're developing a web backendwhere all state is stored externally in a database,then you can have a file watcherthat just restarts the whole Python processwhenever you make any edit,and you're not really losing anything,because all the state is stored outside the Python processin a database. So it works great.But for other kinds of software, likelet's say you're developing an Emacs packageor a video game,then it can be a real pain to restart the applicationand recreate the state it was in beforejust to test the effect of each edit you want to make.Another advantage is the runtime introspection you have available.So since you're always connectedto a running instance of your application,you can inspect the values of variables,you can trace functions, and all sorts of other informationto help you understand your application better.And lastly, it's just a lot of fun to develop this way,or at least I find it fun developing with SLIME,so I wrote a SLIME backend for Pythonso I could have more fun when I'm coding in Python.As for the name swanky-python, within SLIME,swank is the name of the Common Lisp backendthat runs within your Common Lisp applicationand connects to Emacs. So I'm not too creative.swanky-python is just a swank implementation in Python.
So let's see it in action. So we started up with M-x slime.And what that does is it starts a Python process,starts swanky-python within it, and connects to it from Emacs.And you can configure how exactly it runs Python.Or you can start swanky python manuallywithin a Python application running on a remote serverand forward the port locallyand connect to it in Emacs, from Emacs remotely.Within the README, there's more documentationon other ways to start it.But just M-x slime is the basic way that works most of the time.So within the REPL, the first thing you'll notice is thatREPL outputs are clickable buttons,what SLIME calls presentations.So you can do things like inspect them.And for each presentation, in the Python backend,it holds on to the reference to the object.So for an int, it's not too interesting,but let's do a more complex object like a file.Then we can inspect the file.We can describe it, which will bring up documentationon that class. We can use it in further expressionslike if we copy it, it will use the actual Python objectin this expression.We can assign it to a variable.SLIME uses presentations everywherethat a Python object would be displayed.So instead of just their string representation,when you have a backtrace on an exception,or you... within the inspector or anywhere else really,anywhere that the string representationof an object would be displayed,it displays a presentation that you can go on toinspect, reuse, or send to the REPL and so on.One useful utility function is pp for print presentation.We haven't imported it yet.So when we get a name error exceptionand SLIME sees that that name is available for import somewhere,it'll give us the option of importing it.Since it's available for import from multiple modules,it'll prompt us for which one we want to import it from.We want to import it from swanky-python,not from the standard library.Then it will print a presentation of that object.Within the REPL, this is not really usefulbecause all REPL outputs are already presentations.But I use this now whenever I would use print debugging,just whenever I would use insert print statements in my programto see what's going on, I have it print a presentationbecause that way I can go back and inspect it later,copy it to the REPL and further manipulate it and so on.
Next up, let's look at the inspector more.If we go back and inspect the file object,you can write custom inspector viewsfor different kinds of objects.So far, I just have a couple. One for sequences,one for mappings, and one for every other kind of object.Like if we inspect a mapping, there's a shortcutinspect last result, which is what I normally useto open the inspector. Then we see the values,and each value in the inspector is a presentationthat we can go on to inspect, and so on.Let's go back to inspecting the file object.Again, we can inspect each of the values,we can copy them back to the REPL and so on.It just displays all the attributes for the classand their values.We can configure what attributes we want to show.There's a transient menu where we can toggleif we want to show private attributes, dunder attributes,doc strings, so on, or everything,which is a bit much to show by default.So we'll reset it to the default.In the future, I want to add graphical inspector viewsfor different kinds of objects, and also supportshowing plots in both the inspector and the REPL,but that's future work I haven't started on yet.
Let's look at the different options for evaluating Python.So we can evaluate a whole file.We can evaluate just a class.We can evaluate just the method we're working on.We can evaluate a Python statement,and it will show the result in an overlay next to the cursor.We can select some code and just evaluate the highlighted region.We can sync the REPL to the active file.So now everything we evaluate in the REPL will be in thecontext of the eval_demo module.We can also set the module that the REPL is in.We can go back to main.But let's go back to the eval_demo module for now.
One useful thing is when you update a class or a function,it updates old instances of that class or function.So right now, f.bar is foobar.But if we edit that class, it will actually edit the codefor the old instance of that class.And that's provided by code I copiedfrom IPython's autoreload extension.It helps when you're trying to develop in Pythonwithout having to restart the Python processwhenever you make a change.Auto reload in Python is a big topicthat I don't really have time to go into here,but right now it is more limitedthan what is done in Common Lisp.Like for example, if you have a data class in Pythonand you add a new field to the data class,it won't automatically update old instancesof the data class with a new field.So there's more that needs to be done with that,but I am perhaps naively optimisticthat Python's runtime is quite dynamic and flexible,and that I can fully implement autoreload in Python,but there's still work to be done,and it's a big topic to go into.Next up, let's look at the backtrace buffer.But as it is right now, autoreload is actually useful.I mostly develop in Python without having to restart the processand without running into issues from old statethat hasn't been updated properly.
So if we go on to look at the backtrace buffer,whenever we get an exception in Python...Let's go back to it.Whenever we get an exception, it will...let's change the code so that it actuallygets an exception...we will get an interactive backtrace bufferwhere we can browse the source code for the different stack framesand the local variables within the stack frames,which are all presentations that we can inspect and so on.We can also open a REPL in the context of any stack frame.Or we can, when we go to the source for a given stack frame,we can select some Python code and evaluate itwithin the context of that stack frame.One major limitation compared to SLIME for Common Lispis that in Common Lisp, you have the option torestart or resume execution from a given stack frameafter an exception happens, where in Python,what we have right now is pretty much equivalent tothe postmortem debugger.You can view the state that the call stack was inat the time of the exception,but you can't actually resume execution,which you often might want to do,because when you're coding in a dynamic language,you're going to get runtime errors.So if you're writing a script that does like some sort oflong-running computation or processes a ton of filesand gets an exception parsing one file halfway through,normally you'd have to fix the script, and then rerun itand have it process all the same files all over again,and lose a bunch of time for every bug you run intoand fix you have to make.So right now we've got a kind of mediocre workaroundwhich is you can add the restart decorator to a functionand then... where in the case of a scriptprocessing a bunch of files,you would add the restart decorator to the functionthat processes a single file.You'd add it to the functionthat represents kind of the smallest unit of workthat might fail with an exception,Then, when you get an exception,you can actually edit the function.Like, if we edit it so it doesn't throw an error,and then we can resume execution,then it will return from foo using thethe new version of baz,without having to run the script from the beginning again.So in the example of a script that processes a bunch of files,that would let you,as you run into files that cause an exception,fix your code to deal with itand resume execution without having to restart the scriptfrom the beginning.But this is obviously a pretty terrible hack,having to add the restart decorator to the function.I would like it to be able to restart from any function.without needing the decorator, as you can in Common Lisp,but I think that will require patching CPythonand I really have no idea how to do that.So if you do know anything about CPython internalsand are interested in helping, please reach out.
Another feature we have with the backtrace buffer isthere's this library called PyDumplingwhich can serialize a traceback and store it to a file.So you can use PyDumpling with your applications running inproduction to serialize a tracebackwhenever they have an exception and save it to a file.Then you can transfer the file locallyand load it into your local Emacs with slime-py-load-pydumpling.This will load the same backtrace buffer,and you see all the same local variablesat the time of the exception.You can inspect them and get a REPLin the context of the stack frame.Well, this will only work for variablesthat can be serialized with pickle.Or actually, the library uses dill,which can serialize a bit more than pickle can.But yeah so this can help you inspect and debug errorsfor applications running in production remotelythat you don't want to have SLIME connected to 24-7.
Next up, let's look at the documentation browser.We can bring up documentation for any module,and all this information is generatedfrom runtime introspection,from the doc strings for the moduleand the classes and so on.So you won't see documentation for librariesthat you don't have actually loadedinto your running Python process.Then you can go browse to classes.It'll show all the attributes, their methods, and so on.By each method to the right, it will showthe base class where the method was originally inherited from.You can also bring up a screen with all the Python packagesthat are installed, and browse that with imenu,and bring up information on any package and so on.
Next up, let's take a look at the thread view.So let's run this and then bring up the thread viewand this will show information on all running threads.You can configure it to refresh after a given interval,like every second, but I don't have that set up right now,so I have to manually refresh it.Probably the most useful thing is thatyou can bring up a backtrace for any threadwhich won't pause the thread or anything,but will just give you the call stackat the time you requested the backtrace.You can again view the stack frames, local variables,open a REPL in the context of the thread, and so on.There's also a viewer for async tasks,but I'm not going to demo that right now,because for that to work, you have to start swanky-pythonafter the async event loop has started,from within the same thread.If you go to the project readme,there's a demo of how to use the async task viewerwith a fastapi project.
Next up, let's look at tracing functions.So here we got some random error,because this is still very much a work in progress.But it looks like it executedcorrectly this time.So now let's mark the fibonacci functionfor tracing and execute it.We can see, every time the function is called,all its arguments and return values.Again, there are presentations that we can inspect and so on.But let's inspect a more complex object, like a file object.If we trace the count_lines function and run that code,then we can inspect the file it was passed, or the file object.One pitfall is that in Python, objects are mutable.So in the trace buffer, the string representationthat's printed is the string representationat the time it was passed to the function.But when we go to inspect it,we're inspecting the object as it is right now,which can be different than it was at the timethe function saw it. So for this file object, for example,it's closed now, when it was open at the timethe function used it.
Next up, let's look at AI integrations.So if you're used to SLIME with Common Lisp,Emacs actually has a built-in AI that can help with the transition.So it's just a joke, I actually really like Python.And for more serious AI integrations,I have some ideas for the futurebut I haven't implemented anything yet.I think right now, people are mostly passing source code to LLMsbut since we're embedded in the Python process at runtime,we have a lot of more information available,like maybe we can trace all calls to functions,and when we have a bug,we can feed the trace to the LLM,and the LLM can point out maybewhen this function was called with these arguments,its return value doesn't make sense,so maybe that's the root cause of your bug.If you have any ideas of potential LLM or AI integrations,let me know. I'm happy to discuss.
Next up, let's look at standard LSP-type features.So we've got completions. It's fuzzy completions right now,so it's showing everything with a PR in the name.We can bring up documentation for each one.When we start calling a method in the minibuffer at the bottomit'll show the signature.There's some refactoring available.We can extract a function or variable,or rename something,like, let's rename fib to fib2,and it will rename all the uses of it.All these features are based on Jedi,which is the Python library used by IPython.But as it is right now,if you want the most complete Python development experiencein Emacs, I'd probably recommend using LSPfor everything LSP can do, and then just using swanky-pythonfor the object inspector and backtrace buffer,and the interactive features it hasthat an LSP can't provide.
And that's it really.Shortly we'll have questions and answersas part of EmacsConf, and later on,if you have any questions, ideas, or issuesfeel free to reach out over emailor create an issue on the repository.I should probably warn you,if you want to try out the project:so far I'm probably the only user of itand I've only tested it on my own Emacs setup,so it's quite likely you'll run into issuestrying to get it installed and working.But if you do run into problems, please reach out,let me know. I'm happy to help and try and fix them.So that's it. Thanks for listening.