It's an exciting time for the capabilities of Python.
We have the Faster CPython initiative going strong,
the recent async work, the adoption of typing,
and on this episode, we discuss a new isolation
and parallelization capability coming to Python
through sub-interpreters.
We have Eric Snow, who spearheaded the work
to get them added to Python 3.12
and is working on the Python API for them in 3.11,
along with Anthony Shaw, who's been pushing the boundaries
of what you can already do with sub-interpreters.
This is Talk Python to Me, episode 446,
recorded December 5th, 2023.
Welcome to Talk Python to Me, a weekly podcast on Python.
This is your host, Michael Kennedy.
Follow me on Mastodon, where I'm @mkennedy,
and follow the podcast using @talkpython,
both on fosstodon.org.
Keep up with the show and listen to over seven years
of past episodes at talkpython.fm.
We've started streaming most of our episodes live on YouTube.
Subscribe to our YouTube channel over at talkpython.fm
slash YouTube to get notified about upcoming shows
and be part of that episode.
This episode is sponsored by PyBites Developer Mindset Program.
PyBytes' core mission is to help you break the vicious cycle
of tutorial paralysis through developing real-world applications.
The PyBites Developer Mindset Program will help you build the confidence
you need to become a highly effective developer.
And it's brought to you by Sentry.
Don't let those errors go unnoticed.
Use Sentry.
Get started at talkpython.fm/sentry.
Anthony, Eric, hello, and welcome to Talk Python.
Great.
Hey, guys.
It's really good to have you both here.
You both have been on the show before, which is awesome.
And Eric, we've talked about sub-interpreters before,
but they were kind of a dream almost at the time.
That's right.
Now they feel pretty real.
That's right.
Yeah, it's been a long time coming.
And I think the last time we talked, I've always been hopeful, but it seemed like it was getting
closer.
So with 312, we were able to land Per Interpreter Gill, which kind of was the last piece, the
foundational part I wanted to do.
A lot of cleanup, a lot of work that had to get done.
But that last piece got in for 312.
Excellent.
So good.
And maybe let's just do a quick check-in with you all.
It's been a while.
You know, Anthony, start with you, I guess.
Quick intro for people who don't know you, although I don't know how that's possible.
And then just what you've been up to.
Yeah.
I'm Anthony Shaw.
I work at Microsoft to lead the Python advocacy team.
And I do lots of Python stuff, open source, testing things, building tools, blogging, building
projects, sharing things.
You have a book, something about the inside of Python?
Oh, yeah, I have a book as well.
Yeah?
I forget about that.
Yeah.
Yeah, there's a book called CPython Internals, which is a book all about the Python compiler
and how it works.
And you suppress the memory of writing it, like it was too traumatic.
It's down there.
I keep forgetting.
Yeah, that book was for 3.9.
And people keep asking me if I'm going to update it for 3.13, maybe, because things keep changing.
Yeah.
Things are definitely changing at a more rapid pace than they were a few years ago as well.
So that maybe makes it more challenging.
Yeah, recently, I've been doing some more research as well.
So I just finished my master's a few months ago and I started my PhD.
And I'm looking at parallelism in Python as one of the topics.
So I've been quite involved in subinterpreters and the free threading project and some other
stuff as well.
Awesome.
Congratulations on the master's degree.
That's really great.
Thanks.
And I didn't realize you were going further.
So Eric.
Eric Snow.
So I've been working on Python as a core developer for over 10 years now.
But I've been participating for even longer than that.
And it's been good.
I've worked on a variety of things, a lot of stuff down in the core runtime of CPython.
And I've been working on this, trying to find a solution for multi-core Python since, really,
since 2014.
So I've been slowly, ever so slowly, working towards that goal.
And we've made it with 3.12 and there's more work to do.
But that's a lot of the stuff that I've been working on.
I'm at Microsoft, but don't work with Anthony a whole lot.
I work on the Python performance team with Guido and Grant Booger and Mark Shannon here
at Kedrill.
And we're just working generally to make Python faster.
So my part of that has involved sub-interpreters.
Awesome.
Interestingly enough, it's only really this year that I've been able to work on all the sub-interpreter
stuff full-time.
Before that, I was working mostly on other stuff.
So this year has been a good year for me.
Yeah, I would say.
That must be really exciting to get the, like, you know what?
Why don't you just keep, just do that?
That'd be awesome for us.
Yeah, it's been awesome.
Well, maybe since you're on the team, let's a quick check-in on FasterCPython.
It's made a mega difference over the last couple of releases.
Yeah, it's kind of interesting.
Mark Shannon has a, definitely has a vision.
He's developed a plan as of, like, years ago.
But we finally were able to get him, put him in a position where he could do something
about it.
And we've all been kind of pitching in.
A lot of it has to do with just applying some of the general ideas that are out there regarding
dynamic languages and optimization.
Things have been applied to other things like HHVM or various of the JavaScript runtimes.
And so a lot of specialization, adaptive specialization, a few other techniques.
But right now, so a lot of that stuff we're able to get in for 3.11.
In 3.12, there wasn't quite as much impactful stuff.
We're kind of gearing up to effectively add a JIT into CPython.
And that's required a lot of kind of behind-the-scenes work to get things in the right places.
So we're somewhat targeting 3.13 for that.
So right now, I think with where things are at, we're kind of break-even performance-wise.
But there's a lot of stuff that we can do.
A lot of optimization work that really hasn't even been done yet that'll take that performance
improvement up pretty drastically.
It's kind of hard to say where we're going to be.
But for 3.13, it's looking pretty good for at least some performance improvement because
of the JITing and optimization work.
That's exciting.
Yeah, we have no real JIT at the moment, right?
Not in CPython.
Yeah.
I mean, I know there's number.
In Anthony, yeah, it is best.
I know.
Well, that's actually super exciting because I feel like that could be another big boost,
potentially.
You know, with the JIT, you come to all sorts of things like inlining of small methods and
optimization based on type information and, yeah.
Yep.
All that stuff.
One of the most exciting parts for me is that a lot of this work, not long after I joined
the team, so two years ago, two and a half years ago, somewhere in there, pretty early
on, we started reaching out to other folks, other projects that were interested in performance
and performance of Python code.
And we've worked pretty hard to collaborate with them.
So like the team over at Meta, they have a lot of interest in making sure Python is very
efficient.
And so we've actually worked pretty closely with them and they're able to take advantage
of a lot of the work that we've done, which is great.
Yeah.
There seems to be some synergy between the sender team and the faster CPython team.
So awesome.
But let's focus on a part that is there, but not really utilized very much yet, which is
the sub-interpreter.
So back on, when is this?
2019.
Eric, I had you on and we talked about, can sub-interpreters free us from Python's gil?
And then since then, this has been accepted, but it's Anthony's fault that we're here.
Because Anthony posted over on Mastodon, hey, here's a new blog post, me running Python parallel
applications with sub-interpreters.
How about we use Flask and FastAPI and sub-interpreters and make that go fast?
And that sounded more available in the Python level than I kind of realized the sub-interpreter
stuff was.
So that's super exciting, both of you.
Yeah, it's been fun to play with it and try and build applications on it and stuff like
that.
And working with Eric probably over the last couple of months on things that we've discovered
in that process, especially with C extensions.
Daytime?
Yeah.
Yeah, that's one.
With C extensions.
And I think that some of those challenges are going to be the same with free threading as
well.
So it's how C extensions have state, where they put it, whether that's thread safe.
And as soon as you kind of open up the possibility of having multiple gills in one process, then
what challenges does that create?
Absolutely.
Well, I guess maybe some nomenclature first.
Not no-gil Python or sub-interpreter Python.
Free-threaded.
Is that what we're calling it?
What's the name?
How do we speak about this?
It's not quite settled, but I think a lot of people have taken to referring to it as
free-threaded.
I can go with that.
I mean, people still talk about no-gil, but free-threaded is probably the best bet.
Are you describing what it does and why you care?
Or are you describing the implementation, right?
Like the implementation is it has no gill, so it can be free-threaded, or it has sub-interpreters,
so it can be free-threaded.
But really what you want is the free-threaded part.
You don't care actually about the GIL too much, right?
Kind of.
Well, it's interesting.
With sub-interpreters, it really isn't necessarily a free-threaded model.
It's kind of free-threaded only in the part at which you're moving between interpreters.
So you only have to care about it when you're interacting between interpreters.
The rest of the time, you don't have to worry about it.
Where with the no-gil is more kind of what we think of as free-threading, where everything
is unsafe.
And for people who don't know, the no-gil stuff is what's coming out of the sender team
and from Sam Gross.
And that was also approved, but with the biggest caveat I've ever seen on an approved pep.
Sure.
Like, we approve this.
We also reserve the right to completely undo it and not approve it anymore.
But it's also a compiler flag that is an optional off-by-default situation.
So it should be interesting.
Yeah, we can maybe compare and contrast them a bit later as well.
Yeah, absolutely.
Well, let's start with what is an interpreter.
So then how do we get to subinterpreters?
And then what work did you have to do?
I heard there was a few global variables that were being shared, Eric.
Yeah.
Maybe let's give people a quick rundown of what is this and how is this new feature in 3.12
changing things?
Yeah.
Subinterpreters, in a Python process, when you run Python, everything that happens, all
the machinery that's running your Python code is running with a certain amount of global state.
And historically, you can think of it as, you know, across the whole process.
You've got a bunch of global state.
If you look at all the stuff like in the sys module, sys.modules or sys.whatever, all those
things are shared across the whole runtime.
So if you have different threads, for instance, running it, they all share that stuff, even though
you're going to have different code running in each thread.
So all of that runtime state is everything that Python needs in order to run.
But what's interesting is that the vast majority of it, you can think of as the actual interpreter.
And so that state, if we treat it as isolated and we're very careful about it, then we can
have multiple of them.
That means that when your Python code runs, that it can run with a different set of this
global state, different modules imported, different things going on, different threads that are
unrelated and really don't affect each other at all.
And then with that in mind, you can take it one step farther and say, well, let's completely
isolate those and not even have them share a gill, right?
And then at that point, that's where the magic kind of happens.
So that's kind of my first goal in this whole project was to get to that point.
Because once you get there, then it opens up a lot of possibilities when it comes to
concurrency and parallelism.
Yeah.
Then Anthony can start running with his blog posts and showing off good things.
Yeah, absolutely.
This portion of Talk Python to Me is brought to you by the Pybytes Python Developer Mindset
Program.
It's run by my two friends and frequent guests, Bob Belderbos and Julian Sequira.
And instead of me telling you about it, let's hear them describe their program.
In a world where AI, machine learning and large language models are revolutionizing how we live
and work, Python stands at the forefront.
Don't get left behind in this technological evolution.
Tutorial paralysis?
That's a thing of the past.
With Pybytes Coaching, you move beyond endless tutorials to become an efficient, skilled Python
developer.
We focus on practical, real-world skills that prepare you for the future of tech.
Join us at Pybytes and step into a world where Python isn't just a language, but a key to unlocking
endless possibilities in the tech landscape.
Check out our 12-week PDM program and embark on a journey to Python mastery.
The future is Python.
And with Pybytes, you're one step ahead.
One thing I don't know the answer to, but might be interesting, is Python has a memory management
story in front of the operating system, virtual memory that's assigned to the process with pools,
arenas, blocks, those kinds of things.
What's that look like with regard to sub-interpreters?
Does each sub-interpreter have its own chunk or set of those for the memory it allocates,
or is it still a shared one thing per process?
It's per interpreter.
This is something that was very global.
And like you pointed out earlier, this whole project was all about taking all sorts of global
state that was actually stored in C global variables all over the place, right?
And pulling those in together into one place and moving those down from kind of the process
global state down into each interpreter.
So one of those things was all of the allocator state that we have for objects.
And Python kind of has this idea of different levels of allocators.
The object allocator is what's used heavily for Python objects, of course, but some other
state as well.
And the object allocator is the part that has all the arenas and everything, like you were
saying.
Yeah.
So part of what I did before we could make the GIL per interpreter, we had to make the
allocator state per interpreter.
Well, the reason I think that it's interesting asking about it, one, because of the gill, obviously,
but the other one is, it seems to me like these sub interpreters could be used for a little
bit of stability or isolation or run some kind of code.
And when that line exits, I want the memory freed.
I want models unloaded.
I wanted to go back to the way it was.
You know what I mean?
Whereas normally in Python, even if the memory becomes free, right, it's still got some of
that, like, well, we allocated this stuff.
Now we're holding it to refill it.
And then, you know, you don't unimport modules and the modules can be pretty intense, actually,
if, you know, if they start allocating a bunch of stuff themselves and so on.
What do you guys think about this as an idea, as an aspect of it?
Yeah, there's one example that's been coming across recently.
And this is a pattern.
I think it's a bit of an anti-pattern, actually.
But some Python packages, they store, like, some state information in the module level.
So an example is a SDK that I've been working with, which has just been rewritten to stop
it from doing this.
But you would put the API key of the SDK, you would import it.
So you'd import X and then do, like, X dot API key equals.
So it basically stores the API key in the module object, which is fine if you've got,
you've imported the module once and you're using it once.
But what you see is that if you put that in a web application, it just assumes that everyone
uses the same key.
So, you know, you can't have, you can't import that module and then connect to it with different
API keys, like you'd have different users or something.
Right.
So you had some kind of multi-tenancy, right?
Where, like, they would say, you know, enter their ChatGPT, open AI key, and then they could
work on behalf of that, right?
Like, potentially something like that, right?
Yeah, exactly.
So that's kind of like an API example.
But there are other examples where, let's say you're loading data or something and it's,
and it stores some temporary information somewhere in like a class attribute or even like a
module attribute like that.
If you've got one piece of code loading data and then in another thread in a web app or
just in another thread generally, you're reading another piece of data and they're sharing state
somehow and you've got no isolation.
Some of that is due to the, the way that people have written the Python code or the extension
code has kind of been built around, oh, we'll just put this information here.
And they haven't really thought about the isolation.
Sometimes it's because on the C level, especially that because the GIL was always there, they've
never had to worry about it.
So, you know, let's, you could just have a counter, for example, or there's an object, which is a
dictionary that is a cache of something.
Um, and you just put that, as a static variable, and you just read and write from
it.
You've never had to worry about thread safety because the girl was there to kind of protect
you.
You probably shouldn't have built it that way, but it didn't really matter because it worked.
Um, what about this, Anthony?
What if we can, if we can write it on one line, it'll probably be safe, right?
If we can fit it just one line of Python code, it'd be okay.
Yeah.
Dictionary.ad.
What's wrong there?
Dictionary.get.
It's fine.
Yeah.
So yeah, what we're saying within sub interpreters, I think what's the concept that people will
need to kind of understand is where the, where the isolation is, because there are different
models for running parallel code.
And at the moment we've got co-routines, which is asynchronous.
So it can run concurrently.
So that's, if you do async and a wait, or if you use the old, co-routine decorator,
um, you've also got things like generators, which are kind of like a concurrent pattern.
Um, you've got threads that you can create.
Um, all of those live within the same interpreter and they share the same information.
So you don't have to, if you create a thread inside that thread, you can read a variable
from outside of that thread.
Um, and it doesn't complain.
Um, you don't need to create a lock at the moment.
Um, although in some situations you probably should.
Um, and you don't need to reimport modules and stuff like that, which, yeah, which can
be fine.
And then at the other extreme, you've got multi-processing, which is a module in the standard
library that allows you to create extra Python processes and then kind of gives you like an
API to talk to them and share information between them.
And that's kind of like the app, the other extreme, which is you've got on it is the, you
know, the ultimate level of isolation.
You've got a whole separate Python process.
Um, but instead of interacting with it via the command line, you've kind of got this
nicer API where you can almost treat it like it's in the same process as the one you're
running from.
Yeah.
It's kind of magical actually that that at all where you get a return value from a process,
for example.
Right.
Yeah.
Um, but the thing is, if you peel back the covers a little bit, then like how it
sends information to the other Python process involves a lot of pickles.
Um, and it's not particularly efficient.
And also a Python process has a lot of extra stuff that you maybe necessarily didn't even
need.
Like you get all this isolation from having it, but you have to import all the modules again.
You have to create the arenas again, or the memory allocation.
You have to do all the startup process again, which takes a lot of times, like at least 200
million seconds.
The Python code again, right?
At least the PYC.
Yeah.
Yeah, exactly.
So you basically created like a whole separate Python.
Um, and if you do that just to run a small chunk of code, then it's not probably the best
model at all.
Yeah.
You have a nice graph as well that shows like sort of the rate as you add more work and you
need more parallelism.
We'll get to that.
I'm sure.
Um, you know, one thing that struck me coming to Python from other languages like C, C++,
C#, there's very little locks and events, threading, coordinating stuff in Python.
And I think that there's probably a ton of Python code that actually is not actually thread
safe, but people kind of get away with it because the context switching is so coarse grained,
right?
Like you say, well, the gills there.
So you only run one instruction at a time, but like this temporary invalid state you enter
to as part of like, just your code running, like took money out of this account.
And then I'm going to put it into that account.
Those are multiple Python lines and there's nothing saying they couldn't get interrupted between
one to the other.
And then things are busted.
Right.
I feel there's, there's some concern about adding this concurrency, like, oh, we're having
to worry about it.
Like you probably should be worrying about it now.
Not as much necessarily, but it's, I feel like people are getting away with it because
it's so rare, but it's not, it's a non-zero possibility.
What do you guys think?
Yeah.
I mean, those are real concerns.
It's as there's been lots of discussion with the no-go work about really what, what matters,
what we need to care about really what impact it's going to have.
And I mean, it's probably going to have some impact on people with Python code, but it'll especially have impact on people that maintain extension modules.
Yeah.
But it really is all the pain that comes with free threading.
that's what it introduces with the benefits as well, of course.
But what's interesting, I'd like to think of sub interpreters kind of provide the same facility, but they force you to be explicit about what gets shared and they force you to do it in a thread safe way.
So it's, you can't do it without thread safety.
And so it's not an issue and it doesn't hurt that people really haven't used sub interpreters extensively up till now, whereas threads are kind of something that's been around for quite a while.
Yeah, it has been in, well, it's sub interpreters have traditionally just been a thing you can do from C extensions or the C API, which really limits them from being used in just a standard.
Like I'm working on my web app, so let's just throw in a couple of sub interpreters, you know, but in 313, is that when we're looking at having a Python level API for creating, interacting with?
Yeah, I've been working on a PEP for that PEP 554 recently created a new PEP to replace that one, which is PEP 734.
That's the one.
So that's the one that I'm targeting for 313.
And I don't know, it's pretty straightforward.
Create interpreters and kind of look at them and with an interpreter, run some code, you know, pretty basic stuff.
And then also because sub interpreters aren't quite so useful if you can't cooperate between them.
But there's also a queue type that, you know, you push stuff on and you pop stuff off and just pretty basic.
So you could write something like await queue.pop or something like that.
Excellent.
Yeah.
So yeah, this is really cool.
And the other thing that I wanted to talk about here, looks like you already have it in the pep, which is excellent.
Somehow I missed that before.
Is that there's an, we have thread pool executors, we have multi-processing pool executors, and this would be an interpreter pool executor.
What's the thinking there?
People are already familiar with using concurrent features.
So if we can present the same API for sub interpreters, it makes it really easy because you can set it up with multi-processing or threads and switch it over to one of the other pool types without a lot of fuss.
Right.
Basically, with a clever import statement, you're good to go, right?
From whatever import, like multi-processing pool executor as pool executor or interpreter pool executor as pool executor, and then the rest of the code could stay potentially.
Yeah.
What I expect-
The communication, like you gotta, it's gotta kind of be a basic situation, right?
Because there are assumptions.
Yeah.
And it should work mostly the same way that you already use it with threads and multi-processing.
But we'll see.
There's some limitations with sub interpreters currently that I'm sure we'll work on solving as we can.
So we'll see.
It may not be quite as efficient as I'd like at first with the interpreter pool executor, because we'll probably end up doing some pickling stuff, kind of like multi-processing does.
Although I expect it'll be a little more efficient.
This portion of Talk Python to me is brought to you by Sentry.
You know Sentry for the air monitoring service, the one that we use right here at Talk Python.
But this time, I want to tell you about a new and free workshop.
Heaming the Kraken, managing a Python monorepo with Sentry.
Join Salma Alam Nayour, senior developer advocate at Sentry, and David Winterbottom, head of engineering at Kraken Technologies, for an inside look into how he and his team develop
deploy and maintain a rapidly evolving Python monorepo with over 4 million lines of code that powers the Kraken utility platform.
In this workshop, David will share how his department of 500 developers, who deploy around 200 times a day, use Sentry to reduce noise, prioritize issues, and maintain code quality without relying on a dedicated Q&A team.
You'll learn how to find and fix root causes of crashes, ways to prioritize the most urgent crashes and errors, and tips to streamline your workflow.
Join them for free on Tuesday, February 27th, 2024 at 2 a.m. Pacific time.
Just visit talkpython.fm/sentry dash monorepo.
That link is in your podcast player show notes.
2 a.m. might be a little early here in the U.S., but go ahead and sign up anyway if you're a U.S. listener, because I'm sure they'll email you about a follow-up recording as well.
Thank you to Sentry for supporting this episode.
I was going to save this for later, but I think maybe it's worth talking about now.
So first of all, Anthony, you wrote a lot about and have actually had some recent influence on what you can pass across, say, the starting code and then the running interpreter that's kind of like the sub-interpreter doing extra work.
Want to talk about what data exchange there is?
Yeah.
So when you're using any of these models, multiprocessing, sub-interpreters, or threading, I guess you've got two or three things to worry about.
One is how do you create it in the first place?
So how do you create a process?
How do you create an interpreter?
How do you create a thread?
The second thing is how do you send data to it?
Because normally the reason you've created them is because you need it to do some work.
So you've got the code, which is when you spawn it, when you create it.
The code that you want it to run, but that code needs some sort of input, and that's probably going to be Python objects.
It might be reading files, for example, or listening to a network socket.
So it might be getting its input from somewhere else.
But typically you need to give it parameters.
Now, the way that works in multiprocessing is mostly reliant on pickle.
If you start a process and you give it some data, either as a parameter or you create a queue and you send data down the queue or the pipe, for example, it pickles the data.
So you can put a Python object in.
It uses the pickle module.
It converts it into a byte string and then it basically converts the byte string on the other end back into objects.
That's got its limitations because not everything can be pickled.
And also some objects, especially if you've got an object which has got objects in it and it's deeply nested or you've got a big complicated dictionary or something that's got all these strange types in it which can't necessarily be rehydrated just from a byte string.
An alternative, actually, I do want to point out because for people who come across this issue quite a lot, there's another package called Dill on PyPI.
So if you think of pickle, think of Dill.
Dill is very similar to pickle.
It has the same interface, but it can pickle slightly more exotic objects than pickle can.
So often if you find that you've tried to pickle something, you try to share it with a process or a sub interpreter and it comes back and says, this can't be pickled.
You can try Dill and see if that works.
So, yeah, that's the typical way of doing it is that you would pickle an object and then on the other end, you would basically unpickle it back into another object.
The downside of that is that it's pretty slow.
It's equivalent.
Like if you use the JSON module in Python, it's kind of similar, I guess, to converting something into JSON and then converting it from JSON back into a dictionary on the other end.
Like it's not a super efficient way of doing it.
So sub interpreters have another mechanism.
I haven't read PEP 734 yet.
So I don't know how much of this is in the new PEP, Eric, or if it's in the queue.
But there's a...
It's much the same.
Okay, it's much the same.
So there's another mechanism with sub interpreters.
Because they share the same process, whereas multiprocessing doesn't, they're separate processes.
Because they share the same process, you can basically put some data in a memory space, which can be read from a separate interpreter.
Now you need to be...
Well, Python needs to be really careful.
You don't need to worry too much about it because that complexity is done for you.
But there are certain types of objects that you can put in as parameters.
You can send either as startup variables for your sub interpreter, or you can send via like a pipe, basically, backwards and forwards between the interpreters.
And these are essentially all the immutable types for Python, which is like string, unicode strings, byte strings, bool, none, integer, float, and tuples.
And you can do tuples of tuples as well.
And it seems like the tuple part had to...
Something that you added recently, right?
It says, I implemented tuple sharing just last week.
Yeah, that's in now.
I really wanted to use it.
So I thought, well, instead of keep...
I kept complaining that it wasn't there.
So I thought instead of complaining, I might as well talk to Eric and work out how to implement it.
Yeah, that's awesome.
But yeah, you can't share dictionaries.
That's one thing.
Yeah, exactly.
So one thing that I thought that might be awesome, are you familiar with message spec?
You guys seen message spec?
It's like Pydantic in the sense that you create a class with types, but the parsing performance is quite a bit, like much, much faster.
80 times faster than Pydantic, 10 times faster than Marshuro and Seatters and so on.
And faster still even than, say, JSON or UJSON.
So maybe it makes sense to use this, turn it into its serialization formats, bytes, send the bytes over and then pull it back.
I don't know.
It might give you a nice structured way.
You can share bytes strings.
So you can stick something into Pickle or you can use like msgspec or something like that to serialize something into a bytes string and then receive it on the other end and rehydrate it.
Or even Pydantic.
Like Pydantic is awesome as well.
This is meant to be super fast with a little bit of less behavior, right?
Yeah.
So this is kind of a design thing.
I think people need to consider when they're like, great, I can run everything in parallel now.
But you have to kind of unwind and think about how you've designed your application.
Like at which point do you fork off the work?
Yeah.
And how do you split the data?
You can't just kind of go into it assuming, oh, we'll just have a pool of workers and we've kind of got this shared area of data that everybody just reads for.
Yeah, I'll pass it a point or two of million entry list and I'll just run with it.
Yeah, because I mean, in any language, you're going to get issues if you do that.
Even if you've got shared memory and it's easier to read and write to the different spaces, you're going to get issues with locking.
And I think it's also important with free threading.
If you read the spec or kind of follow what's happening with free threading, it's not like the gills disappeared.
The gills been replaced with other locks.
Yeah.
So there are still going to be locks.
You can't just have no locks.
Especially cross threads, right?
Like it moves some of the reference counting stuff into like, well, it's fast on the default thread, the same thread.
But if it goes to another, it has to kick in another more thread safe case that potentially is slower and so on.
Yeah.
So yeah, the really important thing with sub interpreters is that they have their own GIL.
So each one has its own lock.
So they can run fully in parallel just as they could with multi-processing.
So I feel like a closer comparison with sub interpreters is multi-processing.
Yeah, absolutely.
Because they basically run fully in parallel.
If you start four of them and you have four cores, each core is going to be busy doing work.
You start them, you give them data, you can interact with them whilst they're running.
And then when they're finished, they can close and they can be destroyed and cleaned up.
So it's much closer to multi-processing.
But the big difference is that the overhead both on the memory and CPU side of things is much smaller.
Separate processes with multi-processing are pretty heavyweight.
They're big workers.
And then the other thing that's pretty significant is the time it takes to start one.
So starting a process with multi-processing takes quite a lot of time.
And it's significantly, I think it's like 20 or 30 times faster to start a sub interpreter.
You have a bunch of graphs for it somewhere.
There we go.
Yeah.
So I scrolled past it.
There we go.
It's not exactly the same, but kind of captures a lot of it there.
So one thing that I think is exciting, Eric, is the interpreter pool, sub interpreter pool.
Because a lot of the difference between the threading and the sub interpreter performance
is that startup of the new arenas and like importing the standard library,
all that kind of stuff that still is going to happen.
But once those things are loaded up in the process, they could be handed work easily, right?
And so if you've got a pool of, you know, like say that you have 10 cores,
you've got 10 of them just chilling or however many, you know,
you've sort of done enough work to like do in parallel,
then you could have them laying around and just send like,
okay, now I want you to run this function.
And now I want you to run this.
And that one means go call that API and then process it.
I think you could get the difference between threading and sub interpreters
a lot lower by having them kind of reuse basically.
Yep.
Absolutely.
There's some of the key difference,
I think is mostly that when you have mutable data,
whereas with threads, you can share it.
So threads can kind of talk to each other through the data that they share with each other.
Whereas with sub interpreters, there are a lot of restrictions.
And I expect we'll work on that to an extent,
but it's also part of the programming model.
And like Anthony was saying,
if you really want to take advantage of parallelism,
you need to think about it.
You need to actually be careful about your data and how you're splitting up your work.
I think there's going to be design patterns that we come to know,
or conventions we come to know,
like, let's suppose I need some calculation,
and I'm going to use it in a for loop.
You don't run the calculation if it's the same over and over.
Every time through the loop, you run it,
and then you use the result, right?
So in this, you know, a similar thing here would be like,
well, if you're going to process a bunch of data,
and the data comes from, say, a database,
don't do the query and hand it all the records.
Just tell it, go get that data from the database.
That way it's already serialized in the right process,
and there's not this cross serialization to either pickling
or whatever mechanism you come up with, right?
But like, try to think about when you get the data,
can you delay it until it's in the sub process,
or sub interpreter rather, and so on, right?
Yeah, definitely.
One interesting thing is that pep734,
I've included memory view as one of the types that's supported.
So basically, you can take a memory view of any kind of object
that implements the buffer protocol,
so like numpy arrays and stuff like that,
and pass that memory view through to another interpreter,
and you can use it, and it doesn't make a copy or anything.
It actually uses the same underlying data.
They actually get shared.
Oh, that's interesting.
Yeah, and I think there's even more room for that
with other types, but we're starting small.
But the key thing there is that, like you're saying,
I mean, with coming up with different models and patterns
and libraries, I'm sure they'll come up
as people feel out really what's the easiest way
to take advantage of these features,
and that's the sort of thing that will apply
not just to general free-threaded, like, no-gil,
but also sub-interpreters.
Definitely.
It's going to be exciting.
So I guess I want to move on and talk about
working with this in Python
and the stuff that you've done, Anthony,
but maybe a quick comment from the audience.
Jazzy asks, is this built on top of a queue,
which is built on top of a linked list?
Because I'm building this, and my research led me
to these data structures.
I guess that's the communication across sub-interpreter,
cross-interpreter communication.
Yeah, with sub-interpreters, like in PEPS 734,
a queue implements the same interfaces
as the queue from the queue module.
But there's no reason why people couldn't implement
whatever data structure they want
for communicating between sub-interpreters,
and then that data structure's in charge
of preserving thread safety and so forth.
Yeah, excellent.
Yeah, it's not a standard queue.
It's like a concurrent queue
or something along those lines.
Yeah.
All right, so all of this we've been talking about here
is we're looking at this cool interpreter pool
executor stuff.
That's in draft format, Anthony, for 3.13.
And somehow I'm looking at this running Python parallel applications
and sub-interpreters that you're running.
What's going on here?
How do you do this magic?
You need to know the secret password.
So in Python 3.12,
the C API for creating sub-interpreters was included,
and a lot of the mechanism
for creating sub-interpreters was included.
So there's also a...
In CPython, there's a standard library,
which I think everybody kind of knows.
And then there are some hidden modules,
which are mostly used for testing.
So not all of them get bundled,
I think, in the distribution.
I think a lot of the test modules get taken out.
But there are some hidden modules you can use for testing
or because a lot of the test suite for CPython
has to test C APIs,
and nobody really wants to write unit tests in C.
So they write the tests in Python,
and then they kind of create these modules
that basically just call the C functions.
And so you can get the test coverage and do the testing from Python code.
So I guess what was from PEP6...
I can't remember.
I look at how many PEP6...
Eric will probably know.
What is now PEP734,
but the Python interface to create sub-interpreters,
a version of that was included in 3.12.
So you can import this module called underscore XX sub-interpreters.
And it's called underscore XX
because it kind of indicates that it's experimental
and it's underscore because you probably shouldn't be using it.
It's not safe for work to me.
I mean, I don't know.
But it provides a good way of people actually testing this stuff
and seeing what happens if I import my C extension from a sub-interpreter.
So that's kind of some of what I've been doing,
is looking at, okay, what can we try and do in parallel?
And this blog post, I wanted to try a WSGI or an ASGI web app.
And the typical pattern that you have at the moment,
and I guess how a lot of people would be using parallel code,
but without really realizing it,
is when you deploy a web app for Django Flask or FastAPI,
you can't have one guild per web server
because if you've got one guild per web server,
you can only have one user per website,
which is not great.
So the way that most web servers implement this
is that they have a pool of workers.
G-Unicorn does that by spawning Python processes
and then using the multi-processing module.
So it basically creates multiple Python processes all listening to the same socket.
And then when a web request comes in, one of them takes that request.
It also then inside that has a thread pool.
So even basically a thread pool is better for concurrent code.
So G-Unicorn normally is used in a multi-worker, multi-thread model.
That's how we kind of talk about it.
So you'd have the number of workers that you have CPU cores,
and then inside that you'd have multiple threads.
So it kind of means you can handle more requests at a time.
If you've got eight cores, you can handle at least eight requests at a time.
However, because most web code can be concurrent on the backend,
like you're making a database query,
or you're reading some stuff from a file like that,
that doesn't necessarily need to hold the GIL.
So you can run it concurrently, which is why you have multiple threads.
So even if you've only got eight CPU cores,
you can actually handle 16 or 32 web requests at once,
because some of them will be waiting for the database server to finish running at SQL query,
or the API that it called to actually reply.
So what I wanted to do with this experiment was to look at the multi-worker,
multi-thread model for web apps,
and say, okay, could the worker be a sub-interpreter?
And like, what difference would that make?
So instead of using multi-processing for the workers,
could I use sub-interpreters for the workers?
So even though the Python interface in 3.12 is experimental,
I basically wanted to adapt Hypercorn,
which is a web server for ASCII and WSGI apps in Python.
I wanted to adapt Hypercorn
and basically start Hypercorn workers from a sub-interpreter pool,
and then seeing if I can run Django, Flask, and FastAPI in a sub-interpreter.
So a single process, single Python process,
but running across multiple cores and listening to web requests
and basically running and serving web requests with multiple gills.
So that was the task.
In the article, you said you had started with G Unicorn,
and they just made too many assumptions about the web workers being truly sub-processes.
But Hypercorn was a better fit, you said.
Yeah, it was easier to implement this experiment in Hypercorn.
It had like a single entry point.
Because when you start an interpreter, when you start a sub-interpreter,
you need to import the modules that you want to use.
You can't just say, run this function over here.
You can, but if that function relies on something else that you've imported,
you need to import that from the new sub-interpreter.
So what I did with this experiment was basically start a sub-interpreter
that imports Hypercorn, listens to the sockets,
and then is ready to serve web requests.
Interesting.
Okay.
And at a minimum, you got it working, right?
Yeah, it did a hello world.
So we got that working.
So I was, yeah, pleased with that.
And then kind of started doing some more testing of it.
So, you know, how many concurrent requests can I make at once?
How does it handle that?
What does my CPU core load look like?
Is it distributing it well?
And then kind of some of the questions are, you know,
how do you share data between the sub-interpreters?
So the minimum I had to do was each sub-interpreter needs to know which web socket should I be listening to.
So like which network socket, once I've started, what port is it running on?
And is it running on multiple ports?
And which one should I listen to?
So yeah, that's the first thing I had to do.
Nice.
Yeah.
Maybe just tell people real quick about just like what are the commands like at the Python level that you look at in order to create an interpreter, run some code on it and so on.
What's this weird world look like?
Derek, do you want to cover that?
Yeah, there is a whole lot.
I mean, if we talk about PEP 734, you have an interpreters module with a create function in it that returns you an interpreter object.
And then once you have the interpreter object, it has a function called run or a method.
The interpreter object also has a method called exec.
I'm trying to remember.
It was exec sync because it's synchronous with the current thread.
And whereas exec run will create a new thread for you and run things in that there.
So they're kind of different use cases.
But it's basically the same thing.
You have some code.
Currently supports just you give it a string with all your code on it, like you load it from a file or something.
You know, basically it's a script that's going to run in that sub integer.
Alternately, you can give it a function.
And as long as that function isn't a closure, doesn't have any arguments and stuff like that.
So it's just like really basic, basically a script, right?
If you got something like that, you can also pass that through.
And then it runs it.
And that's just about it.
If you want to get some results back, you're going to have to manually pass them back kind of like you do with threads.
But that's something people already understand pretty well.
Right.
And create one of those channels.
And then you just wait for it to exit and then read from the channel or something like that.
Yeah.
And so there's a way to say things like just run.
And there's also a way to say create an interpreter.
And then you could use the interpreter to do things.
And that lets you only pay the process like startup cost once, right?
Yeah.
And you can also, you can call that the run multiple times.
And each time it kind of adds on to what ran before.
So if you run some code that modifies things or import some modules and that sort of thing, those will still be there the next time you run some code in that interpreter.
Which is nice because then if you've got some startup stuff that you need to do one time, you can do that ahead of time right after you create the interpreter.
But then in kind of your loop in your worker, then you run again and all that stuff is ready to go.
Oh, that's interesting because when I think about, say, my web apps, a lot of them talk to MongoDB and use Beanie.
And you go to Beanie and you tell it to like create a connection or a MongoDB client pool.
And it does all that stuff.
And then you just ambiently talk to it.
Like go to that, you know, kind of like Django or whatever, right?
Go to that class and do a query on it.
You could run that startup code like once potentially and have that pool just hanging around for subsequent work.
Nice.
All right.
Let's see some more stuff.
So you said you got it working pretty well, Anthony.
And you said one of the challenges was trying to get it to shut down, right?
Mm-hmm.
Yeah.
So in Python, when you start a Python process, you can press Ctrl-C to quit, which is a keyboard interrupt.
That kind of sends the interrupt in that process.
All of these web servers have got like a mechanism for cleanly shutting down because you don't want to just, if you press Ctrl-C, you don't want to just terminate the processes.
Because when you write an ASCII app in particular, you can have like events that you can do.
So people who've done FastAPI probably know the on-event decorator that you can put and say, when my app starts up, create a database connection pool.
And when it shuts down, then go and clean up all this stuff.
So if the web servers decided to shut down for whatever reason, whether you've pressed Ctrl-C or it just decided to close for whatever reason, it needs to tell all the workers to shut down cleanly.
So signals, like the signals module, doesn't work between sub-interpreters because it kind of sits in the interpreter state, from what I understand.
So what I did was basically use a channel so that the main worker, like the coordinator, when that had a shutdown request, it would send a message to all of the sub-interpreters to say, okay, can you stop now?
And then it would kick off a job, basically tell Hypercorn, in this case, to shut down cleanly, call any shutdown functions that you might have, and then log a message to say that it's shutting down as well.
Because the other thing is, with web servers, if it just terminated immediately, and then you looked at your logs, and you were like, okay, why did the website suddenly stop working?
And there was no log entries, and it just went from, I'm handling requests, to just, you know, absolute silence.
Yes.
That also wouldn't be very helpful.
So it needs to write log messages, it needs to call, like, shutdown functions and stuff.
Right.
So what I did was, and this is, I guess, where it's kind of a bit of a turtles all the way down, but inside the sub-interpreter, I start another thread.
Because if you have a poller which listens to a signal on a channel, that's a blocking operation.
So, you know, at the bottom of my sub-interpreter code, I've got, okay, run Hypercorn.
So it's going to run, it's going to listen to Socket's web requests.
But I need to also be able to run concurrently in the sub-interpreter a loop which listens to the communication channel and sees if a shutdown request has been sent.
So this is kind of a, maybe an implementation detail of how interpreters work in Python, but interpreters have threads as well.
So you can start threads inside interpreters.
So similar to what I said with G-Unicorn and Hypercorn, how you've got multi-worker, multi-thread, like each worker has its own threads.
In Python, interpreters have the threads.
So you can start a sub-interpreter and then inside that sub-interpreter, you can also start multiple threads.
And you can do coroutines and all that kind of stuff as well.
So basically what I did is to start a sub-interpreter which also starts a thread,
and that thread listens to the communication channel and then waits for a shutdown request.
Right.
Tells Hypercorn, all right, you're done.
We're out of here.
Yeah.
Okay.
Interesting.
Here's an interesting question from the audience, from Chris.
Well, it says, when you, we talked about the global kind of startup, like if you run that once, it'll already be set.
And, you know, does that make code somewhat non-deterministic in a sub-interpreter?
I mean, if you explicitly work with it, no.
But if you're doing the pool, like which one do you get?
Is it initialized or not?
Eric, do you have an idea of a startup function that runs in the interpreter pool executor type thing?
Or is it just they get doled out and they run what they run?
With concurrent features, it's already kind of a pattern.
You have an initialized function that you can call that'll do the right thing.
And then you have your task that the worker's actually running.
So with the, I don't know, I wouldn't say it's non-deterministic unless you have no control over it.
I mean, it's if you want to make sure that state progresses in an expected way, then you're going to run your own sub-interpreters, right?
But if you have no control over the sub-interpreters, you're just like handing off to some library that's using sub-interpreters, I would think it'd be somewhat not quite so important about whether it's deterministic or not.
I mean, each time it runs, there are a variety of things.
The whole thing could be kind of reset or you could make sure that anything that runs it, any part of your code that runs is careful to keep its state self-contained and therefore preserve determinist behavior that way.
One thing I do a lot is I'll write code that'll say, you know, if this is already initialized, don't do it again.
So I talked about like the database connection thing.
If somebody were to call it twice, it'll say, well, it looks like the connection's already not none.
So we're good.
Right.
You could just always run the startup code with one of these like short circuit things that says, hey, it looks like this interpreter, this is already done.
We're good.
But, you know, that would probably handle a good chunk of it right there.
But we're back to this thing that Anthony said, right?
Like we're going to learn some new programming patterns potentially.
Yeah.
Quite interesting.
So we talked at the beginning about how sub interpreters have their own memory and their own module loads and all those kinds of things.
And that might be potentially interesting for isolation.
Also kind of tying back to Chris's comment here, this isolation is pretty interesting for testing.
Right, Anthony?
Like my test.
So another thing you've been up to is working with trying to run pie test sessions in sub interpreters.
Tell people about that.
Yeah.
So I started off with a web worker.
One of the things I hit with a web worker was that I couldn't start Django applications.
And realized the reason was the date time module.
So the Python standard library, some of the modules are implemented in Python.
Some of them are implemented in C.
Some of them are a combination of both.
So some modules you import in the standard library have like a C part that's been implemented in C for performance reasons typically.
Or because it needs some special operating system API that you can't access from Python.
And then the front end is Python.
So there is a list basically of standard library modules that are written in C that have some sort of global state.
And then the core developers have been going down that list and fixing them up so that they can be imported from a sub interpreter or just marking them as not compatible with sub interpreters.
One such example was the read line module that Eric and I were kind of working on last week and the week before.
Read line is used for, I guess, listening to like user input.
So if you run the input built in, like read line is one of the utilities it uses to listen to keyboard input.
If you start, let's say you started five sub interpreters at the same time and all of them did a read line listened for input.
Like what would you expect the behavior to be?
Which when you type in the keyboard, where would you expect the letters to come out?
So it kind of poses an interesting question.
So read line is not compatible with sub interpreters, but it discovered like it was actually sharing a global state.
So when it initialized, it would install like a callback.
And what that meant was that even though it said it's not compatible, if you started multiple sub interpreters that imported read line, it would crash Python itself.
The date time module is another one that needs fixing.
It installs a bunch of global state.
So yeah, date time was another one.
So what I wanted to do is to try and test some other C extensions that I had and just basically write a pytest extension, a pytest plugin, I guess,
which you've got an existing pytest suite, but you want to run all of that in a sub interpreter.
And the goal of this is really that you're developing a C extension, you've written a test suite already for pytest, and you want to run that inside a sub interpreter.
So I'm looking at this from a couple of different angles, but I want to really try and use sub interpreters in other ways, import some C extensions that have never even considered the idea of sub interpreters and just see how they respond to it.
Like read line was a good example.
Like I think it was a, this won't work, but the fact that it crashed is bad.
How is it going to crash, right?
Like what's happening there?
Yeah.
So it should have, it should have kind of just said, this is not compatible.
And that's kind of uncovered a, and this is all super experimental as well.
So like, this is not, you know, you've, you've had to import the underscore XX module to even try this.
So yeah, there's, there's read line date time was another one.
And so I put this sort of pytest extension together so that I could run some existing test suites inside sub interpreters.
And then the next thing that I looked at doing was CPython has a huge test suite.
So basically how all of Python itself is tested, the parser, the compiler, the evaluation loop, all of the standard library modules have got pretty good test coverage.
So like when you compile Python from source or you make changes on GitHub, like it runs the test suite to make sure that your changes didn't break anything.
Now the next thing I kind of wanted to look at was, okay, can we, to try and kind of get ahead of the curve really on sub interpreter adoption.
So in 3.13 when PEP 7.3.4 lands, can we try and test all of the standard library inside a sub interpreter and see if it has any other weird behaviors.
And this test will probably apply to free threading as well, to be honest, because I think anything that you're, you're doing like this, you're importing these C extensions, which always assume that there was a big GIL in place.
If you take away that assumption, then you get these strange behaviors.
So yeah, the next thing I've been working on is basically running the CPython test suite inside sub interpreters and then seeing what kind of weird behaviors pop up.
I think it's a great idea because obviously CPython is going to need to run code in a sub interpreter, run R code, right?
So at a minimum, the framework interpreter, all the runtime bits, that should hang together, right?
Yeah.
There are some modules that it doesn't make sense to run in sub interpreters.
Readline was an example.
Some TK interrupt, maybe?
Yeah.
Yeah, possibly.
Maybe not, actually.
I don't know.
Yeah.
If you think about like, if you got, when you're doing GUI programming, right, you're going to have kind of your core stuff running the main thread, right?
And then you hand off, you may have sub threads doing some other work, but the core of the application, think of it as running in the main thread.
I think of applications in that way.
And there are certain things that you do in Python, standard library modules that really only make sense with that main thread.
So supporting those in sub interpreters isn't quite as meaningful.
Yeah.
I can't remember all the details.
So I feel like there are some parts of Windows itself, some UI frameworks there that required that you access them on the main program thread, not on some background thread as well, because it would freak things out.
So it seems like not unreasonable.
Yeah, same is true.
Like the signal module, I remember at exit, a few others.
Excellent.
All right.
Well, I guess let's, we're getting short on time.
Let's wrap it up with this.
So the big thing to keep an eye on really here is PEP 734, because that's when this would land.
You're no longer with the underscore XX sub interpreter.
You're just working with interpreters sub module.
Yeah.
313.
Yeah.
So right now it's in draft.
Like, what's it looking like?
If it'll be in 313, it'll be in 313 alpha something, some beta something.
Like, when is this going to start looking like a thing that is ready for people to play with?
So I, yeah, this PEP, I went through and did a massive cleanup of PEP 554, which is why I made a new PEP for it.
I simplified a lot of things, clarified a lot of points, had lots of good feedback from people and ended up with what I think is a good API, but it was a little different in some ways.
So I've had the implementation for PEP 554 mostly done and ready to go for years.
And so it was a matter, it's been a matter of now that I have this, this updated PEP up, going back to the implementation, tweaking it to match, and then making sure everything still feels right.
I try and use it in a few cases.
And if everything looks good, then go ahead and I'll start a discussion on that.
I'm hoping within the next week or two to start up a round of discussion about this PEP.
And hopefully we won't have a whole lot of back and forth so I can get this over to the steering councils in the near future.
Well, the hard work has been done already, right?
Yeah.
The C layer is there and it's accepted and it's in there.
Now it's just a matter of what's the right way to look at it from Python, right?
And one thing to keep in mind is that I'm planning on backporting the module to Python 3.12 just so that we have a print interpreter Gale in 3.12.
So it'd be nice if people could really take advantage of it.
So for that one, we'd have to pip install it or would it be added as...
Yeah.
Pip install.
Okay.
I probably won't support before 3.12.
I mean, subinterpreters have been around for decades, but only through the C API.
But that said, I doubt I'll backport this module past 3.12.
So just 3.12 and up.
That's more than I expected anyway.
So that's pretty cool.
All right.
Final thoughts, you guys.
What do you want to tell people about this stuff?
Personally, I'm excited for where everything's going.
It's taken a while, but I think we're getting to a good place.
It's interesting with all the discussion about no-gil, it's easy to think, oh, then why do we need subinterpreters?
Or if we have subinterpreters, why do we need no-gil?
But they're kind of different needs.
The most interesting thing for me is that what's good for no-gil is good for subinterpreters and vice versa.
That no-gil probably really wouldn't be possible without a lot of the work that we've done to make a per-interpreter GIL possible.
So I think that's one of the neat things.
The future's looking bright for Python multicore.
And I'm excited to see where people go with all these things that we're adding.
When's the subinterpreters programming design patterns about coming out?
Yeah, my thoughts are...
Subinterpreters are mentioned in my book, actually, when it was like Python 3.9, I think.
Because it was possible then, but it's changed quite a lot since.
They, I guess, kind of some thoughts to leave people with.
I think if you're a maintainer of a Python package or a C extension module in a Python package,
there's going to be a lot more exotic scenarios for you to test coming in the next year or so.
And some of those uncover things that you might have done or just kind of relied on the GIL with global state,
where that's not really desirable anymore and you're going to get bugs down the line.
So I think with any of that stuff as a package maintainer, you want to test as many scenarios as you can
so that you can catch bugs and fix them before your users find them.
So if you are a package maintainer, there's definitely some things that you can start to look at now to test.
That's available in 313 Alpha 2 is at least probably the one I've tried, to be honest.
And if you're a developer, not necessarily a maintainer,
then I think this is a good time to start reading up on parallel programming
and how you need to design parallel programs.
And those kind of concepts are the same across all languages and Python would be no different.
We just have different mechanisms for starting parallel work and joining it back together.
But if you're interested in this and you want to run more code in parallel,
there's definitely some stuff to read and some stuff to learn about in terms of signals, pipes, queues,
sharing data, how you have locks and where you should put them, how deadlocks can occur, things like that.
So all of that stuff is the same in Python as anywhere else.
We just have different mechanisms for doing it.
All right.
Well, people have some research work.
And I guess a really, really quick final question, Eric, and then we'll wrap this up.
Following up on what Anthony said, like test your stuff, make sure it works in a sub-interpreter.
If for some reason you're like, my code will not work in a sub-interpreter and I'm not ready yet,
is there a way to determine that your code is being run in a sub-interpreter rather than regularly from your Python code?
Yeah. If you have an extension module that supports sub-interpreters,
then you will have updated your module to use what's called multi-phase init.
And that's something that shouldn't be too hard to look up.
I think I talked about it in the PEP.
If you implement multi-phase init, then you've already done most of the work to support a sub-interpreter.
If you haven't, then your module can't be imported in a sub-interpreter.
It'll actually fail with an import error if you try and import it in a sub-interpreter,
or at least a sub-interpreter that has its own GIL.
There are ways to create sub-interpreters that still share GIL and that sort of thing.
But you just won't be able to import it at all.
So like the readline module can't be imported in sub-interpreters.
The issue that Anthony ran into is kind of a subtle side effect of the check that we're doing.
But really, it boils down to if you don't implement multi-phase init,
then you won't be able to import the module.
You'll just get an importer.
So that's, I mean, it makes it kind of straightforward.
Yeah, sounds good.
More opt-in than opt-out.
Yep.
Right on.
All right, guys.
Thank you both for coming back on the show and awesome work.
This is looking close to the fetish line and exciting.
Thanks, Michael.
Yep.
See y'all.
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.
Are you ready to level up your Python career?
And could you use a little bit of personal and individualized guidance to do so?
Check out the PyBytes Python Developer Mindset program at talkpython.fm/pdm.
Take some stress out of your life.
Get notified immediately about errors and performance issues in your web or mobile applications with
Sentry.
Just visit talkpython.fm/sentry and get started for free.
And be sure to use the promo code talkpython, all one word.
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.
I'll see you next time.
Bye.
Thank you.