Okay, so welcome to this session about interactive Python
programming. My name is David Vujic and I live and work in
Stockholm, Sweden. a developer and today I focus
mainly on Python software development. So I do this at work
and I also do this on my spare time in my open source projects.
Before that, I've been part of the Lisp community. I've
been a Clojure developer, and also, like, way back,
I was in the Microsoft world and developed C# and .NET stuff.
What I've been doing lately is to try to improve the
developer experience when you write Python code. So what I
want to talk about is this, but also I want to begin with
feedback loops because I think it's very related to this
interactive programming style, like having this nice
feedback when you write code.
So I'm going to begin with that.
So this image, you know, this circle is supposed to be a
visualization of a feedback loop. Let's say we write our
code and then we deploy it to production. Then when it's
running there, we can check if things work, or if maybe someone
else will let us know. Maybe our customers will let us know.
That's a pretty slow feedback loop with potential risks of
damaging your business or whatever.
This is obvious, of course.
So a faster feedback loop probably is to have
some kind of automation when you do commits
or maybe you have this pull request things and even reviews.
So maybe not always as fast as deploy,
don't deploy directly to production, but
it's probably safer and often you get this automated
feedback faster anyway. But it's still kind of slow. You
have to wait. You have to push things to GitHub maybe and
wait. So there's faster ways for sure to get feedback.
So a much faster way is to write code,
and write some unit tests, and run those unit tests.
So then you do everything on your local machine
and you will fairly quickly learn if your code does
what you think it does or if it doesn't. I want to zoom in to
this test write code and test flow a bit. Let's do that.
As a developer, I have used a thing called test-driven
development for quite some time. I find that this way of
working is very fast when it comes to getting feedback on
what your code does and how you should continue the
development. So, test-driven development,
basically that you start writing a test for
something that you want to develop, and then you continue
developing that, and then you go back to the test, and modify
and modify the code, and you go back and forth between the
tests and the code.
It's sort of like a ping-pong game. I find this very
effective when you want to get feedback and to know how to
continue the development. The most important thing
that I feel is that you know what the code does.
You learn very quickly.
Let's zoom into this TDD flow a little bit. The last couple of
years, I've been doing a slightly different thing which is
called REPL-driven development. REPL-driven
development is very similar to test-driven development,
but I find it even quicker. You get feedback even quicker
than with a regular TDD setup. So REPL-driven development
is about writing and evaluating code in a REPL basically.
And you can do experiments and you can refactor and
re-evaluate and you get instant feedback on what the code
does and what you need to change. So I think that's even
faster than test-driven development.
Okay, REPL driven development. Let's go back. What's the
REPL? Most of developers know what a REPL is. The most common
setup is you open this shell and you use the REPL for your
programming language. In this case I'm using the Python
REPL or the IPython REPL which is an enhanced REPL for Python
development. So what happens here is that we start a REPL
session in isolation. So this session knows about the
Python environment. So it knows about the Python language
basically. So as soon as we start writing things, adding
variables or creating writing functions or even doing
imports. Then the session will be more and more aware of the
code so we will add things to the to the session and then that
means that we can run functions we can print out these
variables and things like that. But with REPL driven
development it's not really that well at least not what I
mean with REPL driven development. So what I'm thinking of
is that you are in your code editor where you have your
autocomplete, and you have your syntax highlighting and
your favorite theme, color theme, and all of those things. But
instead, you have this running REPL in the background or in a
smaller window or buffer. So that means that you write code
and you can send that code to the running REPL, to the REPL
session. You write and do everything as you would do when
writing your code basically. In this case, in this
example, I have evaluated these two functions. I've sent
them to the REPL session so it's aware of these functions.
Then I switched to a separate different module and
evaluated that one. So the REPL session now knows about
these two functions and also these two variables. That
means that I can evaluate the state of those variables and
change code and re-evaluate and things like that. So in this
example if you look in the smaller area there you see that I
have evaluated this res variable on line 6 and the output was
that it's a dictionary with two keys and two values
basically. So this setup works in basically any of your
favorite code editors. So you can do this in Visual Studio
Code, you can do this in PyCharm or Vim. But what I have done is
that... More like what I have missed is that when I write code
and do this evaluation, this is really cool, but then I need
to switch context if I want to see the result. I have to switch
context to this other window. I
have my focus on the code and then I have to look in a different
place to know the results. And if it's a larger output, then
maybe I need to scroll. So I wanted to find out if it was
possible to make this even smoother and faster, this
feedback loop even faster, so I don't have to switch
context. What I've done here is that... I can select a row or a
region and I can evaluate and then an overlay, a small pop-up
shows up with the evaluated result right next to it. So I can
change code and re-evaluate and quickly see the result of it
without doing this context switching. So the way I've done
it is that I wanted to reuse the existing tooling that I
already had. I know that my in-editor REPL, the IPython
REPL, already does this evaluation. So I figured maybe I can
extract the data and do this visualization as a separate
thing. That's how I've done it. What I've done is that
I've created this overlay and placed it where my cursor
currently is, right next to the code. Then I've
extracted the evaluated result and put it in this overlay.
I also want this overlay to have this nice looking syntax,
so I've set it to this Python mode, so we get this syntax
highlighting. Make it look very readable. And as a nice
developer experience thing,
when you move the cursor, of course you don't want the
overlay to be there. You want it to disappear. So those kinds
of things I've added. So putting the overlay at the right
place and feed it with the evaluated data and then make it
disappear when it's not interesting to look at anymore.
What I've described so far is something that I use on a
daily basis, and it covers most of my needs while doing Python
development. But one thing I still miss, and I miss it from my
days as a Clojure developer, because over there we could
have a running app on our local machine and we can have our
editor, and the app and the editor were connected. So when I
did some changes in the code, the app would change without
any restarts or anything like that. And the same if I would
change the state of the app, I can inspect the state from the
code. So they were connected. They are connected. So I was
thinking, hey, this would be really cool if we could have
something like this in Python. And that reminded me of
Jupyter and Jupyter notebooks because I think notebooks,
the way you do things there, is very similar to what I was
trying to achieve. So I was reading up a little bit on how this
notebook thing works. It turns out that a notebook is a
client that talks to a server, that communicates with a
server. It's on the server that all this Python
evaluation and all this thing happens. Then what I've
done is that instead of starting up IPython in my editor, I
start the Jupyter console instead. And then I can give it
that unique ID and it will be connected to that running
kernel.
In this example, I've created this FastAPI CRUD app that
has this create, read, update, and delete endpoints. It
has this, it's locally running, it has this database where
you can do all these things. I'm running this FastAPI app
in the kernel and then I've connected to, I've connected to
the kernel in my editor too. Both of them are connected to
the kernel. What I do now is that I want to initially create
some data. I'm going to add this, creating this message.
What I get back is a message ID. I want to experiment in
my browser. What do I get with that message ID? I'm
evaluating the read function. I instantly get this
evaluated result, which was this hello world text. So what
happens if I do some changes in this app? I'm going to grab
this message ID and write something else.
Now I can evaluate the same thing again, and you can see that
the content has changed to this new value. My editor isn't
in any debug mode or something like that. It doesn't know
what database it is. It doesn't have any environment
variables set up or something like that. It is only
connected to the kernel, and the kernel is aware of that. It's
running the app. It has the connection strings and
everything that is needed. So that's how this thing works.
Now I want to do some inline hacking because I want to store
this input that is sent from this app because I want to work
with it afterwards. I can add this dictionary that stores
this message. I'm updating the source code of this app, and
when I run any of these endpoints again, you will see that
the state changes, and the new inputs, I can grab and I can use
them for quick evaluation or testing. This example is
really simple. It was just an integer. For example, if you
are sending a more complex object, maybe a pydantic schema
or something, and you want to inspect what's coming in, and if
you have some sort of validation that you want to test out.
The configuration or the code that I wrote to make this work
is a little bit different than just adding an overlay. I'm
using this overlay just like with the IPython example, but in
this case, when I change code, I have to think about where that
code lives, because it's the app that runs the code. So it's
in the app context I need to manipulate with the data. If you
have started the app from maybe a main function and that
module imports namespaces, then you need to, if you want to
update a function or something like that, you need to update
it in the correct namespace. What I did before in IPython
by adding and changing things, everything ends up in the
global namespace. But here, if you want the app to actually
react to the changes, you need to put it in the right
namespace. So that's what I do here. I do some lookups, where
is this function, and then I do this reload of this function or
module. And when I was developing this, I was thinking, hey,
this is really ugly. I'm in this REPL and do some
manipulation of the imports and things like that. That
didn't feel good. Then I was reminded of the IPython. And
IPython has this feature to reload any updated
submodules. I was curious how do they do it. I looked in the
IPython source code and saw that they also use importlib and
reloading of this module. Once I've learned that, then I
stopped thinking that my code was hacky. I thought it was
good enough at least.
But one thing that has bothered me for a long time is I quite
often want to test out and evaluate individual rows that
lives in a function. Quite often, this code uses the input
to that function like the input parameters. To be able to
do that, I need to manually type some fake data and set it to
this variable, and then I can evaluate the code. But I think
that takes... That slows me down. I was thinking, maybe I can
do this in a quicker way, so I have this quicker feedback, so I
can run this or evaluate this code much quicker.
So my idea was maybe I
can use an LLM for this. If I give it the parameters, maybe it
can return some random data so I don't have to write it
myself. I ended up doing that. I have this source code.
I'm loading the REPL with the code. Then I select this
function name and the parameters with its data type. I
have this prompt that instructs the LLM to come up with fake
data based on the tag name and on the data type. And then I can
send that to the REPL. I do that with a key command. Then
I can proceed by running the code within the function that
uses these inputs. This works for all the data types. If
there's a custom data type, you need to give the LLM extra
context. So that's something to think about. Once it knows
the context, it can generate this fake data that very often is
good enough just to test out, you know, like I've done here, like
string... sorry, list destructuring and parsing and things
like that. I think that was all I had, and thank you for
listening!