Is Python slow? We touched on that question with Guido and Mark in the last episode.
This time, we welcome back friend of the show, Anthony Shaw. He's here to share the massive
amount of work that he's been doing to answer that question and speed up things where the answer
is yes. And he just released version one of the Pigeon project. Pigeon is a drop-in JIT compiler
for Python 3.10. It uses the .NET 6 cross-platform JIT to compile and optimize Python code on the fly
with zero changes to your source code. It runs on Linux, macOS, and Windows, both x64 and ARM64.
It's a cool project, and I'm excited Anthony's here to tell us all about it.
This is Talk Python to Me, episode 340, recorded November 3rd, 2021.
Welcome to Talk Python to Me, a weekly podcast on Python. This is your host, Michael Kennedy. Follow
me on Twitter, where I'm @mkennedy, and keep up with the show and listen to past episodes at
talkpython.fm. And follow the show on Twitter via at Talk Python. We've started streaming most of our
episodes live on YouTube. Subscribe to our YouTube channel over at talkpython.fm/youtube to get
notified about upcoming shows and be part of that episode. This episode is brought to you by Shortcut
and Linode, and the transcripts are sponsored by Assembly AI. Anthony Shaw, welcome to Talk By The
Enemy.
Hi, Michael. Good to see you again.
Yeah, it's great to have you back. You're at least yearly appearance here. If not by name,
you know, you have at least appear yourself once. But I think you get mentioned a bunch of times with
all the stuff you're doing.
Yeah, we're just trying to work out when there's the last time. It's like a year, over a year ago.
Yeah, yeah, it was May 2020. See, that was when the whole COVID thing was about to end. Like,
hey, it's just a couple of months. We'll be through this. It'll be fine. Everyone will wear their mask
and get their shots and it'll be totally normal.
I had to pause for a second. I was trying to work out what year it was.
I know. I know. Well, you're in Australia, so it's probably the next year.
2022, yeah.
Yeah, you guys are always ahead by a little bit.
Yeah, exactly.
Awesome. Well, welcome to the show. This time we're here to talk about a project that you've
been spearheading for the last year or so, Pigeon, which is a JIT compiler for Python,
which is pretty awesome.
Yeah, I kind of, it was sitting on the shelf for a few years and I decided to pick it up. It was
related to a thread of things that I've been working on, looking at Python performance. And also
when I kind of finished working on the book, so.
The CPython internals book, yeah.
CPython internals book. Yeah, it was like really interesting to dig into a compiler and I really
wanted to put some of that theory into practice and work on my own compiler. So yeah, that's kind
of what led into it.
It looks like no small feat as people will see as we get into it, but how much did the diving into
the C internals make you feel like, all right, I'm ready to actually start messing with this and
messing with how it runs and, you know, compiling stuff to machine instruction. Yeah.
Yeah, massively. I don't know where I would have started otherwise. It's a pretty steep learning
curve.
Yeah. If someone gave me that job, I'd be like, I have no idea how to do this.
Yeah. I was one of those projects I got started on because it just had my curiosity. I didn't
understand how it could work and yeah, I just really wanted to learn and it just seemed like
a really big challenge. So yeah, I looked at it and thought this is an interesting thing.
I know that Brett and Dino were no longer working on it and just decided to pick it up and see how
far I could take it.
Yeah, absolutely. I think you've taken it pretty far and it's about to go to 1.0. Is that right?
Yeah. So it could be launching version 1.0 of Pigeon in a few days. So I'm waiting for .NET 6 to be
released and I'll explain why probably a bit later.
Wait, wait, wait, wait, wait, wait, wait, wait, wait, hold on, hold on. This is the Python project
on a Python podcast and you're waiting for .NET 6? Oh my gosh, I don't understand.
Yeah, this bit confuses people and it did originally, I think when the project was written. So
it's a JIT compiler for Python written in C++ and you could write your own JIT compiler,
which is no small feat.
Yeah. And then you'd be done in like 10 years.
Exactly. Or you can take a JIT compiler off the shelf. So a JIT compiler compiles some sort of
intermediary language into machine code. So assembly. A lot of JIT compilers are working with basically
like which registers to use, which operations and instructions are supported on different CPUs.
There's a whole bunch of stuff to work on. Very, very involved. And .NET Core, what was called .NET Core,
now just .NET, has a JIT compiler in it. And yeah, you can actually use just the JIT compiler.
So that's what the project originally did was to basically use the JIT compiler for .NET Core.
Other JITs for Python, some of them use the LLVM JIT. And there's a few other JITs as well you can get
off the shelf. So yeah, I started to use the .NET one. That was originally Dino and Brett when they
built this. It was actually before .NET Core was even released. It was still in beta back then,
but the other used .NET Core's JIT.
Yeah. I think it was a much earlier version of .NET Core back then, right? Like it's come a long way.
I think back then, you probably know this better than I do now, but back then I think there was like
a fork in the road to pass for the .NET world. You could do traditional .NET on Windows only,
but that was like the 15-year polished version of .NET. And then there was this alternative,
funky, open source .NET Core thing that sort of derived, but was not the same thing as that.
And so I think probably when Brett and Dino were working on it, it was really early days on that
version of the JIT compiler.
Yeah, it was version 0.9 of .NET Core.
Right.
So yeah, I did actually get involved back then at just helping out, upgrading it from version
0.9 of .NET Core to version one. So I did like help on the original Pigeon project with some of the
builds and stuff, but that early version of Pigeon required a fork of CPython and a fork of .NET Core.
So it required you to compile both projects from source with patches and like stick a whole bunch
of stuff together. And yeah, very tricky to set up. And that's one of the things when I kind of came in
a year ago to pick this project up again, that I really wanted to tackle was let's make it easy
to install this, which means it should just be pip installable. So you can just pip install Pigeon
on Python. And that's the second thing I had to do was to upgrade it to the latest versions of Python,
the latest versions of .NET. So yeah, it's running on .NET 6 and CPython 3.10. So yeah,
it's basically a JIT compiler for Python 3.10.
Right. So now we have .NET is, I think they've closed that fork. It's a little bit like the two
to three boundary that we've crossed. It's just back to this .NET thing and it's open source,
which is cool. So that closes the .NET JIT side of things from a beta thing. On the other hand,
there's a PEP and you'll, I'm sure you know the number. I don't know the number off the top of my head
that allowed for extensions into standard CPython. So you don't have to fork it and reprogram
ceval.c. There's an extensible, an extensibility layer for this kind of stuff now, right?
Yeah. So maybe to backtrack a little bit, but when you write Python code and then execute it
in CPython, which is the most popular Python interpreter, the one you get from Python.org,
when you compile the, well, you don't compile the Python code. Python does it for you.
When you compile Python code, it compiles down into an abstract syntax tree. And then the next level
down is a bytecode sequence, which you can see if you import the disk module and you run the disk
function on your code. I've got to talk about Pidgin, which I did at PyCon this year.
Yeah. We'll link to that in the show notes. Yeah.
Yeah. So it gives some explanations and examples. So that's kind of the bytecode. And yeah, basically
this looks at how can the bytecode gets handed off onto an evaluation loop in Python, which is called
a ceval.
Is that one of the first things you looked at when you started diving into the c source code? Is that
the first file you went to?
I was one of, yeah. It's such a big one. I remember on Python bytes last week, you mentioned that
Lukash had been analyzing bits of Python that changed the most. And that was like the most changed bit of
Python. It's kind of like the brain of Python, really. It's the loop that evaluates all the
instructions and then calls all the different C APIs to actually make your code do things.
And what you can do in this PEP, which Brett proposed when originally when he was working on
Pidgin, was that you can actually tell cPython to not use its own evaluation loop, but use a
replacement one. So this PEP 523 it is, you can basically write an extension module for Python
and then say, okay, from now on, I will evaluate all Python code. Well, Python compiles it for you and
then just gives it to you as a bytecode code object with bytecodes in it. And then you can write a custom
function which will evaluate those bytecodes.
Right. Exactly. Normally there's just a switch statement that says, if I get this bytecode,
here's what I do. If I get that bytecode, here's what I do.
One of the drawbacks of that, that makes it super hard to optimize, among other things, is it's one
statement at a time, right? Like the C of L dot C, that switch statement, that loop doesn't go,
here's a series of possibly related opcodes, make that happen. It goes, no, you need to load this
variable. Now create this object. And there's just not a lot of room for optimization there,
right? You're not going to inline a function or do other types of things when it's, you know,
instruction by instruction.
Yeah, exactly. So what Pidgin does essentially is it implements that API. When you install Pidgin
and activate Pidgin, which you do by importing Pidgin and then just doing Pidgin dot enable,
it will tell CPython that Pidgin will be the function to evaluate all Python code from now on.
And when it sees a new function for the first time, instead of interpreting all the bytecode
instructions, when you execute the function, it will basically interpret those ahead of time and
then compile them into machine code instructions and then store that machine code in memory and then
re-execute it every time you run the function. So it basically compiles the function down into
assembly, essentially, and then puts that assembly object in memory. And then when you run the Python
function, if it's already been compiled, it will then just run those instructions.
Right. As standard JIT stuff, it has to do it once. But then once it's hit one method, it's like,
okay, this one, here's the machine instructions for it. We're just going to reuse that, right?
Yeah, because the computer needs machine code to do anything. So something has to have compiled it
down into machine code. And in the case of normal CPython, CPython is written in C and the C compiler
has compiled that down into machine code. But it's a loop that it runs through for each bytecode
instruction to go, okay, this is a add operator. So, you know, I'm going to take two objects off the
stack, the left-hand side and the right-hand side, and then I'm going to call the add function in the
C API.
Right. Exactly. I kind of got you diving down deep a little too quick. I do want to set the stage just
a moment before we get into how all of this works. Because understanding where you're coming from and
understanding some of the problems you're trying to solve are really going to be helpful to seeing
the value here. So back in, what was it, April of 2020, whatever PyCon was, that virtual PyCon,
the first virtual PyCon, you had that talk called Why Python is Slow. And you talked about some
interesting things that really set the stage for, well, if you had a JIT and you had all sorts of
control over it, as you do, how could it be faster? What could we do, right? So one of the things you
talked about was this in-body problem, how C++ relative to say Python, there was a bit of a
difference there, but also .NET Core was a lot faster. And really importantly, JavaScript was
faster, right?
Yeah. So that was 2019.
My gosh. Okay.
Yeah. This is part of this lovely pandemic. So yeah, I covered the in-body problem, which is
interesting because it's not a, the in-body problem is a mathematical formula that calculates
the position of the jovial planets.
And as soon as you have three or more, it starts to get super complicated, right?
Yeah. It's basically like a big mathematical formula and it just loops through the iterations
to work out the position of different planets. So it's, it's kind of the difference between C is
seven seconds it takes to run the algorithm in C and 14 minutes it takes to run it in Python.
Python's even slower than Perl, which isn't embarrassing.
That's a little embarrassing. Yeah. It's actually pretty much the, the worst case scenario for all
the reasonable languages. Yeah.
In that talk, I dug into the details about why some of the reasons why that is. And kind of the core of
the in-body algorithm is this, is a few lines of code, which basically calculate, look at floating
point numbers and it calculates, does the big calculation. So there's a lot of mathematical
operations. There's like minus divide power add, which is great. And it can all be done in line.
CPUs are very efficient at doing this because CPUs natively understand floating point numbers.
Yeah. But a number in C and a number in Python, these are not equivalent, right? A floating point
number in C is probably eight bytes on the stack. A floating point number in Python is a, what is that?
A PI float object that's 50 bytes and is out on the heap, probably separated in space from the other
numbers in terms of like, it'll cause more cache misses and flushes and all sorts of stuff, right?
Yeah. So a floating point number in Python is a, is an immutable object and it's basically a wrapper
around a double. So yeah, basically you have to create a Python object to store the floating
point number. And then if the value changes, you have to create a new one. So the issue in N body
is that you have to create for one, one line of Python that just does a whole bunch of work to get a
single answer, like a single floating point number, all the interim values in that calculation create
like 18 objects, which are immediately discarded.
Right, right, right. So the, the memory management kicks in just constantly. Yeah.
Yeah. And Python is pretty efficient at allocating small objects, but when you magnify that to the
level that is seen in the N body problem, then yeah, that's why it's so slow effectively because
it's creating all these temporary objects and then destroying them in the next operation.
This portion of talk Python to me is brought to you by shortcut, formerly known as clubhouse.io. Happy
with your project management tool. Most tools are either too simple for a growing engineering team to
manage everything or way too complex for anyone to want to use them without constant prodding.
Shortcut is different though, because it's worse. No, wait, no, I mean, it's better.
Shortcut is project management built specifically for software teams. It's fast, intuitive, flexible,
powerful, and many other nice positive adjectives. Key features include team-based workflows.
Individual teams can use default workflows or customize them to match the way they work.
Org wide goals and roadmaps. The work in these workflows is automatically tied into larger company
goals. It takes one click to move from a roadmap to a team's work to individual updates and back.
Type version control integration. Whether you use GitHub, GitLab, or Bitbucket, clubhouse ties directly into
them so you can update progress from the command line. Keyboard friendly interface. The rest of
shortcut is just as friendly as their power bar allowing you to do virtually anything without
touching your mouse. Throw that thing in the trash. Iteration planning. Set weekly priorities and let
shortcut run the schedule for you with accompanying burndown charts and other reporting. Give it a try over at
talkpython.fm/shortcut. Again, that's talkpython.fm/shortcut. Choose shortcut because you shouldn't have to project manage your project management.
One of the things you can do is maybe understand that there are numbers there and treat them.
Keep, say, the intermediate values as floating points and only return the result to Python.
Like, okay, this Python runtime is going to need a pyint or a pylong or a pyflow or whatever, but we don't need to do all the intermediate steps that way.
We could compute those in a lower level thing because we, again, understand the whole function, not just understand, you know, multiply two numbers,
multiply two numbers, add two numbers, but like that whole thing as a group, right?
Yeah, exactly. So the principle behind some of the design ideas in Pidgin is that, and in lots of other compilers, this is not something I came up with. But the idea is to try and keep things as efficient as possible by just carrying
values on the CPU registers and then not allocating memory in the heap. And a floating point number fits in a CPU register.
So a 64-bit integer or a floating point number fit in a CPU register. So let's just carry those values on the registers and then do low level instructions to do addition and minus and multiplication as well, but not divide.
Because I found out Python has a whole bunch of like custom rules for division.
Yeah.
So I can rely on the CPU instructions to do that.
So these are the types of things that you're like, well, maybe we could use this PEP 523 and some kind of JIT compiler and turn it loose on this.
I just earlier this week interviewed Guido and Mark Shannon about just general Python performance.
They're working on sort of a parallel branch of making Python faster, which is great.
So out of the live stream, Josh Peake asks,
Mark Guido teased the potential of addition of a JIT to CPython 3.13.14.
Would this potentially intersect with that project?
Is this totally separate?
Do you have any visibility there?
Yeah, I haven't asked to be involved in that yet.
I don't know if, I mean, Mark Shannon's experience with compilers is like miles ahead of mine.
And to be frank and, and, and, you know, Guido has invented the language.
So like their knowledge surpasses quite substantially.
Yeah.
I hope that the work done in this project will be insightful when they're designing the JIT.
And I've already spoken to both of them about this project and walk through like what's working and what isn't.
And cause it's, I guess, quite a bit ahead and it's dealing with some challenges, which they're probably going to hit when they come to this.
And then yeah, it will, it will steer, steer them in that direction.
Cool.
Let's talk about compiling just for a bit, because I did a lot of C++.
I remember pressing compile the build button or the run, which would build in a run and you would see it grind.
And actually thinking back, that was when computers actually made noises.
They would like, you know, like their hard drive would like make noises that they are, they, you would hear it compiling even.
Yeah.
Also in C#, .NET compile, but less and much faster.
But in Python, it's just, it runs.
It feels like it just runs.
And I don't remember this compile step.
And yet there is a, there is an aspect of compiling, right?
Yeah, it happens.
You just don't see it.
So yeah, it happens behind the scenes.
It compiles it, but it doesn't compile it into machine code.
It compiles it into bytecode.
Right.
Which is the same as .NET and Java.
But the difference is what happens to that bytecode next, right?
Yeah, it is similar, but the Python bytecode is, is much higher level.
So there's like single operations for just add two objects, for example.
Right.
Or put this thing in a list.
Yeah.
Like add this thing to a list or merge two dictionaries.
It's like dict merge is, is a single bytecode instruction.
Whereas .NET uses a specification.
It's an open specification called ECMA 335.
And this specification describes different stack types.
So it says, you know, there's like a 32 bit integer, 64 bit integer, 16 bit, et cetera.
There's floating point numbers, which come in the form of four byte, eight or four or eight
byte floating point numbers.
And there's also things like Booleans and then how branches and evaluations work.
So it's closer to assembly.
But the reason you don't want to write things in assembly is because assembly is specific to a CPU.
And you often find yourself writing instructions, which would only work on that particular CPU.
And then when you ship it to the real world, like that doesn't work.
Right.
One of the big benefits of JIT is it can look exactly at what you're running on and say,
oh, this has this vectorized hardware thing.
So let's use that version here.
Or this has this type of threading.
So we're going to do some sort of memory management around that.
C and C++ are ahead of time compilers.
They will interpret your code, parse it and compile it down into machine code instructions,
and then put it in a binary format, like a shared library or a standalone executable.
.NET Java and other languages that have a JIT, they have both a compiled VM, which is something
which has actually been compiled into a standalone executable, which is the framework.
So the, you know, the Java.exe, for example.
And then it could compile down the code into an intermediary language and then evaluate that
just in time, and then typically cache the machine code onto a disk or into memory.
And it does that using a JIT.
And Python, CPython interprets everything at runtime, essentially.
So it does cache the bytecode, but it doesn't cache the machine code because it doesn't compile
to machine code.
And that's what Pigeon does.
Right, exactly.
If you've seen PYC files with the Dunderpy cache, right, down in theirs, that's the compiled
output of Python.
But like you said, it's much higher level and it gets interpreted after that.
Yeah.
So part of the insight of Pigeon is like, well, let's take that compile step.
And instead of outputting Python bytecode, what if we output intermediate language bytecode?
Because there's a nice compiler hanging around that can compile that.
If you could somehow feed it that IL instead of PYC content, right?
Yeah.
So the steps are, it's quite involved.
Yeah.
The steps are, and I do go to this in the talk, but Python code, abstract syntax tree, code object,
which has Python bytecode.
And then Pigeon will basically compile Python bytecode into .NET intermediary bytecode.
And then .NET will compile the intermediary bytecode into assembly, into machine code.
And then you attach that to say the function object or a class or something like that, right?
Yeah.
And then that bytecode is, that machine code, sorry, is essentially an executable, which lives
in memory.
And then when you want to call it, you just call that memory address and it runs the function.
Just in the same way that you would load a shared library and just call the address.
Right.
You might have a .so file and you import it and run it.
And as far as you're concerned, magically, it just runs, right?
Exactly.
Okay.
Is it slow?
The compilation step in particular, not necessarily, we'll get to the performance of the overall
system.
But like, is this JIT step, is this a big deal?
Does it take a lot of memory?
What's it like?
Yeah.
I haven't actually focused too hard on the performance of the compilation step because
a lot of the problems that I'm looking at are compile once, execute 50,000 times.
And the overhead doesn't really matter that much.
Although it's pretty fast.
I've been really impressed.
Pigeon is written in C++ and the compilation step is actually pretty quick.
The overhead is 10 to 15% of the execution time on the first pass, depending on the complexity
of the function.
But yeah, like if the function takes a second to run, then, you know, 0.15 of a second is
around how much it'll take to compile it.
Yeah.
That's not bad.
And then it goes faster.
Yeah.
And then once it's done it once, that's it.
With the exception that Pigeon has a feature called profile-guided compilation, which is
kind of something that I designed to get around how dynamic Python is.
So JIT compilers are brilliant when you've got statically typed languages.
So if you know that this variable is an integer and this variable is a string and this variable is
an object, then you can compile all the correct instructions.
But in Python, variable A could be assigned as a string and then changed to an integer and then
you can assign it to the return of a function, which could be anything.
So one of the challenges I kind of looked at was how do you actually make a JIT is only going to be
faster if you've got optimizations and you can't make optimizations if you have to generalize
everything.
Yeah.
So what it does is a feature called PGC, which it will compile a profiling function.
So the first time it runs the Python code, it's basically going to sort of look at what variables
are.
It's almost like you're doing a C profile on itself.
Yeah.
So basically it compiles a function that runs and then when that function is running, it
kind of captures a whole bunch of information about what's actually happening.
And then it makes some assumptions and says, oh, you, when you were adding these three variables
last time, they were all integers.
So let's optimize that for integers next time.
And if they do change, then it depends.
It won't crash.
What happens if they change?
Okay.
A crashing is an option.
You probably don't totally want to go with the crashing part, but that might be a an intermediate,
like we're building it and it's getting dialed in.
Yeah.
Some options that come to mind is you could have a, an alternate compiled version that says,
okay, we've also seen this come as a string and, and so we're going to compile a separate
one and then do like a lookup on the arguments and go from there.
Yes.
So they're called specializations and that's something that Mark Shannon talked about.
And I think when CPython does its own JIT, they will definitely have specializations.
The downside is extremely, and the downside is that you have a lot of memory overhead if there
are lots of specializations.
And a good example would be in the unit test module, the assert equal function.
This is pretty much the first one I kind of slammed into like Pigeon tried to optimize the
assert equal function, which could take anything.
Like it could take two strings, a string and a number.
Yeah.
Probably the first question is, are they the same type or can they be coerced in the same
type?
Yeah.
And then just keep going down like these different cases.
It can't be simple.
Yeah.
And it was actually in a conversation with Guido, he suggested looking at type guards.
So the type guard is, so before I go into the optimized code, it will check to see, has the
variable changed from what it was last time it got profiled?
And then if it has changed type, then it will default back into a generic path.
So that's essentially how it deals with different types.
Yeah.
One of the fall through paths could be just, well, let Python have it, right?
Let Python just run the bytecode.
Yeah.
There are some things that Pigeon doesn't support.
Async and await is one major, major feature.
If it comes across asynchronous generators, then it will just hand them back to Python and Python
executes them.
It should be in there though, right?
I mean, C# and .NET also have async and await.
I know that means quite a bit differently, but theoretically in a future down the road,
when you have more time, maybe it's not completely out of the world possible.
I actually kind of started implementing it and put most of it together, only to realize that
the APIs for asynchronous generators are all private in CPython.
So I can't import them, which makes it technically impossible to implement, which is a bit of a
shame.
But yeah, that's one of the drawbacks at the moment is you can't do async and await.
Right.
But if this were super successful, I can see that that's like, okay, well, let's go ahead and expose
that because Anthony's so close.
Yeah.
They could probably maybe be coerced.
It's like a one line code change, I think.
Yeah.
Now this doesn't apply to the profile guided optimizations, but one of the things that
these frameworks have, you know, I know that .NET has had it at several levels, like they've got this
NGen utility that will take a .NET assembly in IL and you can pre-compile it like ahead of time, compile it
and generate a native image on your machine.
And then Xamarin had that because they had to have something like this to get onto iOS where they
pre like ahead of, they ran the JIT compiler in advance and saved it.
Is that something that could potentially be done here?
Or is it, is it too much?
I watched this space.
I've been researching that.
That's kind of one of the things I've been looking into is, can you compile it down into a
format which can be stored and then loaded or marshaled?
Or can it be stored into a portable executable format or some other binary format?
Lots of security implications there as well.
So that's one thing I'm cautious of.
But yeah, I want to look into.
Yeah, I hadn't even thought about all the challenges you got there.
Yeah, that could be interesting.
But you know, if it comes along that, yeah, this is pretty good, but there's this slower startup.
And I know something that the core devs and, you know, have been very protective of is the startup
speed of Python, right?
That they don't want it to start super slow because often it's, it's sort of run on a little tiny bit of
do this tiny thing and then we're going to drop back and then maybe run Python again on this tiny
thing or even mic, multi-processing, you know, fork it, run these things, drop out of it.
So I'm just thinking of like, how do you protect, like, how do you still achieve that goal and gain these advantages?
Yeah, I think there probably could be work done to make the compiler more efficient.
Also, you can set the threshold of how many times should a function be called before you JIT compile it.
Right.
So that's the threshold setting.
So if you call a function once, there's probably no need to JIT compile it.
Well, there's, there is no need to JIT compile it because, you know, you're compiling and then
just running it straight afterwards.
Whereas if it gets called a lot, then you would, you would want to call it a lot.
So, and that's kind of where you get these sort of things, your hot functions, which is
a function, which is run a lot.
You want to specialize and make more efficient essentially.
So yeah, if you are like sorting a list, for example, then doing comparisons between two
different types, you'd want to make that as efficient as possible.
And that would inherently make sorting algorithms all quicker.
For sure.
Yeah.
So there's multiple stages here, right?
There's the uncompiled code or letting Python run the code.
Then there's compiling it with those hooks to understand what types come in for the specialization.
And then there's the generate, generating the optimized version.
So if it's run once at best, you'll get the unoptimized compiled version and unoptimized
compiled code is probably not that much better, right?
Yeah.
There are some things that have been that it can do to optimize.
For example, there's a list of all the built-ins and it knows what return types the built-ins
have.
So for sure, like it knows if you run list as a built-in function, then it will return a list.
And I have even put in a check if somebody's overridden the list built-in, which is possible
and had to test that as well, which is interesting.
But yeah, it does make a whole bunch of assumptions like that, which is generic and works in most code.
And for example, if you're accessing the fourth item in a list, so you've got a list called
names.
And in square brackets, you put names, square bracket, the number three, then you three is
a constant.
So it can't change.
It's compiled into the function.
If the code knows for sure that names is a list, then instead of calling a C API to see what it is and get the
index, et cetera, et cetera, et cetera.
The JIT compiler can go, oh, I already know this is a list.
I know the index you want is the fourth number.
So instead of calling all the stuff, let's just calculate the memory address of the fourth item
in the list.
And then put in a little check to make sure that there are four items in that list.
And then, yeah, just compile that into the function.
And it's immediately significantly quicker.
Go to where the data is stored in the list.
Go over by the size of four pointers.
So eight times four or something like that.
And just read it right there.
Something like that.
Yeah, exactly.
I mean, for sure that that's what it is, right?
It sounds dangerous, isn't it?
Yeah, exactly.
This is some of the security things, right?
Go over some part in memory and read it and then do something like that.
Sounds like buffer overflow when done wrong.
So I can see why you'd be nervous.
Yeah, exactly.
But that's how compilers work.
They you're dealing with memory addresses, essentially, and low level instructions.
So yeah.
Something we thankfully don't have to do a ton of in Python.
But like in CPython, you're in C, right?
That's a C thing.
Yeah, definitely.
And when you're working with duples, yeah, you do that as well.
So you work out the address of the nth element and then just use that address and increment
the reference counter.
Yeah.
Also, don't forget that or memory management breaks.
This portion of Talk Python to Me is sponsored by Linode.
Cut your cloud bills in half with Linode's Linux virtual machines.
Develop, deploy, and scale your modern applications faster and easier.
Whether you're developing a personal project or managing larger workloads, you deserve simple,
affordable, and accessible cloud computing solutions.
Get started on Linode today with $100 in free credit for listeners of Talk Python.
You can find all the details over at talkpython.fm/linode.
Linode has data centers around the world with the same simple and consistent pricing,
regardless of location.
Choose the data center that's nearest to you.
You also receive 24/7/365 human support with no tiers or handoffs, regardless of your plan size.
Imagine that real human support for everyone.
You can choose shared or dedicated compute instances, or you can use your $100 in credit on S3 compatible
object storage, managed Kubernetes clusters, and more.
If it runs on Linux, it runs on Linode.
If it runs on Linux, you can find the link right in your podcast player show notes.
Thank you to Linode for supporting Talk Python.
Where are you with this?
You said it's going to go to 1.0, which sounds like I could install this and I could run it and
it would do its magic, right?
It's going to 1.0.
Works only on Python 3.10.
That's one big thing.
I've upgraded it from 3.9 to 3.10.
When 3.10 was released, actually, and I won't be backporting it.
It's just so much has changed in Python and the APIs.
You can pip install it on Python 3.10.
You need to have .NET 6 installed when that is released.
Or you can install release candidate 2, which is already out.
Yeah, you can enable it.
Yeah, it sounds like it's pretty close to done, right?
I don't know when it's actually, but they've got their .NET conference in, what is that?
Six days?
Yeah.
Surely.
Yeah, they say it launches then.
It's already online.
By the time this episode out, it's very likely very close to just out.
So, okay, that's a pretty easy thing.
Can I brew install .NET?
Do you know?
I've not tried.
I don't know.
That's a good question.
Yeah.
There is an installer for Mac.
Yeah.
Pigeon also works on ARM 64, which is worth noting.
And it's a very complicated detail of Jet Comparters.
But yeah, ARM support was no small feat, but it's something that people just expected to be there.
Yeah, sure.
Well, I mean, there's obviously the M1s, right?
Everyone with an M1 would like, who wants to do this would really like it to work on ARM.
But there's also Raspberry Pis and other places that Python shows up.
I don't know, helicopters on Mars.
I don't know if that's ARM or not, probably.
Yeah.
So I've tested Linux ARM 64, which will be the Raspberry Pis and other.
And also Mac ARM 64.
I have not tested Windows ARM 64 because there is no Python for ARM 64 on Windows.
Interesting.
Okay.
Steve Dower has released a preview package of the libraries, but not a standalone executable.
But it may come out in the future.
We're going off on a bit of a tangent, but...
No, but that whole story with Windows on ARM is...
I would love to see it better handled.
But it's like you can't even buy it, right?
Is it supported?
It's provided OEMs, but kind of, sort of.
I don't know.
It's in a weird state, right?
It's not normal Windows.
It's not super supported.
Yeah.
I think...
Who knows?
I can't speak...
I do work for Microsoft, so I'm definitely not going to give my opinion.
Yeah.
I'm not asking for your...
This is more of me just making a proclamation.
I have Windows 11 running on my Mac Mini M1, and it is running the ARM.
But to get it, I had to go join the Insiders program and then install it.
And it's permanently got this, like, watermark that it's a tainted version, but you can try
to use it.
And, you know, it works fine, but it's kind of sluggish and whatnot.
So, anyway.
Hopefully, that comes along better.
Yeah.
So, it sounds like it's supported on the various things.
Yeah.
I have .NET 6 installed and Python 3.10 installed.
Neither of those have to be messed with, right?
You've got the PEP for Python, and you've got just the JIT.
Vanilla installations, yeah.
Yeah.
That's beautiful.
And so, give us a little walkthrough on, like, how we might use this.
What do I have to do to change my code to get these compilation steps?
Wherever your code starts running, you need to import Pigeon and then call the enable function
on Pigeon.
Okay.
There's also a config function, which configures different settings in terms of how Pigeon
runs.
Like that threshold, for example, like how many times before you compile.
Yeah.
The hot code threshold optimization level, which is a level between zero and two, which is
like how aggressive the optimizer is.
And you can also enable or disable the profiler.
Yeah.
I remember in C, way back when I was doing C for projects, there were these different optimization
levels.
Yeah.
And it was like, well, you can go to one or two and it'll probably still work.
If you go too far, it'll just crash.
It's like, what is going on?
Like, I don't understand.
But okay, we'll just, we'll dial it back until it's stable.
And that's as fast as we can make it go.
Yeah.
This is the same.
This is my recommendation is go as hard as you can without it catching fire.
Take a step back.
Go with that.
The start, maybe start at zero.
I know it defaults to one.
Yeah.
So yeah, don't turn out to 11, but yeah.
And then the profiler is on by default, which I may disable in the future because the profiler
probably causes the most issues where you've got a function, which ran with integers a thousand
times.
And then all of a sudden somebody gave it some floating point numbers.
It won't crash.
It will just, it will either fall back to default path or it will raise an exception
to say that it got some values, which it didn't expect.
And if you do see that, it's called a, it'll tell you in the error message and it will suggest
that you turn the profiler off and then rerun the code.
You know, for me, I feel like that would suggest to me that maybe I should go check my code.
Yeah.
Not always, but often if I got something like I'm trying to take these things and add them
and get what I think is the result.
And I'm trying to do math, not string concatenation.
And I get a string, chances are that's actually a mistake, not something that I wanted to take
account for.
It could be, but probably not.
Yeah.
I actually saw a useful example of this yesterday on, on Twitter.
Somebody shared adding two floating point numbers in Python, A and B, A plus B is not the
same as B plus A.
They actually give different results, which is crazy.
But yeah, when you work with floating point numbers and, and integers and you, you don't
mean to, but you end up with different types, you will get some weird results anyway.
Interesting.
Yeah.
So yeah, in terms of whether this will work, I've compiled some pretty big libraries and
they've worked fine.
Pandas, Flask, Django, chucked a lot of the CPython's test suite.
Yeah.
This as well.
So yeah, it will run about, I guess to about 50,000 tests or something.
I think quite happily.
That's really awesome.
A lot of CPython tests, testing specific internal things in CPython.
So some of them do fail, but it's not anything that Pigeon's done.
And pytest works as well.
So yeah, there's a lot of big libraries.
NumPy works fine.
I have this test for NumPy, test for Pandas, test for Flask, Django, all the stuff that I'd
expect people to try is in there.
If you're working with a lot of C extension modules, they also do work.
Cython extensions work.
So in terms of like compatibility, that was one of the main things I wanted to focus on
was instead of going super aggressive with the optimizations, I just want to make sure
this works with existing code because there are lots of other projects which.
Right.
We have PyPy already, P-Y-P-Y, which is also a JIT compiler.
And it works in not with the .NET backing the JIT, but some of like the hot functions getting
compiled versus just running in Python.
That kind of stuff is pretty similar now, but they made the big trade-off like we're going
to just go all in on compiling and we're going to not necessarily integrate with the C APIs
in the same way, which means breaking with some of these things like NumPy or Pandas that
people sometimes care about.
Yeah.
And they also have to play catch up on the language features.
So if there's new features in 3.9, 3.10 in the language, like new operators or whatever,
then PyPy has to then go and implement that, which is hard.
But Pidgin is, you load it into CPython.
So like in terms of language, it would be exactly the same.
Right.
Often a lot of those language features are just syntactic sugar over existing things,
right?
Yeah, exactly.
And then if there's anything which is not compatible, like I mentioned, async and await, then it will
default back to CPython.
And that transition is seamless.
And you wouldn't, you won't notice it will just, it will just run the code regardless.
Awesome.
So it looks like, I'll say it works for the most part.
I haven't totally tried it, but it sounds like it works quite extensively.
The way you use it is you pip install Pidgin and then just import Pidgin, Pidgin.enable
is option one.
And then that's it, right?
There's nothing else that you have to do.
Nothing else you have to do.
You just run the Python code and it all just automatically spots stuff that it should compile
and compile it for you.
Fantastic.
And then another option I see on the page here is I can say Pidgin space some Python
file and not necessarily modify the file, but tell it to execute.
What does it do?
Import Pidgin, Pidgin.enable, eval, something like that.
Yeah, basically it's a very small script.
So yeah, Pidgin is a standalone command that you can run instead of, so instead of running
Python, you run Pidgin against a script or a module and all the arguments should work as
normal.
Awesome.
You also have the dash M for built and stuff, right?
Yeah.
So if you want to run a script, then you'd run Pidgin and then the name of the script.
If you want to run a module like pytest, for example, then you would do Pidgin dash pytest
and it would run pytest with the JIT enabled.
Yeah.
Fantastic.
Or Flask or something like that, right?
Yeah, exactly.
Yeah.
So I guess the dash M would work with external libraries, right?
Long as like Python can see them.
Yeah.
And I've shipped a whiskey extension as well so that you can use it in whiskey apps.
I think that that's an interesting use case, actually.
So when I run my regular Python code, it just loads up and runs.
But when I run Flask or FastAPI or Django or Pyramid or whatever, there's all sorts of
layers of indirection or layers of not directly running it, right?
In production, you would say, hey, I want Microwiskey or G Unicorn to run this.
Like for FastAPI, it would be, I want G Unicorn to run this, but with UVicorn workers and run
five of them and like, boom, boom, boom.
Now you've described like this chain of events, right?
Yeah.
But it sounds like there's, what, middleware to make this work still?
Yeah.
It's a whiskey middleware that is for Pidgin, which will do the enabling and disabling.
Fantastic.
So that sounds like it works for any whiskey app, Flash, Django, Pyramid, et cetera.
What about ASGI apps?
Well, due to the lack of async and await support, then no.
It doesn't really make much sense, right?
Because like the big thing that it does is like not the thing that's supported, right?
Yeah.
I mean, if it's an async function, then it will just give it back to CPython.
I'm sure there's a lot of synchronous things happening in those various places, right?
Maybe the view method itself is async, but it might call a whole bunch of, you know,
give me the headers and the cookies synchronously.
Who knows?
Yeah, exactly.
It also depends on the nature of the program as to whether Pidgin's actually going to make
a difference to their performance.
Yeah.
So that's kind of where I'm up to at the moment is different benchmarks and running Pidgin
against some standard benchmarks.
I shared the N-body execution time at my PyCon talk, and that was 33% faster.
It's now 65% faster.
So I've doubled that.
Oh, nice.
Gain.
So, however, most people aren't calculating the position of planets.
But the few who are, they'll be super thrilled.
Yeah.
For the few people who are and are doing it in Python, the system doing it in Python.
Yeah.
Then they'll be delighted.
So there are, so code, which is doing a lot of math and is in pure Python would be faster
up to 20% fast, 20 times, not 20%, 20 times faster.
I've got some micro benchmarks that do like simple calculus and stuff like that.
And they're 20 times faster with floating point numbers.
And for, I'll say, small integers, because an integer in Python, an int in Python is a
called a...
It's an unbounded thing.
It's bounded by your memory, right?
Yeah.
It's actually a list of digits.
It's not...
Yeah.
So it can have like an almost infinitely large number inside it.
Whereas CPUs work with 32-bit or 64-bit numbers.
And the other languages, instead of keep growing, they just go like, we broke.
So instead of going one more, it goes like negative 2 billion.
Yeah.
You get funny overflows and stuff.
Yeah.
So one of the challenges I've had with Pidgin is trying to optimize integers, but trying to
understand where it potentially could be a very big number and where the number is like
five.
What's five times five?
You don't need to allocate half a meg of memory to do five times five.
Yeah.
So that's one of the challenges.
So if you're working with integers and you're working with floating point numbers and you're
doing a lot of math and Pidgin will make a dramatic difference.
There's also a feature called the graph, which will create .graphviz files for the functions
that it's compiled.
And you can see this on the website.
So if you go to live.trypidgin.com.
You've got this interactive live website, right?
Yeah.
So I've kind of made a sort of live demo site where you can type Python code in and click
compile and then it will show you.
Oh, don't change it, Michael.
You broke it.
It's going to be fine.
And then do you compile?
It's going to be fine.
I got faith in you.
Now I can delete it.
It catches fire.
Okay.
That's the assembly that it has compiled that Python code into.
Okay.
And a fun thing I actually added was there's a comment in assembly, which says which byte
code this is for, which is fun.
If you scroll down on the page, you see this graph.
I got to do my screen.
Press the IL and then keep going down.
Okay.
There we go.
Here we go.
I don't have a big enough screen there.
Here we go.
Okay.
So this instruction graph gets generated if you enable graphing.
It's on the documentation.
But when you enable graphing, it will show you all the Python byte codes.
And then what values are being sent between those byte codes.
So this is basically the output of the profiler.
It can see here that you've got A and B.
B was a float.
A is an integer.
And then it's doing a multiplication operation.
And it knows that if you multiply a float by an integer, then the output is a float.
So it carries that value through the graph.
PyPyde actually does things quite similar to this.
And then once the profiler has run, it will look at the graph and then make some decisions
about which values don't need to be Python objects.
So for example, A times B.
Right.
There's these intermediate binaries here and stuff, right?
This in-place add, all those, yeah?
So yeah, exactly.
So all of those values do not need to be Python objects.
So what it will do on the next pass is it will recompile that.
And then it will, what's called, unbox them.
So it basically just carries them in CPU registers as integers and floats.
And then instead of running the C function to add two numbers together, it will just emit the assembly instruction to add two numbers on the register.
Yeah, that's fantastic.
That's what you're talking about where you don't have to drop back into allocating Python numbers if you know for sure that no one's going to look at it.
It's just an intermediate value.
And this is where it gets tricky with integers, right?
Because A and B might be small, but A to the power of B might be larger.
Exactly.
And then it goes one step beyond as well if you have code that uses fast values.
It's tricky because when you do eval on the website, it will never have used fast locals.
But if you do have a function that has fast locals, if it detects that that local is only ever used in places where it can be unboxed,
then it won't actually store the variable as a Python object either.
It will store it as a native stack value.
So that's something it even does will, like if you have a variable called A and you assign it to the number two,
then it will actually just reserve an area in memory just to store that stack value.
And it will be an offset.
Which is way more efficient.
Enormous, like thousands of times more efficient.
And when you refer to that variable in your function, it will basically just access that point in memory to get the actual value.
Yeah.
Awesome.
This is really neat.
One thing that stands out to me here is I wrote name equals Anthony plus Shaw as two strings.
And that was, you're like, don't do it.
And yet what I see in the graph here is that it loads the string Anthony Shaw.
So did Python look at that and then decide that that's actually, those are two constants.
So we'll just make it one constant or was that .NET or what happened there?
Yeah.
That's constant folding is a feature of Python.
Yeah.
That's what I thought.
If you do, yeah, two strings and a plus, it will actually compile those into one string.
You'd never, you'd never see it.
Oh, interesting.
Because they're statics, right?
Like it knows both of them.
Yeah.
Okay.
Very interesting.
So it sounds like for numerical stuff, this is quite a bit faster.
Did you do any tests on the web frameworks?
I mean, it's kind of appealing to say, what if I could get Flask to kind of run natively?
Yeah, I have.
Because when you look at what, when you think about I'm writing this code to make this website go, most of what you're doing is you're doing like a little tiny bit of code on top of a big framework that's doing most of the heavy lifting, right?
So if you could make Flask do that magic faster or Django or whatever.
Yeah.
So the areas where Python is faster as numerical work, similar to PyPy, PyPy is a lot faster with numerical work.
Yeah.
It can make clear and simple assumptions and optimize based on the CPU.
So that's brilliant.
Areas where it's, it's sometimes it's not faster or sometimes even slower is code, which is just uses a lot of classes and very small functions.
Partly just because of the way the PEP is designed, it will JIT compile functions.
And if your functions are just working with custom classes and you're passing things around, then trying to decide what type things are and then how it can optimize types.
Yeah.
If everything is a Python, custom Python object, custom Python class, there's very little it can actually do to optimize.
Yeah.
And when you were talking about the specializations earlier, it's one thing to say, well, I'm passing a customer and an order object, but then they have fields themselves, each of which have potential types, right?
Like it's this, this object graph, this closure of all the object graphs.
And you got to look at all those types you might touch, right?
Yeah.
And then you've also got to check that fields exist.
It's been set.
It's not none.
I mean, like if you Django.
By the time you're done testing them all, you might as well just run it.
By the time you've done all of that, you've basically just written what CPython would have done anyway.
Yeah.
But the difference is that if you JIT compile it, you've got to emit all those instructions.
And the compiled function, the JIT compiled function ends up being bigger because, you know, if it's compiled in C, it just has one function that does that, that's shared by all libraries.
Whereas in the JIT, you have to make it so that it's their standalone functions.
So that's one downside is that if you're working with stuff which is similar to Django and Flask, I guess like lots of classes, lots of variables, which are all custom types, probably not going to see a performance improvement or potentially it could even be slower.
Will it be transitive?
If I write code that runs in Flask, let's just pick one of them.
And I run Flask with Pigeon.
Will that then make all my code that Flask is executing also run in Pigeon?
Yeah.
So maybe if I was doing like highly computational stuff in my Flask app, having to do that might be worthwhile.
Yeah, definitely.
And in that case, you can just enable Pigeon before those functions or you can set the threshold to be higher.
That's probably what makes more sense, I think.
Yeah.
The other area is strings.
So if you're doing a lot of work with strings, I haven't done any work on optimizing strings.
And I'm not particularly sure what you would optimize either because they're so complicated.
Because you're dealing all with Unicode, different encodings, different bit lengths.
Yeah, I don't even know how you would improve upon CPython's string implementation, to be honest.
Yeah, there's a lot of nuances to strings in all the languages about it, especially in Python, because you don't have to think about it, right?
The fact that you don't know how complicated Unicode is and, you know, word alignment and null characters and so on and so on.
Yeah, if you want a glimpse of how complicated it is, look at the Unicode object source file in CPython.
Is it big?
It's an absolute monster.
I bet it is.
It's probably more complicated than C of L, actually.
It's...
Oh my goodness.
Yeah.
All of that for emojis.
For emojis.
Okay, I understand there's other languages.
No, interesting.
So one thing I wanted to ask you about here, I was hunting around on the screen for it, is it's cool that you can compile these things down and run them as native machine instructions,
instead of piping them through C of L.C as single line operations.
That's great.
But when I think of the stuff that compilers can do and JIT compilers, it's a lot about the optimizations.
It's like, I saw this function and this function.
Actually, we're going to inline that one.
And this one, we're going to rewrite this as some other thing that's going to be more efficient because we're seeing that.
And so you do have some optimizations, right?
Like that's part of the magic.
Yeah.
So I've kind of documented them as best I can on the Pigeon documentation.
There's a section called built-in optimizations.
And I've given them all numbers.
And if it uses that optimization, it will flag the function to say, oh, I'd use this optimization on this function.
And then in the documentation, I'll explain what's the background.
What was the idea?
What difference does it make?
You want to give some examples from some of these, like the is one, for example?
Yeah.
So if you're using the is operator in Python, so if something is false, or you actually probably use not something.
Is none or something like that.
Yeah, is none.
Then it won't run the pyis function.
It won't run the C API to do an is comparison.
It will just look at the address of both objects and see if they're the same.
Because is actually asking, are these objects the same, not are they equivalent, right?
So the same in CPython is the pointer addresses are equal.
Yeah, exactly.
It will just compile that down to a simple pointer comparison.
That's actually one of the first ones I wrote, and it was good to learn.
Also, when it's doing comparisons for small numbers.
So Python inter, it kind of immortalizes numbers between, I can't remember the range.
Negative five and 256?
Yeah, that's it.
Because they use so much that...
Maybe 255, but basically.
It's around that many.
It immortalizes them, and then that keeps them as constant objects so that they're just reused.
So if you create a new integer with the number one, it will just reuse the same number one that it used before.
Right.
Because they're not created on the stack.
They're like complicated things on the heap.
So yeah.
Exactly.
So if you do, if something equals equals 25, Pidgin will go, oh, well, I know 25 is an intern number.
So I'm actually just going to do a pointer comparison instead of a value comparison if the left-hand side is a number.
So that they have to sort of like little things like this, which make small differences, but when you add up...
They add up, right?
Yeah.
So I felt like, I don't know where you are now from where you were when you spoke at PyCon,
but I feel like this is an area that people could come contribute to.
This is an area for growth that doesn't require a lot of changes.
It could be super focused.
But if you see this situation, you know, here's one more way to make it faster.
Absolutely.
So yeah, this is one area where I was hoping that the research that I was doing whilst writing Pidgin could contribute to other projects.
And I'm going to, I've been talking to Carl Friedrich-Osch as well, who works on PyPy.
And we're going to do some pair programming at some point to see kind of like how different projects work and stuff like that.
But kind of hopefully these optimizations can be learned from and then used when CPython gets to implementing its JIT.
Yeah.
Surely the .NET compiler has all sorts of optimizations in it.
Are you already taking advantage of those to some degree?
Yeah, some of them.
Just by nature of letting it run, basically.
Yeah, some of those optimize it.
It does a lot of them already.
And I've been working with the compiler team on the .NET project as well.
Pidgin is one of the only projects to use the compiler directly.
Even though it can be used in that way, it's, it wasn't, it was really, I think it was designed to be run directly, but it's not like advertised as an off the shelf JIT.
So yeah, there aren't many projects that are using it in that way.
And I do work with the compiler team, like specific test cases and stuff as well.
You do have that advantage of being on the inside at Microsoft, even if you're like half a world away, you still can get direct access remotely, which is like being down the street these days.
Yeah, I've done everything.
It's all been via GitHub though.
So I think as far as they're concerned, I'm just another name on GitHub, but like I don't, it doesn't.
They might not even know, right?
I probably didn't even know.
Yeah.
That I work.
Yeah.
Let's see.
What else?
I think we pretty much have covered it given the time that we got.
I mean, another area that's interesting is how does it compare to the other things that have been done before it or going along a parallel?
And, you know, you do have a whole section on the read me on the GitHub page.
Like how does this compare to X, Y, Z, PyPy, Piston, Numba, Iron Python, and so on.
Do you want to maybe just have a quick statement about that?
Yeah, I think it's really hard to compare them.
So probably the most obvious ones to compare it with would be Cite and Numba and PyPy.
So PyPy is a Python interpreter that has a JIT.
So it interprets, compiles, and runs Python code written in Python.
PyPy has been around.
Right.
It's like a fork of Python that was rewritten to behave differently rather than PEP523, right?
Yeah, exactly.
So it's not Cite Python is written in C.
PyPy is written in Python and probably significantly faster in many cases.
It's a very mature project.
But obviously that there's limitations around C APIs and some things don't work in PyPy.
Numba is a JIT.
It's a JIT specific for NumPy.
If you're using NumPy, then if you use Numba, Numba can JIT compile NumPy data arrays and stuff like that.
That's actually a very specific use case.
Right.
That's great.
But if you're not doing NumPy, then not too much.
In any other use case, it would make very little, if any, difference at all.
And then Siphon is not a JIT.
It's an AOT compiler.
And it's a way of annotating Python code with concrete types.
And then it compiles them into C.
Concrete compiles them into C extensions.
Yeah.
It's a little bit like what you're talking about, trying to understand what are these types and then can we create a specialization.
But it's the developer who just says, no, these are integers.
This is the list.
For sure.
Go with it.
Yeah.
That's the point where you actually specify the length of the integer as well.
So in Siphon, you would say this is a 64-bit integer so that it knows that it can be converted into a 64-bit integer in C.
Pidgin is probably the closest to Siphon.
But obviously with Siphon, you have to compile it to a C extension ahead of time.
Whereas with Pidgin, you just import it into Python and just turn it on and it just runs and compiles live.
And it will, yeah, you don't have to have different compiled versions of your app or your library, not necessarily because of this anyway, for different platforms, right?
And you don't have to annotate the code in this sort of special syntax either.
Yeah.
Well, let's close this out with a question on that.
So in Python, we've been able to say optionally that this thing is an integer, you know, x colon int, or it's an optional integer or it's a customer or whatever the heck it is.
We've had these type annotations and traditionally they've meant nothing, right?
Except for if you run tools against them.
On the other hand, we've had things come along like Pidantic, like FastAPI that look at that and go, you know what?
I'm going to do something with that because I got a string because this is the web and that's all I usually get.
I'm going to make that into an int because you said it's an int.
Is there a scenario where type annotations play into this to enhance it somehow?
I'm against that idea, potentially.
I mean, that's how Siphon works.
Yeah.
I think, and with type annotations as well, having a type checker is great until the types are wrong and it's not a fault of your own.
And having work on like strongly typed languages, like C#, C# is brilliant, except when you need to do things like reflection.
So let's say you're working with JSON data or YAML, for example.
And working with JSON and YAML in C# is incredibly hard.
Because you're like, oh, I've had to do parsing YAML files in Java.
It's incredibly difficult because you're like, oh, well, this could be a list of strings or it could be a dictionary where the key is a string, but sometimes it's a number.
Which is like YAML is like that sometimes.
And JSON is like that.
It's completely free.
So when you're working with dynamic content using strongly typed languages is extremely difficult.
It just gets in the way and just makes your life harder.
And the compiler just complains and just won't let you parse because it's saying, oh, well, that's not compliant with my view on the world.
With Python, I think it's cool to say, okay, if I just tag this as an integer, then it should be able to optimize it for integers.
And I think that's what Mark was suggesting.
If it stops there, that's fine.
I think if it goes beyond that, then that's where things get very, very complicated.
And it just becomes a thing that's in the way being noisy and it slows you down.
That's where I have a strong disagreement with it is that we use Python because it's fast to develop in.
And it's fast to develop in because, you know, if you know what you're trying to get it to do, then the compiler doesn't really give you any grief.
If it's syntactically, it's okay.
I'll just try and run it.
Yeah.
And I think the thing that's most similar to this is TypeScript.
And my experiences with TypeScript have been often met with frustration.
I think it's really cool that you can write.
The language specification is really neat, but I want to use this library and that thing and pull them together.
And if that thing doesn't have the type specified just right, I can't get JavaScript to pop out the other side because the transpiler gives you a compiler error.
Like, this doesn't work right.
Well, they know it's going to work right.
Just let this little part go through.
I just, I don't want to re...
I know there's ways to say this is just any and stuff, but still.
I feel like my experience was bumping up against that stuff, even though it's kind of the same as Python type annotations in a lot of ways.
Yeah.
And under the hood, it's still dynamic.
Yeah.
Yeah, exactly.
At runtime, it would still be the same thing, but you won't get to the runtime because you can't get the stuff compiled.
Yeah.
Or it does really weird things at runtime that at compile time, it made assumptions that wouldn't happen.
All right.
Let me throw one quick thing out there before you pass final judgment on this type thing.
Because you're right.
I could write a thing that says my function takes an int and a string, and I could run my pi against my code.
And sure enough, it only takes ints and strings in these situations.
But if it's a library, all bets are off.
There's nothing that says people who use your library are going to run my pi and listen to that or do anything, right?
They could write in Notepad if they had no self-respect, but that would like you could write whatever you want, right?
And then you could just feed it on and then you'd have these problems.
That said, over on the Pydantic world, there is a validator, a decorator called at validate arguments that will make sure at runtime, they really are a string in an int.
Yes.
To make you feel any better?
Probably not, because the transitive closure of objects is too complicated to check and describe.
The validate arguments is, I think, it's a convenience function.
It's there because you often have to validate data, user-provided data that's coming in from an interface, like a web interface, or via a file, or an API of some sort.
So if people are submitting user-provided data, then you want to have to validate the types.
And that is just a lot of boilerplate code, and it's annoying to write.
As a convenience function, let's write this thing.
Right.
Probably because we're going to immediately try to convert it in the next thing, right?
Yeah.
Yeah, exactly.
So I think it's quite different to using it internally to make changes in terms of how the code is compiled.
Yeah.
For numbers, like I said, numbers and strings and base types, I think is cool.
But for objects, like which fields do you assume exist?
Yes, exactly.
And then do they all have to match the types or just the two that you touch?
Yeah.
You've got an attribute there.
Like sometimes it has a getter and a setter.
Sometimes it's just an attribute.
Sometimes it's a descriptor that inside the descriptor is the thing you actually want.
Is that the right type?
Yeah, you're right.
It's insane.
It's just, oh yeah.
Once you kind of open, I think that once you open custom types, then it just kind of mushrooms
into this.
Sure.
And that's why you see things like Cython and stuff having like little fragments of here's
some limited code that we're going to use, often fundamental types.
Yeah.
Cool.
All right, Anthony, this is really awesome.
I could tell you've done a massive amount of work on it.
That's more stuff I want to talk to you about, but I think we're out of time, like calling
into .NET, stuff like that.
But we'll save that for some other time, huh?
Yeah.
Until you have a little more progress.
Now, you know, there's always the two questions at the end of the show.
So if you're going to write some code, what editor are you using these days?
VS Code.
Right on.
Yeah.
And notable IPI library.
I mean, it could be Pigeon.
Anything else?
It's like you come across like, wow, this is awesome.
Yeah.
I think I mentioned it before.
Tortoise, I'm a big fan of at the moment.
Yeah.
Tortoise ORM.
Yeah.
It's a nice async ORM.
Yeah.
And Beanie as well.
I'm really enjoying playing with Beanie as an ODM.
Yeah.
Async ODM on top of Mongo.
Yep.
Beanie is very cool.
I'm actually having Roman Wright on the show talk about Beanie not too far out as well.
And Beanie is cool because it's basically Pydantic plus async and await on top of MongoDB, which
is cool.
Can I have three?
You can have three.
I released like a small package called Hathi, which is a SQL attack tool for Postgres, MySQL, and SQL Server.
I'm looking at your GitHub profile here.
You've got one of these fancy profiles with the new readme that shows up and all of your stars.
Look at this.
Yeah.
Like I made a custom graphic for everything.
Yeah, you did.
That's fantastic.
Okay.
What is this one called?
Hathi.
H-A-T-H.
I have too many repositories.
I need to go up.
Hathi.
Got it.
It is a dictionary attack tool for Postgres, MySQL, and MSSQL, Microsoft SQL Server designed for internal testing, of course.
Don't be bad.
Yeah.
So don't break the law.
And yeah, I've been using it to test like internal, not internal stuff, but test environments and look at like bad passwords.
See if like an admin has a login, like password, password one or admin or super user has a login or whatever.
Yeah.
It was also a test of like I think an await networking and how fast I could make it.
It can do up to 120 login attempts a second.
So yeah, on my machine.
But if you maybe this is a four core Mac, but yeah, if you had a few more CPUs, you could probably get a bit more than that.
But yeah, it'll go through a few thousand passwords.
And there's a password list in there as well of about 10,000 common database passwords.
Yeah.
Nice.
So yeah, just make sure your password is not on the list.
And if it is, you can raise a pull request to remove it.
Yeah.
You don't want to have your database open.
Like it doesn't get much worse than I just have read write access to your database.
But that's what this would test for, right?
Yeah.
So you can scan a cluster or a machine and see if it can predict what the username password is.
Yeah.
Cool.
All right.
Well, those are three great recommendations.
It's awesome to have you back on the show.
And congratulations on the work here.
You know, final call to action.
You know, people maybe to try Pigeon.
You want them to contribute.
Got other ideas.
What do you say?
Yeah.
Go to trypigeon.com.
Yeah.
On trypigeon.com, you'll see at the top, you can try it out live.
Link to documentation.
Link to download.
Yeah.
I'd love more contributions or discussion, I think, around what optimizations you could have and people using it and checking it out.
And if you have any issues, you just raise them on GitHub and I'll check them out.
All right.
Fantastic.
Well done.
I can tell this is a ton of work.
So you've come really a long ways.
And congrats on 1.0 when it comes out in a few days.
Thanks, Michael.
Yeah.
You bet.
See ya.
This has been another episode of Talk Python to Me.
Thank you to our sponsors.
Be sure to check out what they're offering.
It really helps support the show.
Choose Shortcut, formerly Clubhouse.io, for tracking all of your project's work.
Because you shouldn't have to project manage your project management.
Visit talkpython.fm/shortcut.
Simplify your infrastructure and cut your cloud bills in half with Linode's Linux virtual machines.
Develop, deploy, and scale your modern applications faster and easier.
Visit talkpython.fm/Linode and click the Create Free Account button to get started.
Do you need a great automatic speech-to-text API?
Get human-level accuracy in just a few lines of code.
Visit talkpython.fm/assemblyai.
Want to level up your Python?
We have one of the largest catalogs of Python video courses over at Talk Python.
Our content ranges from true beginners to deeply advanced topics like memory and async.
And best of all, there's not a subscription in sight.
Check it out for yourself at training.talkpython.fm.
Be sure to subscribe to the show.
Open your favorite podcast app and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes,
the Google Play feed at /play,
and the direct RSS feed at /rss on talkpython.fm.
We're live streaming most of our recordings these days.
If you want to be part of the show and have your comments featured on the air,
be sure to subscribe to our YouTube channel at talkpython.fm/youtube.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Now get out there and write some Python code.
Thank you.
Bye.
you
Thank you.