If you're a .NET developer or work in a place that has some of those folks,
wouldn't it be great to fully leverage the entirety of PyPI with its almost 600,000 packages
inside your .NET code? But how would you do this? Previous efforts have let you write Python syntax,
but using the full libraries, especially the C and Rust-based ones, has been out of reach
until CSnakes. This project by Anthony Shaw and Aaron Powell unlocks some pretty serious
integration between the two languages. And we have them both on the show today to tell us all about it.
This is Talk Python to Me, episode 486, recorded Monday, November 4th, 2024.
Are you ready for your host? Here he is.
You're listening to Michael Kennedy on Talk Python to Me. Live from Portland, Oregon,
and this segment was made with Python.
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 accounts over at fosstodon.org. And keep up with the show and listen to over nine years of
episodes at talkpython.fm. If you want to be part of our live episodes, you can find the live streams
over on YouTube. Subscribe to our YouTube channel over at talkpython.fm/youtube and get notified
about upcoming shows. This episode is sponsored by Posit Connect from the makers of Shiny. Publish,
share and deploy all of your data projects that you're creating using Python. Streamlit, Dash,
Shiny, Bokeh, FastAPI, Flask, Quarto, Reports, Dashboards, and APIs. Posit Connect supports all of
them. Try Posit Connect for free by going to talkpython.fm/Posit, P-O-S-I-T.
And this episode is brought to you by Bluehost. Do you need a website fast? Get Bluehost. Their AI
builds your WordPress site in minutes, and their built-in tools optimize your growth. Don't wait.
Visit talkpython.fm/bluehost to get started. Hey, everyone. Before we jump into the conversation,
I want to talk about infrastructure. Actually, there are a couple of interesting things going on over here
at Talk Python. And I've written them up on our new Talk Python blog, which I will link to both of these
articles in the show notes. First, we moved our web hosting over to a brand new hosting center in the
U.S. run by a German company called Hetzner. And it's super interesting. I wrote this all up. And I
think people who are considering, like, where should we run our apps? Or if they're unhappy where they are,
it's worth thinking about. And I just rewrote all of talkpython.fm's code, about 10,000 lines of Python,
to move to an async web framework. Which one was it? Well, that was part of the journey. And so I did a
big long write-up on migrating from Pyramid to Quart, which is async Flask, all the other frameworks I
considered doing that, how I went through the migration, and how it's working. So if either of these two sound
interesting to you, check out the links to the articles in the show notes. As always, thank you
so much for listening. Let's jump in the conversation. Hello, Anthony. Hello, Aaron. Welcome to Talk Python.
Hi, Michael. Hi, thanks for having us. It's great to have you here. Anthony, always, always good to have
you back on the show. You're one of the most popular guests, and we'll see where it lands, where this puts
you in the list. But you're right near the top of the guest page out of all of them. I love having you
on the show. Thank you. And Aaron, welcome. It's great to have you here. So even though you've been
on a bunch of times, Anthony, maybe a real quick introduction, just who you are for those who have
not met you yet. Yeah, I'm Anthony Shaw. I work at Microsoft. I'm the lead Python advocate. And I do a
bunch of open source projects. And I wrote a book called CPython Internals, which is how most people
know me these days. That's me. Yeah. So we're going to talk about more CPython Internals or maybe putting
Python inside of .NET. I don't know. It's something's going into the internals of the something else. We'll figure
out which way that goes. It's a very cool project. Hello, Aaron. Tell people a bit about yourself.
Sure. Well, I'm definitely the odd one out here. I am someone who is pretty much never written Python in my life.
Millie, I now know a bit more about it, having worked on this project with Anthony. But I'm a .NET developer
by trade. I've been doing that for longer than I'll admit on a recorded audio. At least 10 years. Okay, got it.
Yes, we'll go with at least 10 years. I'm setting a lower bound for us. That's a good one.
I also work at Microsoft. I mean, a team adjacent to Anthony's doing advocacy around .NET. And yeah, but otherwise, I also write
JavaScript. So I do everything I can to avoid having to get into the Python space in my time.
And yet here you are. And here I am. Yes. Amazing. Yeah. So it's a different world, right? Coming from .NET, that's guaranteed static typing, a lot of semicolons, a lot of ceremony around the types. And then Python kind of says, hold on now.
And white space. Python loves its white space. But then again, I have a very soft spot for F sharp. So I'm not like, I kind of get the white space significance aspect of a Python language. But at the same time, I do miss McCurley brackets.
Yeah. The trick is, if you use a good editor, you don't really notice it. If you don't, it's pretty bad. So use a good editor. I mean, that's all there is to it. Anthony, what have you been up to? What's new in your world?
I've been up to this project for a few months now.
Is this primarily what you're focused on?
It was for a few months. Yeah. I'm trying to get ready for we've got a massive conference, one of our two big conferences of the year coming up in a couple of weeks. I'm working hard on demos to try and get ready for that. But is that .NET Conf? What is that?
No, this is for Ignite. It's the partner conference.
Oh, yeah, yeah. Ignite is so big.
It's like 15,000 people or something.
Yes, exactly. I went to a pre pandemic. I think it was 30,000 people.
Yeah, it was in Florida. And there was a whole podcast row. So Microsoft had me out as one of 10 podcasts or something. I thought, Oh, what is this? I hardly ever heard of this. I've heard of build and some others. And I show up like, this is something else. Where'd all these people come from? It's a big conference. It's crazy.
Yeah, I'm getting ready for that at the moment. But yeah, I've been working on this project for a few months. And we kind of kicked it off in May. Idea kind of floated around in May at the build conference, which is Microsoft's developer conference.
After talking to a lot of different developers and people in Microsoft, we were kind of kicking around the idea of if we looked at this again, so like running Python code from .NET, like how would you approach it?
I kind of had some ideas, put some ideas together and then started working with Aaron on a really early prototype just to see like if it was even possible. And then we kind of got something working, chucked it in the bin, pretty much started again.
And then redesigned it. And then yeah, we've been sort of fiddling with it ever since.
From my perspective, I said before, I'm not a Python person. But as someone in the .NET ecosystem, looking at the breadth of libraries that exist for Python, there's a lot of things that I would have loved to have used, particularly doing some of the AI stuff that we've been doing in projects recently.
And then finally, there's just not as many packages or options in the .NET space. So it's always been that like, "Ah, okay, well, if I want to learn this, the tutorial is in Python or the package it recommends using is in Python." So it was always kind of like, how do we bridge that gap?
That was kind of one of the early thinkings of just, you know, that's a clear problem space, right? Like a lot of people are learning that, but then going into industry and maybe Python isn't the language that the organization they're working with is using.
How do we still have that? A fundamental knowledge that they've gained or the like the tooling they want to use available to them?
Well, just think about me. I'm sure you have thought about how much of an explosion in possibility it is if you can say, and by the way, there's almost 600,000 other libraries, including the biggest community of AI based resources that are now, as far as you're concerned, native to .NET, right?
Yeah. But from the way you consume it, not the way it runs is what I mean.
Exactly. Yeah. Like you mentioned there, nearly 600,000 Python packages and you add that to, there's like 400,000 packages on NuGet for .NET. Like you got quite literally a million possibilities.
Yeah, it's amazing. And just for people listening, primarily Python audience, NuGet is .NET's equivalent of our PyPI.
Yeah.
Yep. Yep. Yep. Okay. So Anthony, you had this idea. Somebody said, "Hey, how would you go about doing this?" And then you were sucked in?
Yeah. It was actually one of the sort of lead designers on .NET asked me at Build. They said, "Oh, we've been looking at how you might integrate Python into .NET for a while. Like if you were to approach it, how would you do it?"
Mm-hmm.
And I kind of went through all the projects that have tried this in the past or there are different ways of doing it and pros and cons of each and kind of the pros and cons of a really low level integration and a high level one.
So like a high level one would be stick a FastAPI on top of your Python app and just call it via HTTP REST.
And somehow auto-generate some REST endpoints and then make those do their thing.
Yeah. And you could use it like an open API spec on top of it and auto-generate a client and stuff like that.
So that's like the high level option. And then like the lowest level option is like, you know, you could get into the bytecode level, which I've explored years ago with Pigeon.
And yeah, that's a very deep, dark tunnel.
It's full of terrible edge cases and corners and sharp edges, huh?
And one of the challenges there is that the like Python's implementation is changing so rapidly right now.
Like, but just in Python 3.13 and looking at 3.14 now and like all the changes and stuff in there, like it's just continuously changing.
So it needs to be a happy medium in terms of how we blend the two together.
Right. There has to be some level of disconnectedness a little bit, right?
So that Python can do it, have its changes without constantly chasing it, right?
Yeah, exactly.
So yeah, kind of what we kind of came up with was we would do the integration at the C level using Python C API.
There's a way of consuming Python through sort of like an embedded mode.
Not many projects do this, but it is definitely possible.
So you can spin up a Python interpreter inside another process, which in our case is a net process.
Yeah. And I think Blender does that.
I'm pretty sure that there's a bunch in the film and movie industry that do that for their automation pipelines.
I don't think it's that great.
One, I talked to some of the people from the movie and video space, and they, most of them are still running on Python 2 because when they wrote the implementation to integrate the thing, integrate CPython, they're like, well, it's done.
And like you kind of said, Python moved on and they're like, well, that's a lot of work.
And then we got to, we'll just leave it on Python 2.
And that's not, that's not great.
Yeah, exactly.
So we looked at this and said, we want to be, we want to support the newest version of Python.
We want to support the sort of practically old version, which I think you settled around 3.8 was like as old as I was willing to go.
And I think we maybe push that forward to 3.9 because there were some specific features we need on the .NET side.
Again, there's a lot of changes happening in .NET.
Oh, Sharon, maybe you can share a bit more, but like, you know, there's .NET is going through just as many changes.
So we've got like two moving, constantly moving platforms and we want to make an integration between the two of them.
So let's think about like, how can we do this in a way which is actually maintainable and we can make it, you know, we can test it and we can make it stable as well.
Yeah.
One thing maybe you could speak to as part of this, people might know .NET as this Windows programming library that you use Visual Studio with.
Right.
But it's, it's changed a lot since then.
There's .NET Core, there's open source.
It runs on Linux.
There's, there's a lot of different things that have been changed about it.
Right.
Yeah.
And it's a fairly common misconception that we've got is that, that people equate .NET with Windows and like historically that's, that's true.
Like that's, that's where I got started is developing on Windows.
And I, I'll be honest, I'm still a Windows developer.
It's what my primary OS has been.
I've, I've flirted with macOS and Linux over the years, but Windows is just where I'm more comfortable with.
But I think it's been a bit over a decade now since the, they essentially relaunched .NET was originally called .NET Core.
So, so now we kind of have these two streams concurrently.
You've got .NET framework, which is the one that is what we traditionally thought of as .NET.
It, it ships kind of like with Windows and stuff like that.
So it's, it's very much designed around that longer tail support lifecycle and, and, and whatnot.
It is fairly stable, but also getting minimalistic investment just because it has that kind of need to be stable and consistent year in year out.
Then we have .NET Core, or now we just kind of refer to it as .NET.
Just naming things is hard.
Let's, let's be honest.
But so we, we have .NET and every year a new release comes out and this is like a, like a major framework, like a major revision to the runtime.
So .NET 9 is actually coming out next week, at least at the time of recording, it's next week.
And this is considered a, like a short term support lifecycle.
So if you're familiar with support lifecycle of a lot of runtimes out there now.
So like Node popularized this quite a lot now.
And Python is starting to adopt it a bit more that kind of you have this yearly rolling lifecycle and, and you'll have long-term supports and short-term support.
So the long-term is every even number.
So .NET 8 being the most recent long-term support.
.NET 6 was the previous one, which is actually going out of support next week at the time of recording.
Yeah, being realistic of where people are and developing their applications.
Where we couldn't just target Python 3.13 and .NET 9 as the only supported versions, because be honest, that's not where most packages are.
That's not where most projects are going to be.
So we have to make, you know, some, some calls there.
Yeah.
You do have to have a little bit of, a little bit of slack in there.
Even for me and some of my web apps and stuff, I had to wait a few weeks to upgrade to 3.13 because some dependency of a dependency had something that had been removed.
You know, been deprecated for a while and was removed and it didn't work.
Like, well, try that again in a few weeks and see how it works.
And eventually they, they caught up, but yeah, it's definitely a challenge.
This portion of Talk Python to Me is brought to you by Posit, the makers of Shiny, formerly RStudio and especially Shiny for Python.
Let me ask you a question.
Are you building awesome things?
Of course you are.
You're a developer or data scientist.
That's what we do.
And you should check out Posit Connect.
Posit Connect is a way for you to publish, share and deploy all the data products that you're building using Python.
People ask me the same question all the time.
Michael, I have some cool data science project or notebook that I built.
How do I share it with my users, stakeholders, teammates?
Do I need to learn FastAPI or Flask or maybe Vue or ReactJS?
Hold on now.
Those are cool technologies and I'm sure you'd benefit from them, but maybe stay focused on the data project.
Let Posit Connect handle that side of things.
With Posit Connect you can rapidly and securely deploy the things you build in Python.
Streamlit, Dash, Shiny, Bokeh, FastAPI, Flask, Quarto, Ports, Dashboards and APIs.
Posit Connect supports all of them.
And Posit Connect comes with all the bells and whistles to satisfy IT and other enterprise requirements.
Make deployment the easiest step in your workflow with Posit Connect.
For a limited time, you can try Posit Connect for free for three months by going to talkpython.fm/posit.
That's talkpython.fm/posit.
The link is in your podcast player show notes.
Thank you to the team at Posit for supporting Talk Python.
Anthony, when we opened this, you mentioned there had been other attempts, other ways.
The one that comes to mind for me is IronPython.
IronPython is a CLR implementation of Python.
And that's a pretty big difference right there than what you're trying to maybe contrast it with some of the things that people have done previously.
Yeah, so probably three projects, I think, that have been done previously that kind of sit in the same space, but they have different goals.
So, I mean, the main goal for our project is that there are loads of .NET developers that have existing applications.
They are typically quite, it like sits in the enterprise space, a lot of it.
So they're like quite big, big applications.
They've got teams working on them and there's a technology in Python that they want to use.
And that technology is often a package or a library or an example, especially in like data science now, and a lot of the ML and AI libraries that are floating around.
So a lot of the idea for this and the demand from developers was from, you know, we want to use these libraries or we want to use this particular package in Python.
And like, how do we import that into our .NET application?
So like, that's kind of the angle that we've come at this from, which is like, how can we make the best of both worlds?
Like you've got a big, you're like a fast, mature enterprise application in .NET.
And it is like a piece of technology that's in Python that you want to use.
Iron Python is an implementation of Python written in .NET.
And that was done quite a while ago, I think.
And yeah, like older versions of .NET, older versions of Python.
And it's kind of similar to Jython as well.
Like I think I put it in the same bucket as Jython, which is...
I would as well. Yeah.
It would like, and at that time, a lot of people were making different implementations of Python, the language in other frameworks.
And so, yes, you can write Python syntax and run it in the CLR, but you couldn't do something like import NumPy.
And that's the difference is that most packages in the ecosystem these days are designed and tested for CPython.
And this is all, at least they're tested for PyPy.
But all the other implementations are pretty much a no.
Like Pandas, NumPy, scikit-learn, like all the data science ML tools.
Especially the C interop type of stuff.
Yeah.
And I've sort of written about this in the past, how, you know, Python's...
But one of the solutions to Python's performance problem is that you implement extension modules in C, which is a great solution.
Or you use Cython, which is the same kind of thing, which is a great solution.
It just means that the ecosystem is more and more tied to CPython as the interpreter of choice.
So what I didn't want to do is write a whole new interpreter.
Like that goes back to the compatibility problem and the maintainability issue.
Like by the time we published it, it would be out of date and we'd just be opening up stuff up for a whole load of work.
It's not even about, it's not even a matter whether you can do it and you can keep up with the language.
As we talked about, you know, as Aaron mentioned, there's a million libraries you can use.
But if you did this, you wouldn't be able to.
Yeah, I'm Python is one.
Pigeon, we've covered in a past episode.
Oh, that's not going to work.
Is a P-Y-G...
There we go.
Is a weird spelling.
P-Y-J-I-O-N.
No, that's right.
I remember now.
P-Y-J-I-O-N.
This is a JIT for CPython that happens to use the .NET Core JIT engine because that was one of the most pluggable JITs at the time.
There are other JITs available.
It could have been written in ASM JIT.
It could have been written in LLVM JIT.
Dino who actually worked on Iron Python.
So like he knows this space really well.
Brett Cannon worked on Pigeon originally.
And then I rewrote it like a few years later.
That's a different problem.
That doesn't help you integrate .NET and Python together.
It just happens to use components from .NET to implement a JIT.
That project is effectively dead.
Some of the benefits of it have been rolled into CPython in versions 3.10 and 3.11.
The things that made it faster.
Most of those were sort of picked up anyway.
And then the third one is Python.NET, which is probably the closest to the project that we've started.
This one has been around for a while.
This one's been around for a while.
It's a similar thing, but they mostly focus on the integration the other way around.
So like how do you call .NET libraries and things from Python?
You can call Python.
Oh, right.
Yeah.
A lot of the focus is actually the other way around.
So if there was a popular .NET library that you wanted to use in your Python app, you might Python.NET your way to that.
And this project has been around for a while.
So the API is pretty stable.
And we originally looked at this one actually as a potential solution, but found a number of things that they'd kind of tied themselves to in the design, which are going to go away in .NET 9.
So it wasn't really an option for us.
And also like a lot of the way the C API was used was different to how we wanted to do it.
So yeah, that's kind of the alternatives.
And so you ended up writing your own because just none of these really match, right?
The Iron Python was just never going to keep up and it didn't have this package support for data science mostly.
And then the other one was the other way around.
And yeah, so well, I guess a good thing to think about is, well, why did you think you could do that?
Like a lot of people, a lot of smart people put a lot of time into this.
Why did you think, oh, this is going to be, this is going to work well.
Do you have a fresh take on it or what were you thinking?
I really wanted to use some of the modern benefits of both Python and .NET to do this, to design it.
And one of the biggest changes in actually, as we've been implementing this, we're learning 80% of the challenges are related to typing.
And Aaron's smiling because he's agreeing with me.
Like integrating two different ecosystems together like this is about calling functions, methods, whatever.
But calling things is about calling them with arguments really.
And it's passing data backwards and forwards, marshaling information and getting things backwards and forwards between different types.
Like that's if you integrated, I don't know, some function that you wrote in Python into .NET, you're going to be calling it with something like you're going to be sending it some data and expecting some data back.
Now in Python, there are types.
It is dynamically typed, but the types do exist.
Even if you don't put it in your function signature, there's still instant strings and stuff being sent all over the place behind the scenes.
Yeah, exactly.
And when we looked at Python.NET, like, you know, you could call a function in from .NET in Python, but you'd have to write a load of code that said like, oh, I think this parameter is probably this type.
So convert that to this and then when it comes back, then it's probably this one to try that one first.
And so we sort of said, like, now typing type annotations are more popular in Python.
What we wanted to do was basically you give it some Python code and you write like a function, which is the one that you want to call and you annotate the function.
So you say it takes, you know, has these arguments and they are these types and it returns back this type.
And then what we did is wrote a code generators called a source generator for .NET that would in in your project, it would look at the Python file that you wanted to embed.
It would see what functions are defined, what their signatures are and what types they take.
And then it would generate a function that can call that for you with .NET equivalence.
And then it handles the the marshalling, like the transfer of the types backwards and forwards between .NET and Python.
And like that's most of the project is really about that particular problem.
Okay.
So it's leveraging the C API of Python kind of talk directly to the runtime.
You can write a function in Python, you know, that takes a string, a list and a tuple and returns back a dictionary.
Like it's a fairly common thing to do.
Yeah.
You would give it type annotations in Python and you drop that Python file into your .NET project and you run build again and you would see a type in .NET with the same name, with a function that is the one you just defined and with arguments which are .NET equivalents to that.
And you just call the function and it just runs in Python.
How complicated can this data exchange be?
Can I come up with my own types and exchange them or do they have to be six or seven core types?
I think the data exchange is actually one of the really tricky bits, right?
Here's a list of a million items.
Why don't you copy them all over to a .NET type, run it or copy it over to a Python list, run the thing and then copy the back to a .NET list with one more item in it or something crazy like that, right?
Yeah. If you jump to the documentation and then in the reference page, there's a list of the ones that we support at the moment.
Oh, yeah.
Then there's a list of the core ones that we support.
And then anything beyond that.
So we do generics, for example.
So if you say it's a list of strings, we will give you back a read only list of string in .NET.
If it's a dictionary and you type the key and the value, we will give you back a .NET key and value.
Now in Python, often it could be an any or it could be a union or it could be something complicated.
So you've got the option to say it could be a dynamically defined type.
It does still support it.
And then at runtime, you can look at the type to see what it is and then convert it into something that you need.
I see a lot of read only on the .NET side.
And this is the thing receiving the data from Python.
I'm glad that you picked up on that because that was something that I made a decision when we're doing the .NET service area for this of it.
So that's the thing that I think that anytime we're receiving data back or when we're sending data to is it should be represented as read only just to indicate that if you're calling a function that's been exposed from Python from .NET and it's returning a collection like a dictionary or a list or anything like that.
So that's the thing that you're going to do.
So that's the thing that you're going to do is you're going to do that.
And then you're going to do that.
That you're very familiar for .NET developers for doing like creating an array from an existing collection of data.
And then that becomes obviously a more mutable data structure or like a list, like the read-only list, for example, that then plugs straight into links so that you can do the query syntax or the expression methods syntax to perform operations on those collections of data inside of .NET space.
Kind of once it's being hydrated back out of Python.
So I guess you get a lot of these read-only innumerable type of things.
You could do link on them, L-I-N-Q.
Yeah.
I wish we had this in Python.
I know we have something kind of like it with list comprehensions, but it's not the same.
One thing that I really wanted to make sure that we were tackling with this and to Anthony's point before of the, that you've got two very different type systems that you're working with.
And structurally, the way that you write code in Python is going to look different to the way that you write in .NET.
So we didn't want to feel like the code gen that we do for you is that we've just made this, it's just .NET casing of Python code.
So I wanted to feel like as a .NET developer, I'm working against a .NET interface.
So when you work with the types and the modules that you expose out to .NET and we do code gen against, we then generate you a class behind the scenes that is then, we then generate an interface for you.
And then that's registered into the dependency injection container.
So you then can just resolve that elsewhere with inside of your application.
So now as a .NET developer, I'm getting an interface injected into a class that I'm using elsewhere with inside of my code base.
I haven't had to go, okay, well, no, I'm calling some kind of like an import or I'm doing some kind of resolve or something that might be unfamiliar to me as a .NET developer.
This is what I'm doing is just, it just feels like I'm writing, or I shouldn't say necessarily .NET developer, I'm running C#.
And we've optimized this for C#.
I said earlier on, I have soft swap for F# and it kind of works in F#.
And then similarly, if you're a .NET developer, we optimize this to the kind of development experience you would expect when you're writing C# code.
We just so happen to have, you, you haven't written this, some of this C# code.
We've just automatically generated it for you and injected it into a dependency injection container.
Amazing.
So when we're talking about exchanging this data, this is CPython in the same process.
Is that right, Anthony?
Yeah.
How's that work?
Python's C API, normally if you call, so CPython is the interpreter that you would use to run your Python code.
It is implemented in C mostly.
The standard library modules are written, some of them are in Python, some of them are in C.
You can write modules for Python in C.
And the way that you would do things like create numbers and dictionaries and lists is you use this API.
It's a C API.
And part of that C API is the ability to spin up a Python in C.
You can spin up a Python interpreter inside another process.
And that's embedded Python.
There's different APIs for doing it.
There's a lot of different ways of approaching it.
But really what we do is that when you spin up .NET and you use CSnakes, you inject a Python interpreter into the environment.
It's not like a separate process.
Like it's not, it hasn't got like a ship command line windows running in the background.
It's embedded into the process.
It's like loading the library.
It's running inside the process.
And then we have full control over everything in CPython.
So we can then import modules.
We can convert things, types backwards and forwards.
So we're kind of doing a lot of the work that often gets done either in a C extension module or inside the interpreter.
So if we want to create a Python number, for example, like an integer from a .NET integer, we do that at the C level.
So we can do that like a really high performance API.
If we want to look at the size of dictionaries or the types and stuff like that, we do that at the C level.
So again, it's super high performance.
And then really importantly, well, the typing stuff is really important.
The other major challenge with this is, you know, .NET freely uses threading and thread pools and coroutines and stuff like that.
Python can use threading, but there is a lock.
So you have to be really, really careful.
And this is something I don't didn't see like a novel solution from the other projects that are looking in this kind of space about how they would tackle it.
Or it was kind of based around like older styles of Python and things have changed so much recently.
So we kind of wanted to make sure that you could run this in a multi threaded environment, whether that's like a UI, for example, like a desktop interface or it's a web app.
So like one of our demos is like a .NET web app, like API, that each request can run a Python function if it wants to.
And they're all running in separate threads in a thread pool in .NET.
And you shouldn't have to worry about the GIL.
Like you shouldn't have to know too much in terms of what it is and how it works.
So we've done quite a lot of work to make sure that that can happen.
This portion of Talk Python to Me is brought to you by Bluehost.
Got ideas, but no idea how to build a website?
Get Bluehost.
With their AI design tool, you can quickly generate a high quality, fast loading WordPress site instantly.
Once you've nailed the look, just hit enter and your site goes live.
It's really that simple.
And it doesn't matter whether you're a hobbyist, entrepreneur or just starting your side hustle.
Bluehost has you covered with built-in marketing and e-commerce tools to help you grow and scale your website for the long haul.
Since you're listening to my show, you probably know Python, but sometimes it's better to focus on what you're creating rather than a custom built website and add another month till you launch your idea.
When you upgrade to Bluehost cloud, you get 100% uptime and 24/7 support to ensure your site stays online through heavy traffic.
Bluehost really makes building your dream website easier than ever.
So what's stopping you?
You've already got the vision.
Make it real.
Visit talkpython.fm/bluehost right now and get started today.
And thank you to Bluehost for supporting the show.
Python has async and await.
C# has async and await.
They're similar, but not the same.
Yeah.
Can I have an async function in Python that is called by C#?
No, not yet.
Just a task.run, async.io.run to completion sort of thing.
And then keep going.
Yeah.
I would love to do this, but there is no API for it on the Python side to manage event loops and embed an event loop into another process or to write a custom event loop.
It's all kind of built into the standard library or was put into another module.
There's been a couple of proposals in draft peps, but none of them have been accepted.
And if they were, then it might be in 3.14 anyway, which we can't backward compatible support.
Yeah, you're not quite there time wise yet.
Do you find that async.io event loops and Python are hard to juggle?
Right?
Like you have to create one if it doesn't exist, but if it does exist, then it doesn't want you to create a new one.
But there's not a great way to see if it exists.
And if you don't have direct access to it, how do you get like, it's just, it always feels to me a little bit out of touch to fully take control over that and work with it.
And it changes between Python versions.
Like it gets, it's gotten easier recently.
Like there's a, there's like a dot run function now.
And whereas previously you have to like get the event loop and create one and stuff like that.
But now you can just kind of run the coroutine.
It's still tricky.
And the idea of basically writing a custom event loop and then giving it coroutine objects and like polling them and looping them from another runtime.
I just couldn't figure out the API.
I'd love to do it.
If anyone knows, then please ping me.
Well, UV loop does it.
Yeah.
Somehow, but maybe they just swap out some pieces of it.
Not all of it.
Yeah. I'm not sure how, but you can set the event loop from UV loop.
So maybe, I don't know, maybe there's a breadcrumb there to chase.
All right.
So I see in the steps, install Python, put your projects in a C# file, use annotations on your Python functions, right?
That's a really important step that you talked about.
Install the CSnakes runtime package.
So maybe from this point on, let's talk a little bit about some of the steps.
So this CSnakes runtime, this is a NuGet package that you put into your .NET project.
Is that what I'm reading here?
Yeah.
So we essentially have two binaries that we ship.
One, which is going to do all the code generation for you.
So from a .NET perspective, you'd be, it wouldn't be uncommon to have multiple different projects with inside of your solution, which is then like one might just be a bunch of services, which are then going to then talk to a database or whatever the case may be.
So you might have that as just a, like, that would be where you would have the code gen that you want to do.
So where the Python files are actually getting their C# interop generated for you.
So you have one project with that.
And then we have a second binary, which is then the runtime to actually like make that work when your application is running.
Now we do ship this as a single consolidated package just for ease of use, but that's kind of why we differentiate some of it in terms of like code gen versus runtime because they're different concepts.
And one is a compilation step versus obviously runtime step.
Yeah. Okay.
With our runtime package and the documentation talks a little bit more about this.
There's just a few steps you've got to do to make sure that the C# compiler is capable of finding the Python files.
So we actually do stuff with inside of the C# compiler pipeline to generate code that is then added to it.
So this also means that we can very early on find out if you're going to have compatibility issues.
So if something is type of the type that we don't understand or the Python code is syntactically obscure, because what I've learned is that there are ways that you can write valid Python code that is very obscure.
And they showed me some, yes, some things that you can do, but please don't do it.
And yeah, so you'll actually fail a compilation step rather than fail runtime process.
And I think like, again, from a from a .NET developers perspective, like you were used to that static typing that compilation step that that belief that the compiler has done a whole bunch of checks for us to give us code that should just work rather than just like that more dynamic or typing system that you get out of Python or like in my experience, like the JavaScript side of things.
Yeah, that's definitely the expectation.
the expectation if you're doing C#, C++, if it compiles, at least it's clicked together in a coherent way.
Right. Whereas, whereas Python, we're pretty sure we don't know for sure, but we're pretty sure.
So in Python typing, you can lie.
Right. I could say it takes a list of integers and really what it took was strings, which had numbers as characters.
Who knows? Right.
What happens if I lie to the C# code gen thing?
So it just crashes.
Don't do it.
Yeah.
It does actually just crash when it calls because it has to make the assumption that what you've told it is correct.
So in .NET, if I've got a tuple of ints and I give it across and it's like, I actually wanted strings.
No, it's not going to have it.
It will raise a, yeah, because to get, for example, if it returns a list of strings, but you actually return a list of integers, you know, in order to give you back a list of strings in C#, we first check that it's a list.
And if it isn't, or if it's not a sequence type, like it could be a, you could have inherited from this to major and custom list, or you could make something which is a sequence.
Like if it's not that, then we will raise an exception.
It doesn't just crash.
I was joking.
And we've got a lot of testing to make sure there's a whole, like a test environment that's around like lying code and type hints and stuff, which are completely invalid.
And then for the strings, you know, we need to look at the Python string.
And then because Python mostly uses UTF-8 and .NET mostly uses UTF-16, you know, we need to convert from one to the other.
And if it's, and if it's an integer, not a string in the first place, then that's never going to work.
So we would raise an exception there and say, you taught us it was a string and you lied.
But you lied exception is thrown.
Yeah.
I think it's like a cast, invalid cast exception or something like that.
Yeah.
With the description.
We've tried to raise like the exceptions we raise are .NET exceptions, which are relevant to the concept of the problem that we've covered.
But at the same time, it actually will give you, imagine if something failed in the Python code as well.
Like you type annotated that it was like a bunch of strings and we, and I know a bunch of ints and we, we gave you a bunch of strings.
Well, okay.
The Python code is going to fail there.
So where we then surface out the Python exceptions as well as like embedded with inside of the .NET exception.
So you can, you can actually get down to like the, the lines of code with inside of Python that failed, which I think is kind of.
The stack trace actually includes C#, C#, C#, Python, Python, Python, potentially.
Yeah.
That's pretty awesome.
In Python, if you didn't raise from it, that will show up as an inner exception in .NET as well.
So we did a lot of work on exceptions, mostly because we had to like debug this stuff when we're developing it.
And it was pretty amazing.
We saw the pain that someone else was going to feel.
So yeah, you see the, you can see the stack trace, you get like all the frame variables and stuff like that.
So things that you would probably want from like a PDB, you can see on the .NET side.
So if you're running the debugger in .NET and it crashes in Python, like it or raises an exception or something, you see the Python exception with all the details.
And then you see like inner exceptions and stuff like that as well, as well as the locals and the globals and any other frame information.
Yeah, I was pretty impressed when I saw that, that how integrated, how much I felt from both directions, kind of natural and not just bolted on.
Yeah.
Another step is mark these for code for source generation.
This feels like a Visual Studio 20, whatever year the last release of Visual Studio was.
Aaron, where are we?
What's the number?
22.
Okay.
So the reason I bring this up is a lot of people hear Visual Studio, especially in the Python space and think Visual Studio code.
Does this work in Visual Studio code or is it just a other Visual Studio?
No, it definitely works in VS Code.
They sometimes open it in VS Code.
And obviously you'd likely just installed the C# extension to get a better developer experience on that.
But what we have to do is because the source generator is actually running as they step with inside of the C# compiler pipeline, we need it to know how to find these files.
And we kind of toyed with the idea of just to like, just do a directory scan.
Everything that ends in .py, we're going after.
That's not particularly efficient.
And also like if you've just got like modules that are using other modules, like you want to, what's, how do you craft a surface error in Python that you actually want to expose rather than just expose everything?
So we're inside of the csproj file.
So this is the file that is the instructions on how to compile a more complex .NET application.
We can then add nodes to that XML structure to say, like, here's a Python file.
It's an annotate that for the C# compiler to pick up so that when the source generator hits it, it hits our steps.
It goes, okay, I'm going to find all of these additional files that you've said I need to be aware of.
And then I can read them in and it's actually got some optimizations for how like internally it will read that file in rather than us then having to do file IO or anything like that.
So it just, it's more in physically aware of them and that it will make those available so that it can then generate out the code rather than, rather than like I said, us having to try and guess what Python files you want available.
Like, you know, they have to be in a special folder or they had to be named a certain way or we just directory scan everything that ended with .py.
Yeah. You want to be really careful about which ones you're trying to do this to because they require type annotation and you don't want to just expose all the guts of some bit of code.
You want to maybe talk about the surface area, right?
As .NET people, you have these protected private internal, protected internal, et cetera, things.
We don't talk about them in Python.
I want to be consistent with that, right?
So Anthony, it says last step to instantiate the Python environment.
This is, it feels almost like a, an import statement.
Then you use it namespace style later.
Do you want to talk about that real quick?
In order to start like into embed Python into the .NET process, you need to create a Python environment.
And there's a few things we have to know, like where is Python installed?
What version is it?
Do you have a virtual environment?
Is it just like the global environment?
Do you have like packages?
There's like additional things that we need to know that you would have in Python, you would have kind of set those elsewhere.
Like if there was a virtual environment, you would have activated it.
You know, if you were running Python, then you would have set which version it was and where it's installed.
So you've in .NET, you haven't done those steps.
So we need you to tell us like which version of Python do you want to, do you want to create and where is it?
And we've created a whole bunch of code to help you find it.
The UV team actually have been solving a similar problem with like spitting up Python and running Python.
So I'm kind of curious to see like how they've tackled it.
And we might even try and look at bringing UV in as one of the mechanisms for finding Python for you.
Because it's such a complicated.
Yeah.
It's something I wanted to talk to you about is you've got to have Python installed and you've got to get the dependencies.
And yeah, we're not going to install Python for you.
Yeah.
But with UV, V and V, give it a Python version.
It can fetch it.
It can fetch it and just run it and it's cached.
And maybe you could embed that as a resource at compile time.
There's interesting possibilities there.
Virtual environment support uses either condor or pip as the package manager, depending on what you want.
And you can say, here's my requirements file, and it will go in and pip install them as well.
Like it can do all of those things for you because we're kind of assuming that, yeah, you've developing this locally and you're running it locally, but it's going to be deployed somewhere else.
So you don't want to have to like log into the production servers and like create virtual environments on the command line and stuff like that.
That's like with hopefully 15, 20 years past people doing that.
Like, so you should be able to programmatically say this project needs a virtual environment and it needs these packages installed and needs this version of Python.
And then you can either use the version of Python that comes with your Linux container, for example, or you can pull in one from somewhere else.
For Windows, you can actually download it from NuGet.
It's like you can actually add it as a dependency in .NET.
And that's a really simple solution for Windows development for macOS.
If you've installed it from Python.org, like using the official installer, you just you just tell it that you just say, use like the macOS Python and give it the version number.
And then for Linux, it's a lot more complicated because there's no standards.
So, yeah, we've kind of support every which way of finding it on Linux.
And then once you've kind of put the Python environment into the process, there should only be one of them.
Python itself does not want you to ever spin up more than one Python environment in the same like operating process.
It will have all sorts of problems if you if you do that.
So it's basically put in as a singleton and then you import a module by creating an instance of it.
And then on that module, you can then call methods which are the functions in the module.
So like if you had a module called demo.py, then in your environment, you'd see like a factory for like effectively like import demo and giving back an instance of it.
And then on that instance, you'd have methods which are the functions.
And the example you've got on your screen, you know, there's a function called hello world that takes some arguments in a in a file called demo.py.
So you just say environment.demo, like make a demo instance.
And then on that instance, you can call dot hello world with the arguments and it gives you back the results.
So there's no really like in a few lines of code, like four or five, you can spin up a Python environment, import a module run a function.
So we'd really try to minimize the border plate code by and then following the patterns that are best for both platforms.
So a lot of places you could go with this is really cool.
For example, if you if you got a singleton, which is great.
And that's kind of how Python typically is one environment.
But with the sub interpreters, you could theoretically create these in parallel.
But one thing that stands out as maybe a little non expected in this is like you have var module equals env a demo, right where you're creating this.
But in Python, this thing is called demo.
Would it be a pattern to maybe name the variable the same as the module name?
That's probably just more from laziness in the way we've written our docs.
Yeah.
But what we're probably lacking from because this is obviously docs and just markdown code is that so when you were so we would get back our IPython environment from our dependency injection container and then things like the demo methods exposed there would then return an i demo.
I think it is or and that is then the representation of the that demo dot PY file that you've imported.
So, yeah, probably.
Yeah.
So then then all those methods are then exposed on that.
So, yeah, a module from it like conceptually, if I'm if I'm thinking about this as a .NET developer looking at it from Python, like, okay, that the module is that Python file.
And that's what I get back when I say, like, I want an instance of the interface into that file.
Yeah, and it does honestly it doesn't matter how I feel about this as a Python person.
It feels about it matters how .NET people feel about this when they look because that's this is C# code.
That's their code.
That's their world.
It just so happens that C snakes make it makes it really nice possible to talk to the Python world.
Yeah.
So one thing I wanted maybe could spend a couple minutes talking about this Anthony is I saw an example where you you showed integrating open telemetry and ASP .NET and Postgres and all sorts of stuff together, not just a, you know, hello world sort of example, which shows some really cool stuff, but maybe doesn't convince people that they could do cool stuff with it.
You want to just maybe talk us through some of the moving parts of that to give people a sense of a more realistic example.
Yeah, so we have a another demo, which is a web app medicine in the project.
Well, this is kind of what we thought was a more realistic integration example.
So you've got a web app written in.
Yeah, well, the front end written in HTML and JavaScript.
The API is written in dotnet.
You're using Postgres as the database.
And you've got like a feature that you want to add to your app where you want to use some Python libraries and some Python code.
And we were looking at whether everyone loves the weather demo.
Just real enough.
Come on.
It's just real enough.
The weather demo is like the canonical .NET demo of when you do file new API projects like here is a weather app.
So I from a .NET developer, I feel very much at home.
Yeah, you're already on board.
Okay, got it.
Yeah, we're creating the one that people would expect.
Now on the .NET side, we also said, right.
Yeah, let's be realistic about what people would want to do, because like modern applications, you would write the API in ASP.net and you would, you know, have API endpoints that you would and routes and configure those.
And you would have a probably a relational database like Postgres or SQL Server or something as the storage environment.
And then now we support open telemetry in a platform called Aspire, which Aaron can probably explain in a second.
So open telemetry is a new standard for like tracing and instrumentation and observability generally of applications written in any language.
And in .NET you can have.
So for this app, for example, like, you know, somebody makes a request to the web app and you see the request come in and all the timing and there's any log records in .NET or if anything crashes or like it's all associated with the same request.
Now, if we're going to spin up and run a Python function and in Python, you do import logging, you know, logger logging dot info or logging dot error.
Like, where does that go?
Because you've just logged something in Python.
Does it just disappear into the ether?
Is it like it goes into the terminal output of the container?
It runs it.
Who knows?
Right?
Yeah, exactly.
Like it would be kind of useless and not easy to manage.
And we're coming at this at the angle, like people write big enterprise apps in dotnet.
So like they would expect that they do logging and it's all in the same system.
So this demo basically says, OK, one of the API endpoints in on dotnet, it runs a function in Python, which spins up.
It uses pandas and as a data frame to look at the weather or in it, like calculate some averages and shifting means and stuff like that to give you back like a prediction for what the weather is going to be like this week.
And we just use Seattle was the data set because it's easy because it's always going to rain.
And then it's not actually most of the time it doesn't.
It's not fair.
So it runs that in Python.
And in the Python code, we use open telemetry to show the trace of it talking to the database.
And we use Python logging and that log information on the trace information all gets put into the same trace.
So from .NET from start to finish, you see the request come in on the front end.
You see the .NET code executing.
You see the Python code.
And then if Python crashes or if the log entries go in, they're all in the same environment.
So in like an open telemetry dashboard, you see the whole trace across both .NET and Python or in the same environment.
And you can just use logging as you would normally.
And .NET aspire comes with a like a free open telemetry dashboard that you can run locally.
It's like a Docker container.
It's really nice to save you kind of spinning up Jaeger or something.
Yeah, this is really cool because you've got an ASP.net API.
But then when it's called, it uses one of these code gen CSnakes Python integrations.
But a lot of stuff just flows through like the Postgres connection string flows through the open telemetry context flows through the logging kind of goes tangentially off.
Then it talks to the database in Python and then it returns back.
And your example is just pure SQL connections and stuff.
But it's like a PG2.
But you could use pretty much any ORM or anything, right?
There's no real constraint there.
Yeah.
And it uses a few modules which are tricky like pandas and psycopg is a anyone who's deployed that to production in containers or in different environments.
And those is it's hard because like it's like which binary do you use and like importing it is hard.
And, you know, you need libpq and like it's a bit of a nightmare.
So it's just demonstrate that they're like, this is this is a real thing you can do.
And it shows something which is practical in Python and something which is practical in .NET, which is like a proper app with tracing and logging and all the, you know, all those kind of features.
And on the Python side, we're using pandas, which, you know, and we're using numpy, which is the C extension.
And we're using psycopg, which is like depends on a whole bunch of like third party libraries as well.
And stringing it all together and it just looks and feels seamless.
Like you see the demo working and you would never know that the, the weather prediction comes from Python or even that it's running in this way.
Like it just looks like and it's implemented like it's part of the same API.
It's call a function and one of the libraries and off it goes.
But yeah, it's all connected together like this.
All right. I'll let either.
We're about out of time, but I'll let either of you take this.
That was a web example.
What about something like WPF or Maui or any other desktop app possible?
Yeah. So because I'll talk about Maui because obviously that's the, the more modern UI framework.
Give a really, really quick definition to listeners who probably think of that as part of Hawaii.
It's kind of the, the next evolution of Xamarin, which people may be familiar with the way that you do cross-platform UIs.
I'm definitely not the expert on Maui, but the idea is that, you know, you can have something that is a bit more portable from a application experience,
if you want to target like a mobile application, like an Android or an iOS or run as a desktop application on Windows or something like that.
So just like a level of portability and some structure between UI concepts.
But fundamentally that's just running .NET behind the scenes.
So there's no reason why we couldn't also drop in a, a bit of a sea snakes in there and have that, yeah, work with some kind of backend service.
Like you could use this and this is something that Anthony and I have talked about trying to, trying to get a demo of doing like a local model running with,
like, so you, you pull a model off of hugging face or something like that, just runs it locally to do some kind of AI inference or whatever the case may be for the scenario you want to tackle.
But that way you don't have to be, you know, calling out to an external service.
Now, while sure running like a local model, it's doable with .NET.
It is just way easier to do that with Python.
And all the examples you would find are going to be written in Python first and foremost.
So trying to then go, okay, well, so this is doable in Python.
So what's the C# equivalent?
How do I find the equivalent .NET libraries on you yet?
And so on and so forth.
Well, you know, like with sea snakes, you could expose that model, create some surface areas in just Python APIs that is then going to be consumable by a .NET application.
So yeah, it's something you can definitely do not 100% sure whether you can do it on mobile.
I think that's probably stretching it a bit far just because of the way that we have to bootstrap Python with inside of it.
And you'd probably be limited from a mobile OS runtime to be able to do that kind of bootstrapping.
But at the very least, like a desktop application, or if you wanted to tackle with like a cross platform UI framework like Avalonia.
So Avalonia is a cross platform .NET UI framework that means you could build for Linux and Windows and macOS at the same time.
You could then run sea snakes under the hood of that to then have that desktop experience that is leveraging Python for where Python is going to be better suited to tackle a problem.
.NET for a different set of problems that's better suited for that just inside this application where you would have no idea as the end user that you're like that that's what's actually happening.
Very compelling. Very compelling.
Very compelling.
All right, Anthony, let's close this out with one final thing.
I think this is important and we haven't really touched on it yet.
You guys started the conversation by saying this is really important for data science, right?
Data science use cases.
And I made the joke about copying a million items to add one to the list.
But what about sharing data like NumPy across different, you know, the .NET side and the Python side and so on?
We have tackled that specifically.
So there's something called the buffer protocol.
It's a PEP from a few years ago.
It's a kind of an API for the internals of something like an ND array in NumPy.
And the buffer protocol with it, we support that.
So if your function returns a buffer, which is a type, then we can access the internals of that buffer directly without having to convert the data.
What this means in reality is that if your function returns a like a NumPy array, like an ND array and the internal values in the ND array are a specific type like a 32 bit integer, you can access them as 32 bit integers in .NET without having to marshal or copy data.
That's awesome.
You reach inside of the the ND array, just grab its actual memory and just say, got it. Let's go.
Got it. Let's go.
Yeah, that's exactly what we do.
And so that's a is in .NET.
It's called a read only span, which is the you can write the data as well.
So you can do it and you can actually just modify the values inside the NumPy array directly from .NET without having to copy backwards and forwards.
And we support pretty much every like low level types that NumPy supports.
And that means you don't have to do a .2 list on the NumPy array because like that is really expensive.
And also .NET works in a similar low level numeric types that NumPy does.
You know, it will work with 32 bit ints and 32 or 64 bit floats, for example.
So like if you're storing a 2D matrices in 32 bit floats in NumPy and then you want to send that to to .NET to I don't know, like pick out some specific values or like do some calculations in .NET, then you can just chuck that back as a buffer.
And there's information in the docs about how to do this and then you can just access the values directly out of memory.
So it's super fast.
And if you even want to like fill a buffer with values from .NET, then you can do that using the writable span.
That's pretty awesome.
So, yeah, that really is leaning into the data science benefits here as well, isn't it?
Nothing ever goes wrong if you're sharing memory across runtimes for different programming languages.
It's no, I don't see how this could go wrong.
Yeah, perfectly fine.
And that's why I pushed to make sure we supported read only span.
Yeah, right.
Okay.
No, I think it's great.
I think it's great.
And the UI stuff with Avalonia is something I'm going to look at.
That sounds really cool to play with.
So, all right.
Final thoughts, you guys.
People out there going, oh my gosh, I cannot believe we can finally do this with our .NET app and plug in some of the other things we're already doing in Python.
Should they?
Is it ready?
If somebody were to take this and build a thing, is that a good idea or should they wait?
Yep.
They should download it, give it a go.
The only thing we're possibly changing at the moment is the API based on feedback.
We're trying to keep that as stable as possible right now.
So we're only making changes that are backward compatible.
And yeah, we just want to see more use cases and people can get more feedback.
So yeah, please pull it, grab the samples, spin them up, build something, tell us what you made, and then give us any feedback.
Well, congrats.
It looks really, really nice.
Definitely, definitely looks promising.
And I think it's, it solves a lot of the problems that the other attempts had.
So nice work.
Thanks, Mike.
Thanks.
Yep.
Bye guys.
Be sure to check out what they're offering.
It really helps support the show.
This episode is sponsored by Posit Connect
from the makers of Shiny.
Publish, share, and deploy all of your data projects
that you're creating using Python.
Streamlit, Dash, Shiny, Bokeh, FastAPI, Flask, Quarto,
Reports, Dashboards, and APIs.
Posit Connect supports all of them.
Try Posit Connect for free
by going to talkpython.fm/posit, P-O-S-I-T.
And this episode is brought to you by Bluehost.
Do you need a website fast?
Get Bluehost.
Their AI builds your WordPress site in minutes
and their built-in tools optimize your growth.
Don't wait.
Visit talkpython.fm/bluehost to get started.
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.