You might use Python every day, but how much do you know about what happens under the covers, down at the C-level?
When you type something like variable equals open bracket square bracket to create an empty list,
what are the bytecodes that accomplish this?
How about the class backing the list itself?
All of these details live at the C-layer of CPython.
On this episode, you'll meet Anthony Shaw.
He and I take a guided tour of the CPython source code.
After listening to this episode, you won't have to guess what's happening.
You can get clone the CPython source code and see for yourself.
This is Talk Python to Me, episode 240, recorded Wednesday, October 30th, 2019.
Welcome to Talk Python to Me, a weekly podcast on Python, the language, the libraries, the ecosystem, and the personalities.
This is your host, Michael Kennedy.
Follow me on Twitter, where I'm @mkennedy.
Keep up with the show and listen to past episodes at talkpython.fm.
And follow the show on Twitter via at Talk Python.
This episode is sponsored by Linode and the University of San Francisco.
Please check out what they're offering during their segments.
It really helps support the show.
Anthony, welcome back to Talk Python to Me.
Hey, Michael. It's great to be back.
Yeah, it's great to have you back.
To say you've been on the show before is a bit of an understatement.
You were on episode 131, contributed to open source.
155, practical steps from moving to Python 3.
168, 10 Python security holes and how to plug them.
180, what's new in Python 3.
7, and then 214, diving into 3.
8.
So I think you might be one of the most prolific guests here on the show, which is awesome.
I love having you on here.
Yeah, thanks for having me back.
It's good to be on the show.
Yeah, I can't believe that this is the sixth time I've been on here.
Yeah, I had to actually use the search engine on the website to figure out how many times you've been here.
So yeah, this is going to be a good one because we're going to dive into something that everybody uses.
But there's so many dark corners for most folks who are not core developers, right?
Like, okay, we all know there's CPython.
There's probably other Pythons.
You maybe have heard of those like PyPy and whatnot.
But can you open up the code?
Where do you get it?
Where are the important parts, right?
It's a huge project.
But there's certain parts you really should pay attention to.
And the others are like details, right?
Yeah, absolutely.
I mean, I think if it's kind of like my car, like if I open up the bonnet of my car, I know where the engine is.
I know where to put the oil.
And that's pretty much it.
Like, I don't know what half the other components do.
So yeah, I feel like it's like that with CPython sometimes.
You know how to use it.
But in terms of how it actually works, it's a bit of a mystery.
That's a good analogy for sure.
Now, before we dig into this, maybe just tell people what it is that you did day to day.
So they get a sense of where you're coming from, like in the open source space and in your day to day job.
If they want your whole story, they go back to episode 132 and the whole getting started and whatnot.
But give us the quick summary there.
Yeah, sure.
I work for a company called NTT.
And I run talent transformation for them.
So looking at skills and development of employees for NTT globally.
That's my day job.
And then I'm also a sort of Python enthusiast and get involved with various open source projects as well.
Some Apache projects as well as some of my own personal projects.
Yeah, awesome.
Like Wiley.
Like Wiley, yeah.
And been playing a lot with pytest and Azure Pipelines recently as well.
Yeah, right on.
Cool.
So we're going to talk about this whole CPython source code story.
And you've been touching on this in several ways.
You've been writing some articles.
But then you decided to write a book and disguise it as an article.
And you called it your guide to the CPython source code over at RealPython.
It's excellent.
We'll link over to it.
But we're going to cover a bunch of the ideas that you touched on in there because this is a really good exploration.
But, you know, what got you started in like digging into the source code in the first place?
Some of it was curiosity.
A few years ago, I wrote an article on how to add an operator to the Python syntax.
So how to add a plus plus.
So like an in-place increment operator, which Guido is famously against for good reasons.
But it was more of an exploration, like how would you actually add that to the syntax and recompile Python, which was really, really interesting to dig into.
And also I found that if you want to contribute to CPython, the documentation that there's a site called the dev guide, which is great telling you like the process for raising pull requests, like what the branch strategy is.
But if you, you know, if you were to join a new software team, you would expect that in the first few weeks, one of the senior developers would sit you down and walk you through the code and explain how everything works.
But that documentation is kind of missing.
So I wanted to write something that kind of filled that gap so that if people wanted to get into working on CPython, contributing to it or making tweaks, enhancements or customizations, then there's something that really kind of takes them through in depth, the whole source code and how it works and what each component does.
It definitely does that.
I feel like these large projects that often have a bunch of special steps to get started, to get your machine configured and whatnot, you know, they can be intimidating.
But, you know, using the article, I was able to get the code, get it up and running and be playing with Python 3.9 super quick.
It was just, I don't know, most of the time was waiting on the compiler, actually.
Yeah, there's ways of making it faster, but it's a big piece of code to compile.
So it takes a while.
Yeah, I definitely ramped up the number of cores getting used there, but it still takes a while.
It's quite cool.
All right.
So before I guess we maybe dive into the source code itself, let's maybe talk a little bit higher level.
It was like some of Python is Python, which is cool and meta.
And some of Python is C code, which is maybe surprising to some folks who are kind of new to Python and how it's like executes internally.
And maybe there's even some other code in there as well.
Like I haven't seen any inline assembly, but you never know, right?
What's the breakdown there?
Or like, how would you categorize that?
It's about 70% Python and then the rest is C code.
So there's about 350,000 lines of C code, which is a lot of C code, but over 800,000 lines of Python, which includes, I guess, the test suites as well.
On top of that, actually, there's documentation is over 220,000 lines of documentation.
So the documentation itself is a huge amount of work.
So the restructured text is actually one of the main languages.
Restructured text is one of the main languages.
Yeah, absolutely.
230,000 lines of restructured text.
Would it be safe to say that most of the standard library is written in Python, but not all of it, but almost all of the core interpreter and compiler is written in C.
Is that a good representation?
The core types are written in C.
The compiler is written in C.
Most of the sort of core engine and the runtime is written in C.
In terms of the standard library, anything which doesn't need to patch into any of the operating system,
APIs, like the networking or any hardware or anything, is written in Python.
Otherwise, it's written in C.
Some languages that all of it are written in that language, right?
Like Go, for example.
Then there's other ones like Python words, some Python, some C.
But we also have things like PyPy, which is more Python.
Is it 100% Python?
I'm not sure there might be some little tiny shim to get it started.
But why is it in C?
If you're making a new programming language, to write the compiler, you need a programming language to write the compiler in.
So it's difficult if you're starting a new language from scratch.
The Go is actually a good example because the Go compiler is now written in Go, but it wasn't originally.
Once they got Go a bit more mature, then they basically rewrote the compiler in Go.
But you still need an actual interpreter and a compiler to be able to do that.
So CPython is written in C largely because they need something to start off with.
This was written a while ago.
C is still a very popular language.
And also Python has a lot of integrations into the operating system components.
And most operating system APIs are in C.
So for Windows, Linux, and macOS, if you want to talk to the sound card, if you want to talk to the screen,
and if you want to open a socket on the network, then you're going to be talking about C APIs.
So, you know, the ability to do all that stuff seamlessly in Python means that at some point,
it needs a C layer to integrate into the kernel.
Right, to call the Win32 API or down into Linux or macOS, their native APIs, right?
Yeah, exactly.
Yeah, cool.
So this is a huge project, as the size, my joke about your book, you know, I sort of hinted at.
So when you look at it, like, how did you get started?
There's got to be a bunch of stuff you decided not to cover.
Some stuff you did.
You do have your sort of mission of like, here's the missing dev guide, sit down with a senior developer.
But how do you decide to get started on this?
Or like, what goes in and out?
Yeah, so the approach I took was not to go file by file,
but instead kind of follow a trace from typing Python at the command line with some code,
all the way to it being executed and then back up again.
So it kind of takes the article takes you through, you know, what happens when you run Python,
how and then basically steps through each layer deeper and deeper and deeper into the code,
and then explains each point what's happening.
And then kind of I've added diagrams and stuff like that to show you.
So it's almost like a, you know, like a traceback.
If you were to kind of add like a custom traceback, but actually doing tracebacks in Python is really hard.
So not in Python, but in CPython.
Yeah, most of the code that you'd be trying to look at would actually be in C, not in Python, right?
Yeah, exactly.
And I've ended up writing some tools to help me put the article together and also do some debugging to kind of pick this apart.
Sure.
Well, what's your background in C?
Like how prepared were you for this journey?
And how easy was it?
I guess is what I'm getting at.
I thought I understood C, but then diving deeper and deeper into this code,
I really kind of had my head scratching a few times.
There's a lot of macros in the CPython source as well.
So anyone who's worked quite a bit with C code might be surprised at the shiv volume of macros.
So a macro is basically a way of before the code gets compiled,
the preprocessor will replace a macro with another piece of text basically before it gets compiled.
And there's a lot of these in CPython.
So it makes it basically they're micro optimizations to the code,
but it does make it quite tricky to read and understand.
Yeah, I can imagine.
You know, when I was looking through it, I used to do for a handful of years, professional C++ development.
And I could read it, but I was thinking, you know, I'm really glad I'm writing Python these days because,
wow, I know what this means.
It's a lot of work.
A lot of work to write C.
Yeah.
And also making changes to the code.
So in the article, it kind of encourages you to not just understand how it works,
but also to make little tweaks and changes and add your own customer statements and maybe,
you know, interfere or look at the tracing and stuff like that.
And as part of this, I ended up kind of writing a few pull requests into CPython and doing a couple of bug fixes and things like that.
That's awesome.
What were they for?
They were really minor ones, just stuff that I discovered when I was kind of digging in.
There's a couple that still need to be merged as well.
I'm still working on one for Windows support for changing the parser generator.
So if you want to add custom syntax to Python from Windows, then getting that support in.
And also I worked on one which was rejected, but it was an interesting experiment to do with list comprehensions.
So if you basically do a list comprehension over a list, so typically you'd use list comprehensions for things like filtering a list into another list.
But when you run a list comprehension, it first of all initializes an empty list.
And what I realized is that if you initialized that list to a larger size or you predicted the size of the list, then it's a lot more efficient.
So it ends up being about 10%.
If there was no if block, there's no if part in the list, and you know you were doing a comprehension over the list, the size should be exactly the same as before, right?
So you should just pre-allocate that.
Yeah.
So it was an experiment to see if that was possible, which it was, but it was a bit hacky.
And it did make a difference in terms of performance.
I think it worked out being about 8% or 10% faster on list comprehensions, but it added too much complexity.
So it was rejected.
But I think it's an ongoing experiment that we need to look into.
That's a non-trivial difference you made by doing that.
I mean, I understand the complexity thing, but 8% is a lot these days on a 30-year-old polished piece of software.
Yeah, it's if you're doing a list comprehension over a list of a fixed size, but all of the benchmarking tools in CPython use the range function, which doesn't have a fixed size.
So basically, the benchmark suite didn't think there was much difference because the benchmark suite heavily uses range.
But in practical applications, you wouldn't use range a great deal.
How interesting.
Okay.
That's a super cool one.
I love it.
Nice.
All right.
Well, let's start at the beginning.
I'm interested in the CPython source code, and I want to play around with it.
How do I get it?
What is it, in Subversion or something these days?
Well, so it's really easy.
Yeah.
So it's all moved to GitHub.
It's easy to find.
GitHub.com slash Python slash CPython.
And you can download that as a zip file.
You can download that using a Git client, or you can use your IDE to pull it for you.
Yeah, it's so cool that it's over on GitHub these days.
It's really nice to have it kind of modernized.
I think it encourages people to participate more in the discussion.
They were talking about moving the issues there as well, but I don't know if they were moved yet.
That'll be cool when that happens.
There's a PEP that Marietta has put together, and it's proposing moving GitHub to GitHub issues
from a bug tracker that they have at the moment.
I don't think that's been decided on yet.
It wouldn't surprise me to see it happen, but I guess maybe one of the questions is just
all the historical issues get somehow migrated over, and I can see challenges there.
This portion of Talk Python to me is brought to you by Linode.
Are you looking for hosting that's fast, simple, and incredibly affordable?
Well, look past that bookstore and check out Linode at talkpython.fm/Linode.
That's L-I-N-O-D-E.
Plans start at just $5 a month for a dedicated server with a gig of RAM.
They have 10 data centers across the globe, so no matter where you are or where your users
are, there's a data center for you.
Whether you want to run a Python web app, host a private Git server, or just a file server,
you'll get native SSDs on all the machines, a newly upgraded 200 gigabit network, 24-7 friendly
support even on holidays, and a seven-day money-back guarantee.
Need a little help with your infrastructure?
They even offer professional services to help you with architecture, migrations, and more.
Do you want a dedicated server for free for the next four months?
Just visit talkpython.fm/Linode.
All right, so we've got the code.
We get cloned it, or however we're going to get it off of GitHub.
And then you get this project structure with like 13 or 14 top-level folders.
So maybe we could talk through just some of the major sections, because I think the file
structure is a pretty good partitioning.
The folder structure, there's pretty good partitioning to start to understand, like, where do I go explore?
Yeah, once you've downloaded the source code, the first folder is called doc, which is where
the documentation is.
So that's the 230,000 lines of restructured text.
Wow.
Yeah, and if you want to start off by understanding some of the APIs as well, the documentation is
a good place to go.
There's also a folder called grammar, which is for the sort of computer-readable language
definition.
So what is in the Python syntax?
What makes an if statement an if statement?
Are you, you know, could you type if else colon?
Like, that wouldn't make sense.
And why?
So the computer understands, I guess, how the language is structured.
There's a folder for include for C header files, which is also good to understand the
API in a bit more detail.
There's the lib directory for basically the standard library models, all the ones that are
written in Python.
Yeah, I think the include one is pretty good because you can just see the function definitions.
You don't have to like jump through all the implementation and the macros and the hash if def stuff.
You can say like, these are the things I can call over here.
Right.
You can get a little higher level view.
This is why if you're going to jump into this, you want to pick a pretty decent IDE because there's a lot of code in here and using a plain text editor is going to be extremely difficult to navigate things.
I agree.
So yeah, I'd recommend picking a decent IDE to start off with.
For me, when I was playing with this, I used VS Code on the whole top level project.
And it just said, hey, you should have the C, C++ extension installed.
Sure.
Do that.
Yeah.
It was pretty good from there.
Also installed the restructured text extension.
It was kind of adapting.
Maybe over if I was on Windows, I might use Visual Studio proper because it's got a Visual Studio solution in there, which is kind of cool for Windows developers.
Yeah.
And the article actually I'll take through.
So Visual Studio 2019 came out whilst I was writing this.
So it's been updated to explain how to use the community edition, which is the free version.
So it's different to VS Code.
Visual Studio is like a fully blown IDE.
It's designed for sort of C, C++ and C# development.
And there's some explanation in here about how to use that to both compile CPython from source as well as do debugging and stuff like that.
Yeah.
You've got some really cool stuff how you have like the REPL running in a debugging instance embedded in Visual Studio or something like that, right?
Yeah.
It's pretty cool.
Actually, I was really impressed.
Definitely for Windows users, I'd say if you want to make changes and not just explore,
then I'd pick on Visual Studio rather than VS Code because you're going to get much richer debugging.
And Visual Studio 2019 as well is going to be able to compile for you.
So if you just use VS Code, then you're going to need to run the MSBuild script files,
which are located in the PCBuild directory.
But it's actually a lot easier to use Visual Studio rather than running it all in the command line.
Yeah, that makes sense.
And I totally derailed your summary of these things over here.
So we were talking about the lib folder is full of all the part of the standard library that is written in Python,
like the CSV module or whatever.
But then there's a bunch more, yeah?
Yeah.
So there's a folder for macOS support files.
There's a couple of other miscellaneous directories that you shouldn't need to worry about.
There's a folder called modules, which is the standard library modules that are written in C.
So the standard library modules are split between the lib and the modules folders.
So whether they're written in Python or C, they're in two different places.
Right.
So object, where you have a class and it drives from object, there's an object.c file in modules.
Right.
That part's pure C.
Actually, they're in the objects folder.
Oh, yeah.
So there's a folder called objects, which has got the core types and the object model.
So what is a number type?
What is a string?
What is an array?
That sort of thing.
Yeah, I had it totally wrong.
So maybe like GC or something like that.
Yeah.
Then there's the parser, which is basically the thing that actually parses the source code into something that can be interpreted.
Then there's some directories for Windows users.
So there's PC and PC build.
PC is the new version.
PC build has got some sort of legacy scripts for building for older versions of Windows.
There's a programs directory, which is the source code for the sort of either the Python.exe or the Python binary that you end up with on your machine.
And there's a Python folder, which is confusing, but it has the interpreter source code.
So I think it interprets the code through to execution.
And then there's a folder called tools, which has got some tools and scripts and stuff like that for either building or extending Python.
Super cool.
And yeah, there's some of these that you want to really dive into and others are just support files.
When I was looking around in the lib folder, I was kind of blown away at some of the stuff that's in there.
I'm like, all right, well, what in here is actually implemented in Python?
What's the code look like?
You know, those are all interesting things.
And then I came across some stuff that surprised me.
Obviously, you would expect that these files would have comments, documentation that describes what they do, right?
Absolutely.
Yeah.
So, but then I saw that a lot of them, not a lot, some of them, non-trivial number of them, actually have like ASCII diagrams that describe, say, like workflows or like relationships in the comments.
It's pretty wild.
Like the lib concurrent features process.py has like a great long like in process, out of process, like workflow diagram in the help doc.
Oh, wow.
Okay.
That's pretty funny, right?
And then also the JSON module, you know, has like cool ASCII art documentation.
So anyway, I thought those were, those were surprising to me.
Like really, there's, there's like diagrams in here.
How cool.
Yeah.
I think if anyone wants to have a go at contributing to CPython, a really easy place to start is in the lib folder is all the standard library modules that are written in Python.
They're easy to read.
Most of them are not too complicated.
Have a look through some of those because there's stuff in there, which is legacy syntax, which needs updating.
There are bugs in there, which have been reported in the bug tracker, but never fixed.
As well as if you compare what's in the code to what's in the documentation, you'll pretty quickly find gaps.
The functions, which either don't have any documentation or the documentation is wrong, or the argument list is up to, is not up to date.
So if you want to have a go and contribute and you're looking for something simple to get started with, then I'd say pick some of the, probably some of the more obscure standard library modules and see what needs fixing in those.
Yeah, that's a good idea.
And I honestly don't know how much of those have these, these issues, but it seems like a pretty straightforward way.
Certainly contributing to the lib folder or the docs folder seems much more approachable to me than to like the objects or the modules.
Cause down there, that's where the C code lives, right?
Yeah.
Before you get stuck into the core runtime, then I think it's a good idea to have a look at some of the high level Python code first.
Probably a good idea as well.
So I thought I'd just pull out a couple of files from each of these major sections, these folders that you talked about that were just kind of interesting.
So over in the lib module, we have things like CSV, whole CSV implementation is over there.
And it's, you know, it's not that much.
I don't remember exactly how long it was, but it's, you know, a couple hundred lines, maybe 500 lines and you can just go read it and play with it.
Right.
Yeah.
And you can make changes.
You can put your own debug statements in, you can see how things are working, but it's pretty straightforward.
There's a, you used to use the dict reader before, which is a really handy way of doing CSV parsing.
Yeah.
It's easy to understand how that works.
It's written in clear, clear Python.
Super cool.
Yeah.
So people can poke around there over in, let's say objects.
That's the one I got wrong, actually.
Over in objects.
This is where object.c is defined, right?
And this thing is way more complicated than I expected it.
Obviously it's the base class for everything.
So it's going to be doing a lot.
It doesn't do as much as I thought it would, but it, there's a lot of code involved in there, isn't there?
The core object types are actually quite complicated.
That was something that surprised me when I was going through this deep dive is that I thought that there wasn't really a big difference between objects that I defined and objects that were built in like the, you know, the int type and the string type.
I thought that they were more or less the same, but actually everything kind of sits on top of the core types.
So the core types are all declared in C effectively, and they have C functions and everything that you put on top of that goes into a dictionary and is, is pure Python.
Right. And it seems like there's a lot of memory management stuff that's happening down in there as well, like allocation and finalization.
And it seems like that's the main purpose of what the plain object is about.
Yeah. I think the two ones that are really interesting to look into is the list object and the dictionary object.
So the beautiful thing about the list object is that you never have to worry about writing linked lists or doing list allocation like you would in many other languages.
You can just add items to a list and it just magically makes it the right size.
In the article, we actually talk about the growth pattern and how it reallocates the size automatically and how that works.
But yeah, it's pretty cool to see how that is behind behind the scenes.
Yeah. Do you really appreciate what it's doing for you down there?
That's awesome.
I love that.
It's not just here's an array.
Now you get to figure out all the complicated details of using it dynamically.
Right.
It's no, it's beautiful.
It's a list.
You know, there's one, you know, I told you I opened this whole project up in Visual Studio Code and Visual Studio Code has this cool extension, built in extension or like something I installed that has like a little gray highlight.
When you're on a line of, you know, who's done what I think it's called get lens, maybe it's the thing I installed.
But it had, you know, I was just poking around and it shows, you know, who's contributed or checked in this file.
Like, so Pablo Galindo had just worked on object.c 22 days ago.
But then as I arrowed down, like different parts would light up with people doing different things.
Like line three, it says, get it on Rossum 29 years ago, initial revision.
Some of the code in the CPython source is quite old and hasn't needed to change.
Like it just worked the first time and it hasn't needed to be updated or there's nothing that's been changed in it.
It's really interesting because when you dive through the code, I guess you can kind of see there.
If you're looking at the Python 3.8 source code, you're actually looking at a almost like a canvas of 2.anything all the way up to the latest version.
Even actually, even before 2.something, like some of this stuff is all the way back from version one.
Right, right.
Like object.c.
Yeah.
Yeah, exactly.
So some things haven't really changed since the first versions, but the vast majority of the code has been drastically rewritten.
over the last 10 years.
It makes me think of like a canvas that like a painter would paint on and it's been painted over and painted over, right?
Yeah, exactly.
Cool.
All right.
So the next major area was the modules folder and this is where the standard library C implementation goes, right?
Yeah.
So there we have the main.c, which is the Python interpreter main program, which is pretty cool.
And also GC module and stuff like that, right?
Like memory allocation and whatnot.
Yeah.
So one of the ones we dig into quite a lot is main.c.
So this is some of the really high level APIs for initializing the Python application.
So the binary that you would run.
So there's different ways that you can run Python.
One that you typically use is just by typing Python on the command line or Python.xe.
And that basically goes through a sort of a high level binary.
And then that either takes an argument, which is the name of the file you want to run or the library you want to run or the module.
Or you can even do, you know, Python dash C, for example, and actually give it a string with some Python code.
So all the wrappers around that are in both this main.c and also another file called pymain.
And then also there's a Python API, which you can call at the C level.
So the Python binary is actually just a wrapper for the Python C API.
And the Python C API, you can also import and use from your own application.
So you can actually write an application in C that has embedded Python.
And it uses exactly the same code, compilation, parsing, everything that you get when you type Python at the command line.
So there are some practical uses for that.
So there's some big applications out there that have Python kind of like built into them.
One would be like a 3D designer called Houdini, which is like a 3D graphics tool.
This kind of uses Python like deeply integrated into it, you know, using the C APIs.
Yeah, that's really cool.
I don't think people do that maybe as much as they should, right?
Because saying the way that you extend our application is not to go write tons of C code that you could forget to allocate something and crash the whole program.
But here, just write a little simple Python code and it makes our main app go.
I think the folks in the movie and 3D game space use that a lot in a lot of their tools and actually use Python to like kind of drive those pipelines, the automation of a lot of the tooling like you're talking about Houdini.
Yeah, that's pretty cool.
So you spend a whole lot of time in the article diving into those different things and how that works.
That's pretty cool.
And then I guess the last interesting, there's lots of interesting stuff, but there's the last one that I want to call out on the different sections of different directories is the whole Python one.
And this is where the Python runtime lives as opposed to the standard library or maybe the stuff that starts up the processes or the builds.
Like this is where the execution happens, right?
So the Python directory in the source code is really the brain of the whole application.
So it's basically how it does the evaluation of the top codes, which is the sort of low level assembly code that Python ends up building.
The Python run.c, ceval.c.
So these are highly optimized C files, which have been written in and changed over time that basically executes the Python code.
For sure.
And so ceval.c, this is the big one when it comes to execution.
This portion of Talk Python to me is brought to you by the University of San Francisco.
Learn how to use Python to analyze the digital economy in the new master's in applied economics at the University of San Francisco.
Located at the epicenter of digital disruption, USF is the ideal launching pad for the next phase of your career.
Their new STEM designated economics program doesn't just provide technical training in high demand skills like machine learning, causal inference, experimental design, and econometrics.
It takes the next step, teaching you how to apply these techniques to understand the economics of platforms, auctions, pricing, and competitive business strategy in the world of big data.
The program is open to beginner and to advanced coders looking to apply their skills in a new area.
Applications are now open for the fall 2020 classes.
To learn more and get an application fee waiver, go to talkpython.fm/USF.
That's talkpython.fm/USF.
Maybe talk about the process of going from Python source code, what we would think of as what we wrote, to getting to Python bytecode before we get to CofLC.
Like, what's the high-level flow there?
Okay, cool, cool.
So, first things first.
So, you've written some Python code in a file, I'm assuming.
So, first of all, it has to read the file, which is actually non-trivial because you've got to think about encodings and all sorts of other fun things.
Then, basically, the parser will go through and take the file apart and put it into something called an abstract syntax tree.
So, there's basically, actually, before that, there's a step called tokenization, which is to split the application into components.
Then it goes into an abstract syntax tree.
You can use the AST module.
And in the article, I talk about how to use the AST module.
And I wrote a small web application called Instaviz, which you can download on GitHub.
Nice.
What does it do?
It basically represents the Python syntax in a massive tree that you can explore, like an interactive tree.
And you can write Python code and it will give you the abstract syntax tree in, like, web application, like an interactive D3 graph.
So, you can kind of play around and see.
So, it's a bit easier to see how that tree works with the syntax and understanding the difference between a new line and an indent and what a name is and things like that.
So, yeah, the tokenizer will kind of look at the file and wrap it up into tokens.
And then the next step will be to put that into an abstract syntax tree.
Once it's got the abstract syntax tree, then it will essentially compile that by doing a depth-first search.
And then it puts it into something called a concrete syntax tree.
That is more of a sort of literal interpretation.
So, an abstract syntax tree says that you've got an if statement and inside the if statement you're comparing two variables and you're checking that they equal each other.
And then if that is successful, then you're going and doing these three extra lines of code, which you've nested with a tab, for example.
So, that's what an abstract syntax tree would look like.
The concrete syntax tree, the CST, is basically a bit more low level than that.
So, it's actually saying at the lower level, like, here's the statements we need to execute.
And then the compiler is another step, which basically takes the concrete syntax tree and converts it into a list of opcodes.
And this is the Python bytecode, basically, which is no longer a byte.
It's actually a word.
But, yeah, there's a separate issue.
They're long words.
Yeah, they're long words.
Yeah, yeah.
So, I think that's interesting because a lot of folks think of Python as a scripting language.
They think of it as, like, this thing that is not, like, one of the things that defines what Python is that it's not compiled.
But you just said there's a compiler.
Yes.
So, it absolutely is compiled.
It's compiled into an intermediate language.
So, similar to .NET and Java.
So, .NET and Java both have JITs.
So, just-in-time compiles and execution.
But Python is a bit different.
CPython, that is.
PyPy does have a JIT.
But CPython compiles down to an intermediate language as well.
It's kind of like Csharpen.net or like Java.
But where it differs is what happens when you try to execute that, right?
In, like, Java, it would JIT that to machine instructions.
In Python, it takes this big bunch of these opcodes and feeds them off to ceval.c.
Yeah.
There's a switch statement.
The opcodes end up getting cached in a PYC file or in newer versions in the Dunder PyCache folder.
So, if you've ever noticed that when you've run a Python application, it creates this cache folder in your application directory.
So, basically, it goes from the source code all the way through to almost like the compiled code.
And then it puts that in a cache folder.
So, it does actually do the compilation.
And then the next step is to execute that.
It's a literal list of statements that it works through.
And it has a frame stack and a value stack, which I talked through in the article because it takes a bit of time to get your head around those.
But in a nutshell, the frame stack is if you called a function inside your Python code, then you'd expect the local variables to be different, for example.
And you couldn't just reference the stuff that you were using beforehand.
So, there's essentially these frames.
And then also there's a value stack, which is used by the opcode so that it doesn't really understand the different variables you have.
They're just all on the pile.
And you can add things on top of the pile and you can remove things from the pile.
So, that's essentially how it works.
Right.
It might load three things onto the value stack and then call the function with that, right?
Something to that effect.
So, the opcodes are really low-level statements, essentially.
So, they're sort of push and pop values from the stack, for example, or to initialize a new list or to initialize a new variable or to call a C function.
That's cool.
If you want to explore those, there's the disk module, right?
And people can import, they can say, from disk, import disk, and then they can start taking a function and asking what bytecodes or opcodes make up this thing, right?
Yeah.
I've got a small snippet of code which will work in Python 3.7 and above.
So, in 3.7 and above, you can actually do, in the sys module, there's a tracing flag, which you can enable.
And you can run a piece of code and it'll actually print out where you are in the frame stack, what opcodes are being executed,
and you can inspect the value stack as well.
So, if you want to almost, like, play around with the runtime and see what's happening live when I run this piece of code,
there's a snippet in the article, which we'll link to in the show notes as well, where you can basically see the frame stack live.
That's pretty awesome.
So, you can just get it to just dump, like, every bit of what it's up to, huh?
Yeah, and I got it to nest as well, so that, you know, as you go deeper down in terms of the frame stack,
it will pad things out further and further to the right, so you can see where you are in the tree as well.
Yeah, that sounds like you'd almost have to have it or you'd just go crazy.
That's cool, though.
It sounds super useful if you're just trying to understand.
Yeah, so then eventually it goes down to ceval.c, which is a very complicated piece of C code.
It has a lot of macros as well, which makes it quite hard to follow.
But essentially, it's a big loop.
So, it's a big for loop, essentially, and it goes through each opcode in that frame and executes it.
So, it's a big for loop, and inside the for loop, there's a massive switch statement, which says, you know,
if it's this opcode, do this, if it's that opcode, do this.
And then for each one, it typically calls a C function.
So, you know, if you're going to load a variable onto the value stack, then it would fetch the variable and push it onto the stack.
Like, there would be, you know, two or three lines of code for each opcode.
Some of them are a bit more complicated.
So, you can do, let's say, set add, for example, is a basic opcode.
So, if you had set add, you're adding two sets together, then basically it would call py set add, which is a C API.
So, most of the opcodes actually just call C functions.
Right, they take the variables that are on this value stack and they just go call the C function with that.
Yeah.
Yeah, this is like a serious switch statement.
It's, you know, people haven't looked at it yet.
It's like three, four thousand lines long.
I don't have it pulled up just right a second.
Another thing that surprised me is there's some interesting flow control mechanisms in there.
Like, go-to.
There's a lot of go-tos.
How did that, did you feel like when you saw that?
Were you like, what is this?
The go-tos make sense because basically there's some optimizations in here.
So, sometimes you'll get opcodes that typically come in pairs.
So, what they've done is over time they've said, okay, you know, if you're going to create a new list, then, you know, you're going to create a new name and initialize a new list.
And those two opcodes are going to end up being next to each other.
So, in the switch statement, it actually kind of has like shortcuts in the code so that it knows that if it's running this opcode, the chances are it's probably going to run that opcode next.
So, it basically shortcuts a lot of the other inspections and a lot of the other checks.
Also, there's a lot of go-tos in terms of yields.
So, if you're yielding values back as well as doing errors.
So, if you call a function and the function crashes or if you try and store an attribute but there's some sort of error at the low level, then it will go to like a generic error section which basically starts off the whole exception process.
It makes sense because it's so highly optimized.
Like, forget doing it the right way.
If the go-to makes it a little bit faster, get in a go-to, right?
Because this is the hot loop that runs every single thing that happens in the language.
Yeah, this thing is going to run thousands and thousands and thousands of times.
So, you want it to be as fast as possible.
I'd say that in terms of micro-optimizations, I think they've pretty much done most of what they can.
There was some stuff introduced in 3.7 to do with the fast method calls.
Right, for methods without keywords, right?
Yeah, so the fast method calls were to do with if you're calling a method in a class that doesn't have keyword arguments, then it's about 20% faster.
And you can see in this loop, you can actually see that opcode and how it works as well.
So, it's pretty cool.
I would say if you are trying to understand how CPython executes code, the C eval C and the switch statement, this is the place to start, right?
It's a good place to understand.
I wouldn't say start here.
I'd say get here.
I'd say work your way towards it.
If you just jump in, it's not going to make a whole lot of sense.
Right, okay, fair enough.
So, another thing that you spent a fair amount of time on and you tied it back to the underlying C API was memory management.
Yeah, this is definitely one of my weaknesses in understanding.
I thought I understood how memory management worked in CPython.
But the more I looked into it, the more complicated it actually is.
There's basically different types.
There's different ways it allocates blocks and also arenas.
And there's basically a Python version of, or a CPython version of Memalik.
So, instead of calling Memalik directly from C, you're supposed to call the sort of CPython version, which has got more governance and also more cleanup.
Yeah, and it also does a bunch of work to try to avoid fragmentation and stuff like that.
So, I think it's interesting.
I don't think people talk about memory management in Python very much.
On one hand, like, who cares?
Whatever.
We don't have to worry about it.
Hooray, right?
That's, like, one of the reasons we like the language.
Like, I'm so done with calling Malik and free.
I just don't want to do that again.
But on the other hand, just having a conceptual understanding of what is happening at a pretty good level helps you think through, this algorithm might be better than that.
Or if we're having these memory problems, we might be able to, you know, do something slightly different in terms of how we're using, like, how we're defining our code.
Like, you know, maybe to take advantage of not work against the way it works, but to work with it, right?
Yeah, one of the biggest benefits to the CPython is that everything basically comes from PyObject.
So, the core type of an object, which is used by integers and strings and lists and everything, including the objects that you define, everything kind of comes from the same type.
So, the memory management is very optimized because basically everything inherits from something.
So, you know that the structure at least has this fixed size.
So, what they've done is they've built in these utilities for allocating sections of memory on your machine so that you can store objects easily and fetch them and reference them.
So, that's something called the PyArena, which is referenced quite a lot in the code.
And you'll see, you know, when you add objects to the arena, where that goes to.
So, basically, it's a way of putting Python objects into memory.
And also, that's where the PyArena malloc kind of comes from, which is to do with object memory allocation.
So, that's the really sort of low-level memory allocation techniques that are used in size-seat Python, which are optimized around the size of the PyObject type and typically the types of memory that are requested and the way that they're used.
But if you're using Python at the Python level, you'd never care about that sort of thing.
You just expect that when you declare an object, it has its memory.
Like, you know, it figures that out itself.
But at the Python layer, you definitely do need to know about the reference counter and the garbage collector if you're writing applications which run for any long period of time.
Yeah.
Well, you talked about Java and .NET before.
Those are both market sweep garbage collecting type of systems.
If you go back to something like C or C++, it's manual.
You know, maybe you could create a smart pointer in C++ and then that's kind of reference counting.
But Python's interesting, I think, because it has this blend, right?
It's like, well, we're going to do reference counting, which is pretty awesome and predictable and deterministic and fast.
Except for when you have cycles.
Like, that's the main weakness of reference counting, right?
You cannot break a cycle because the reference count is never going to go below one.
Or if you have two things that refer to each other, like, how do they become garbage, right?
So we have this GC that also runs.
But yeah, it's pretty interesting.
And you could see some of that happening that you're exploring there.
I'll go through the garbage collection module.
So there's GC module.
And you can actually put debugging in the GC module from the Python layer.
So if you import GC and then run GC set debug, you can basically turn on debug statistics so that when you're even at the REPL, like if you want to assign variables and stuff like that, you can see like what's happening at the garbage collector and what the threshold is and when it runs.
And you can also customize how many cycles the garbage collector runs at.
Because basically the garbage collector, I've taken the easy analogy that it's like the trash trucks that come and pick up your garbage.
Like it doesn't make sense for them to come every time you put something in the bin.
They come once a week or once a fortnight.
So you can basically customize that.
So you can say how many cycles until it goes and checks which objects are no longer needed and which ones don't have any references anymore.
And it'll go and clean those up.
Another thing that's interesting, the two things that are interesting that you can write in Python code that are fun to play with to give you a better understanding is you can write some code.
I think you've got to do some C imports, but you can basically ask how many references are there to this object ID.
Right.
And it'll tell you there's five or whatever and so on.
The other one that you can do that's interesting is to play with the weak reference type to create a weak reference to a thing.
Then see if it's still alive or not.
Right.
Because that doesn't still let you address that thing, but not actually keep it alive by holding a pointer to it.
Right.
The garbage collector's debug stats are a great way of doing that because it kind of dumps that information to the to the repo as well.
So, yeah.
Another thing you can do is implement the Dunderdell, I think.
You can actually get it to print out when an object is finalized or deleted.
Yeah.
Dunderdell is really useful for doing any of the custom cleanup code.
So it's almost like you have a constructor and Dunder in it and then you have a destructor as well.
Yeah, exactly.
So while we're on this memory stuff, let me just throw out this really quick, this article by Instagram.
It's a little bit old.
It's a couple of years old now, but it's called Dismissing Python Garbage Collection at Instagram.
I think maybe I covered this on Python Bytes long, long ago, but it says because you can import GC and say GC.disable or GC.collect now.
Right.
You can take a little bit of control.
Not necessarily that you should, but you can.
And over at Instagram, they said they can run 10% more efficiently by disabling GC and reduce the memory footprint and improve the CPU LLC cache hit ratio on their Django servers.
That's a very focused use case.
But they happen to do that.
I'll put a link to that in the show notes.
So you can also see about playing around the GC, some over there and whatnot.
Awesome.
Yeah, pretty wild.
All right.
Oh, let's see.
So another thing that I think will be fun to talk about, we've got just a little bit of time left, not much, is just objects in the Python data model.
Right.
So the objects folder is where all that stuff lives.
And we talked a little bit about object C, but there's more stuff that defines the object, the data model, right?
Like dunder iter, dunder enter, dunder exit, repper, all that kind of stuff, right?
There's basically a list of these core in the Python data model.
And actually, I referenced Luciano Romano's book, which if you want to understand.
Yeah, it's a fluent Python.
It's a great book.
Yeah.
Fantastic book.
And if you want to understand the Python data model and how to really leverage it to write fluent Python, then I'd recommend checking out that book.
It's available in pretty much every language now.
So that's awesome.
I think it's a must read for any Python programmer.
But basically, if you are writing a custom type that was a sequence, so it was a sequence of items, then you'd have dunder len, for example.
You can customize what the length is.
You can say dunder contains or you can do slicing.
You can do repeats, concatenation.
So you can kind of override the behavior of these kind of core operators.
But that stuff is actually built in to the object.
So there's a list object.
If I pick on a list as an example.
And in the list object type, there is something called sequence methods, which is basically in the data model, the dunder methods that are in place for anything which is a sequence.
So a byte array, for example, or a list.
So you can go in there and just look at all the different parts or aspects of the core data model.
It's spread across all these different classes, right?
It's not just all jammed into object.c.
Like you said, there's a list object.c and an iter object.c and so on.
Yeah, so there's basically these core types.
I think there's about 20 of them, the core types.
So you'd recognize them, things like dictionaries and modules and methods, memory and long objects.
So yeah, it's interesting to dig through them.
If you want to look at them, I'd say don't jump into the Unicode object first because it's probably one of the most complicated.
So the Unicode object is the string type.
The old string type basically doesn't really exist anymore.
You can do byte arrays.
So there's a byte array object type now.
But if you want to look at strings, the Unicode object is in there.
But it's hugely complicated because it has to deal with all the encodings and all that magic.
Yeah, wide character pointers and all that.
Yeah, no thanks.
I'll start somewhere else.
So, you know, that about covers it for our guided tour through the actual source code.
But maybe we could talk just really quickly about a couple of things before we're out of time.
You talked about doing a lot of stuff with pytest and doing testing.
What's the story around testing in Python, Python source code?
In CPython, there's a huge test suite, which takes a lot of time to run.
It's both.
There's a all the tests are written in Python, which is great.
They are written using unit tests.
They're using the unit test module.
And they run using concurrent processes as well.
Because there are so many tests to run in the test suite.
There's basically a it doesn't call unit test directly.
It actually runs like this test runner, this custom test runner that they've built.
So inside the test module for CPython, it'll test both the standard library module behaviors,
as well as the parser, as well as the core runtime, as well as the APIs.
So, yeah, like I said, there's a huge test suite.
The simple ones to understand, I guess, are the tests which are focused on the standard
library modules because it's Python code testing Python code, which is fairly simple.
And then if you look at the C layer, then basically the way that you wrap C code and call it from
Python, essentially using that from the test code to test different functionality.
There's also documented somewhere is the sort of coverage for the different parts of Python.
Some standard modules, standard library modules have a pretty low test coverage.
So if you do want to get started somewhere, adding tests is always a great place to have a look.
And you'll find some of the more obscure modules as well have little to no tests.
Yeah. So you could write some potentially, right?
Yeah, definitely. You could write some.
And, you know, when you're writing tests, you might come across a few bugs that you want to fix as well,
which is awesome. But the core runtime itself is heavily tested.
I'm sure it's super, super heavily tested.
The other thing that might be fun to talk about is, you know, now that CPython is over on GitHub,
it's really easy to go look at the branches and how they're working at those.
Maybe give people an overview of that.
It looks like it's pretty much focused around releases and not like, say, feature branches or
something like that.
Yeah. So they have released branches.
So if you were to go and look at CPython now, it would say Python 3.9,
which might take some of you by surprise because 3.8 only came out a week ago.
So actually what they do is they declare a sort of a feature freeze in the Python release cycle.
So the feature freeze for 3.8 actually happened a few months ago.
Basically when they go to beta, right?
Yeah, basically. So any new enhancements or stuff like that would go into the next version,
which is 3.9 and bug fixes would get merged back in.
There's a series of bots in the GitHub repository that do merging back of bug fixes and stuff like that.
So when you tag the issues or the PRs that you've raised with certain tags,
there's some really cool bots that Marietta wrote that will actually go and merge that back into the appropriate releases,
which is really cool.
Yeah, that actually sounds super awesome.
That's awesome. That's great.
I suspect people who are core developers or making contributions,
they might do feature branches, but not here in the main repo.
Yeah. So basically most of the core developers have their own fork
and they run feature branches on their own forks.
So if you want to look at some of the proposed peps,
then typically at the bottom of the pep, there'll be a link to example implementation.
And that typically sits on a fork of the CPython repo on that core developer's copy.
So you can actually see different experimental versions and different experimental features,
like some of the work that Eric Snow was doing on subinterpreters,
lived inside his fork, which is really interesting to explore.
But it doesn't live in the main Python, CPython repository.
Right. Not until it's officially accepted, part of it.
Yeah. And then when it gets officially accepted, they typically rewrite it anyway and clean up the code.
Yeah, I can imagine.
Oh, very cool.
And then I, you know, just it's worth throwing out that we're over on GitHub.com slash Python slash CPython.
But if you step it up one level just to the Python organization,
there's actually some other interesting projects there as well.
Right. We've got the peps are over there.
TypeShed, which are the stubs that define the static types for various Python things.
Python.org is there.
The dev guide that you referenced, a bunch of stuff to go play with, right?
Yeah, there's heaps of stuff on there.
Super cool.
All right, Anthony.
Well, this is really interesting.
Thanks for doing all the research, writing this great article, and walking us all through it.
It's going to be a good resource for years to come, I think.
Yeah, I'm hoping that people read this and get some value out of it, get some knowledge,
and hopefully fix some bugs, write some documentation, do some tests,
or maybe even add a new feature and get it merged into CPython.
Yeah, absolutely. You know, one really quick thing, you did mention that as you were writing this,
the source code that you were referring back to was changing, and you didn't want it to get out
of date. So you also did some cool extensions to keep the article up to date, didn't you?
In the article, I reference some of the functions actually a lot of times. So like thousands of times,
I reference a particular function or a particular file. And as I was writing the article,
obviously the code is not static, it's changing all the time. So what I ended up having to do was
actually write a markdown preprocessor in Python.
Were you inspired by the C macros you were seeing all over the place?
Yeah, I just saw so many macros. So I thought it'd be a good idea. So in the article,
like if I reference a function, you can click on it, and it takes you to the GitHub source code,
and it takes you straight to the line where that function is defined. But actually, that work was
done using Python using a preprocessor, so that I can basically refresh the article with newer versions
of newer versions of CPython as they come out, and it rewrites it for me.
That's awesome. I love it. That's really, really a cool way to approach it.
All right, so I think we have to leave it there for the guided tour. But I do have the two questions
to ask you, of course, before I let you out of here. I'm going to write some Python code. Maybe let's
change it. If we're going to work on this project, right, where it's got Python and C,
what editor are you going to use on it?
I use, thanks to the nice people, JetBrains. I've got access to PyCharm and CLion. CLion is the
C version of PyCharm. So it's made by JetBrains. It's very similar. So I use CLion for really deep
debugging and PyCharm for exploring some of the Python code. And then when I was writing on Windows,
I would use Visual Studio 2019. So I'd recommend either of those two stacks. You can use VS Code
in both environments as well. But for the deep debugging and stuff like that, then I found that
CLion was pretty good.
Yeah, that's cool. I was definitely considering CLion to explore this as well. But I'm not going
to compile it. I just want to walk through it and kind of pull some stuff out. So VS Code.
Super cool. All right. And then notable PyPI package. What have you run across lately that you're
like, oh, this is sweet?
That's a hard one, actually. I'm kind of working on a few at the moment.
Yeah, at the moment, I'm working on pytest support for Azure Pipelines. So if you search
for pytest as Azure Pipelines, if you're using Azure's new CICD service, and you're using pytest,
then please check out the module. It basically automates how you run pytest and upload test results.
And it gives you coverage automatically and stuff like that. So yeah, it's a really,
really simple package that I put together. And it's ended up being referenced in the documentation.
So it's become quite popular pretty quickly.
That's super cool. So it's becoming officially part of the way to work over there, right?
Yeah. And then there's also now a plugin for your GUI. So in the Azure GUI, if you install this plugin,
you can actually get all the pytest information in the UI as well.
Okay, that's awesome. People definitely have to check that out. All right, final call to action.
People are excited about CPython. You gave them some ideas about going back,
adding some documentation, adding some tests, looking for unsolved bugs, things like that.
What are you telling them?
Oh, yeah. Have fun as well. Like you can add silly features. You can do experiments.
You know, don't feel like you have to do something that's, you know, might seem like a chore at first.
Just experiment, see what you can break, see what you can change, and run your own custom fork.
And just I think you'll learn a lot by just experimenting.
Yeah, it's super easy to get started with the article you put together. So it's fun to experiment for sure.
All right. Well, thanks. Bye.
Thanks, Michael.
This has been another episode of Talk Python to Me.
Our guest on this episode was Anthony Shaw, and it's been brought to you by Linode and the University of San Francisco.
Linode is your go-to hosting for whatever you're building with Python.
Get four months free at talkpython.fm/linode. That's L-I-N-O-D-E.
Learn how to use Python to analyze the digital economy in the Masters of Applied Economics at the University of San Francisco.
Just go to talkpython.fm/USF to find out more.
Want to level up your Python?
If you're just getting started, try my Python Jumpstart by Building 10 Apps course.
Or if you're looking for something more advanced, check out our new async course that digs into all the different types of async programming you can do in Python.
And of course, if you're interested in more than one of these, be sure to check out our Everything Bundle.
It's like a subscription that never expires.
Be sure to subscribe to the show.
Open your favorite podcatcher 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.
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 really appreciate it.