If you work in data science, you definitely know about data frame libraries.
Handis is certainly the most popular, but there are others such as QDF, Moden, Polars,
Dask, and more.
They're all similar, but definitely not the same APIs, and Polars is quite different.
But here's the problem.
If you want to write a library that is for users of more than one of these data frame
frameworks, how do you do that?
Or if you want to leave open the possibility of changing yours after the app is built,
you've got the same problem.
Well, that's what narwhals solves.
We have Marco Garelli on the show to tell us all about narwhals.
This is Talk Python to Me, episode 480, recorded September 10th, 2024.
Are you ready for your host, please?
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 brought to you by WorkOS.
If you're building a B2B SaaS app, at some point your customers will start asking for enterprise features like SAML authentication,
skim provisioning, audit logs, and fine-grained authorization.
WorkOS helps ship enterprise features on day one without slowing down your core product development.
Find out more at talkpython.fm/workos.
Marco, welcome to Talk Python to Me.
Hey, thanks for having me.
Hey, it's fantastic to have you here.
We talked a little bit on the socials and other places, but, you know, nice to talk to you in person and about some of your projects.
Yeah, nice to finally do it.
Been listening to your shows for years, so it's a pleasure to be here.
Yeah, that's really cool.
It's awesome when people who are listeners for a long time get to come on the show.
I love it.
So we're going to talk about narwhals and data science, data frame, libraries, and basically coming up with a way to write,
consistent code against all these different libraries, which I think is an awesome goal, which is why I'm having you on the show, of course.
Before we get to all that, as you know, let's hear a little bit about yourself.
Sure.
So, yeah, my name is Marco.
I work at a company called Quantsite Labs, which supports several open source projects and also offers some training and consulting services.
I live in Cardiff in Wales and been at Quantsite for about two years now.
Originally hired as a Pandas maintainer, but then shifted considerably towards some other projects such as Polars.
And then in February of this year, I tried releasing this narwhals library as a bit of an experiment.
It's growing a bit faster than expected.
Yeah, it's a really interesting project.
Quantsite is quite the place.
You know, I didn't really know about y'all before having people on the show from there.
But here's my experience.
I've had I reach out to say some of the Jupyter folks, whatever.
Let's have let's have some of the Jupyter people on.
There's three or four people from Quantsite show up.
And then, oh, let's talk about this other project.
Another person from Quantsite two weeks later.
And then you're from Quantsite.
And none of those connections were like, let me try to find people from Quantsite.
I think you all are having a pretty big impact in the data science space.
That's cool.
Yeah, it is a bit amusing in the internal Slack channel.
If you ask a question, does anyone know how to do this?
Someone will reply, oh, yeah, let me ping this person who's a maintainer of that library.
And you're like, okay, well.
Exactly.
I think we know how it works.
Let's ask them.
Yeah, exactly.
Yeah, it's a big world, but also a small world in interesting ways.
Yeah.
How did you get into programming in the first place?
I think the first experience with programming I had was at university.
So I studied maths.
I think like you as well.
Yeah, yeah, yeah.
That sounds really.
Yeah, keep going.
So far, you're telling my story.
Yeah, sure.
Although my initial encounter with it, I didn't, I'm not quite particularly enjoyed it.
It was just having to solve some problems in MATLAB.
I did find it kind of satisfying that if you gave it instructions, it did exactly that.
But I wouldn't say that I felt like naturally talented or anything.
I then really took to programming, though, after I started a maths PhD and dropped out because it wasn't really going anywhere.
And once I went deeper into programming, then I realized, okay, actually, I do have some affinity for this topic.
I do quite enjoy it.
Yeah.
I think that's often the case.
What was your PhD focus before you dropped out?
It was meant to be, well, some applied mathematics, like stochastic partial differential equations.
But, you know, in academia, you publish or perish.
And I wasn't publishing and didn't really see that changing.
So I had to make a bit of a pivot.
I imagine you made a pretty good choice, just guessing.
I mean, I love math, but the options are just so much broader outside of academia.
In hindsight, yeah, I kind of wish that somebody at the time had told me that I could have still had a really interesting and rewarding career outside of academia.
And I shouldn't have stressed myself out so much about trying to find a PhD or about having to complete it when I had already started it.
The secret, I think, about being a good programmer is it's kind of like grad school anyway.
You're constantly studying and learning.
You feel like you've figured something out.
It's like, well, that's changed.
Now on to the next thing.
You're kind of like, well, we figured out pandas.
Now we got polars.
Okay, well, we're going to start over and figure out how to use that one.
All right.
So that is true.
You need to do a lot of learning, a lot of self-directed learning in particular.
It's really stimulating, I must say.
It is.
It's great if you want that.
If you want to just nine to five, you don't need to stress about.
Look, I think there's actually options there.
We'll get to narwhals in a second.
But I think there are options there.
I think if you want to do COBOL, FORTRAN, some of these older programming languages where so much of the world depends on them, but nobody wants to do them.
You could totally own that space and make really good money if you didn't want to learn anything.
But where's the fun in that, right?
Yeah.
It would be nice if we could all get rid of our legacy systems.
But, you know, this stuff does power the world.
They're there for a reason, right?
That works.
I would really like it if you don't touch it, please.
But that's not the way it is with narwhals.
Let's start with an overview of what narwhals is and why you created it.
And then I want to talk a bit about some of the data science libraries before we get too much deeper.
What is narwhals?
A narwhal is a cool whale as far as I know.
It's like the unicorn of the sea, basically.
What is this library?
So it's intended as a compatibility layer between different data frame libraries.
So narwhals does not do any computation itself.
It's more of just a wrapper around different data frame APIs.
And I like the Polar's API, so I figured that I should keep it fairly close to the Polar's API,
and in particular, to Polar's expressions.
As to why narwhals, so I was just getting frustrated with the fact that there's,
let's say about a year ago, there were relatively few libraries that supported Polar's.
And if libraries did support Polar's, it was often just done by converting to pandas or converting to pyarrow.
Yet a lot of these libraries, they weren't doing anything that complicated with data frames.
A lot of data frame consuming libraries, they don't really want to do that much.
They want to select columns.
They want to select rows.
Maybe they want to do some aggregations.
Like they're not doing stuff that's completely wild.
And so trying to design some minimal compatibility layer, I think, is a lot easier than trying to make a full-blown data frame API that end users are meant to use.
So the idea with narwhals is this is a tool for tool builders.
If library maintainers want to support different data frame libraries as inputs with minimal overhead and with minimal maintenance required on their side, this is the problem we're trying to solve.
It's a great problem to solve because maybe you want to have a library that works with an abstract concept of a data frame.
But usually I would imagine you have to start out and say, are we going to go and support Polar's?
Are we going to support pandas?
And the APIs are different.
Not just the APIs, but the behaviors.
For example, the lazy execution of Polar's versus the eager execution of pandas.
And so being able to just write your library so it takes you there is probably a big hassle, right?
Because it's just kind of have to have almost two versions each step, right?
Yeah, exactly.
Well, I actually heard from a maintainer recently who was saying that he was interested in using narwhals even just to have pandas as a dependency.
Because pandas, the API changes a bit between versions.
And he was getting a bit tired of pandas' API changes.
And was like, okay, well, if we can just defer all of the version checks and API differences to an abstraction there that might even simplify our life, even if we're just interested in supporting pandas.
Yeah, that's cool.
Yeah, actually, that's an interesting idea.
It's just like, we'll have a compatibility later, just in case.
And pandas went from one to two on major version recently, which is a big deal, and switched to PyArrow and all that, right?
Yeah, so version two, it was sometime last year, I think, 2023.
So yeah, the PyArrow, I think there were some misconceptions around that.
So as we're live on air, let's take the chance to address some PyArrow misconceptions.
PyArrow in pandas currently is optional, and it'll probably stay optional for quite a while.
So there is some talk about in version three using PyArrow strings instead of the classical NumPy object strings.
By default, if people have PyArrow installed, it's not totally decided.
It's not totally set in stone whether PyArrow will be a required dependency.
And maybe pandas version four will have it as a required dependency, and it'll be the default everywhere.
But that's a few years away.
Yeah, maybe.
Maybe we'll get Python four as well.
You never know.
You know, it's interesting.
I think the data science space, more than many other areas, has this ability to run Python in more places, right?
For example, there's Pyodide, there's Jupyter Lite.
There's a lot of more constrained environments that it might go in.
And I don't know what the story of PyArrow and WASM and all these different things.
You tell me you still get benefits there.
But there's a lot to consider.
Yeah, totally.
And I think that's one reason why some library maintainers are really drawn to a lightweight compatibility layer like narwhals.
With narwhals, you say you don't need any dependencies.
You need narwhals, but that's just a bunch of Python files.
Like if you wanted to, you could even just vendor narwhals.
Like it's not that big of a deal, but there's no extra dependencies required.
Like pandas users don't need polas installed, and polas users don't need pandas installed.
So if you're trying to deploy to a constrained environment where package size is limited, like if a library has narwhals as a required dependency, as opposed to any big data frame library, and then the user can just bring their own data frame.
Then like this, we're really minimizing the number of installation and dependency hell issues that people might run into.
I think you've covered dependency hell on the show a few times before.
Yeah, indeed. I think one thing that's interesting for people out there listening, we'll talk about the different libraries that it works with right now.
But if you have a library out there and you're listening, there's not too much work to integrate it into or make it narwhal compatible, narwhalification of a library to let it do this interchange, right?
So I think I can interpret your question in a couple of ways.
So I'll just play them back and let's just see.
Let's do it.
One is if you're a library that consumes data frames.
So yeah, there's some examples there on the readme of who's adopted narwhals.
So like Altair is the most recent, probably the most famous one.
I think that's where I heard of it was when I was, some news about Altair and narwhals together is actually how I heard of narwhals.
Okay, yeah. So yeah. And how complicated that is really depends on how complicated the data frame operations this library is doing.
In the case of Altair, they weren't doing anything that was that crazy.
They needed to inspect the data types, select some columns, convert date times to strings, get the unique categories out of categoricals.
It wasn't that bad. So I think within a few weeks we were able to do it. Same story with Psychic Lego.
There's some other libraries that have reached out that have shown interest where it's going to be a bit of a heavier lift,
but it's generally not as bad as I thought it was going to be when I started the project.
The other way that I think I might have interpreted your question is how difficult is it for a new data frame library to become narwhals compatible?
Yes.
And there's a couple of ways that they can go about doing that.
The preferred way is if they either write to us or open a pull request, adding their library as a backend in narwhals.
However, we love open source, but I don't consider myself an open source absolutist.
I understand that not everything can be open sourced.
So if somebody has a closed source solution, we do have an extensibility mechanism within narwhals such that somebody just needs to implement some dunder methods.
And then if they pass the data frame into a library that's been narwhalified, then narwhals will know how to glue things together.
And they'll be able to still support this closed source solution without it needing to go out into the open.
Right. It's kind of something like inheriting from a class and implementing some functions and then it knows, right?
Yeah, exactly.
Yeah. Yeah. Cool.
So right now it has full API support for CUDAF, C-U-D-F. I'm guessing that's CUDA data frame library?
Yeah, I'm not totally sure how we're supposed to pronounce it. I call it CUDAF.
Yeah, that came out of the Rapids team at NVIDIA. It's like an accelerated version of Pandas on GPU.
Yeah, that's been quite a fun one.
Nice. Yeah, I bet. That's pretty wild.
The API is quite similar to Pandas, but it's not exactly the same. So we have to do a bit of working around.
Right, right. Because graphics cards are not just regular memory and regular programs. They're weird, right?
Yeah, that's part of it. So there's some parts of the Pandas API which they intentionally don't support.
And there's another part of it is just that the Pandas API is so extensive that it's just a question of resources.
It's pretty difficult to reimplement 100% of the Pandas API. But Moden does attempt to do that. Moden does tell itself as a drop-in replacement for Pandas.
In practice, I think they do have a section in their docs where they do mention some gotchas, some slight differences.
But that's the idea. They've kind of got their own intermediate representation, and they've got their algebra, which they've published a paper about, which they then map onto the Pandas API.
A pretty interesting project that was a lot easier to support. The way they mimic the Pandas API is a lot closer.
But it's been interesting. Like with novels, we did find a couple of minor bugs in Moden just by running our test suite through the different libraries, which we then reported to them and they fixed very quickly.
That's pretty awesome. Yeah, yeah. That's super awesome.
So Moden lets you use Ray, Dask, or Unidisk. Unidisk.
Two of, one of which I know. One of which I've heard of. Two of which I've heard of.
So I was going to ask about things like Dask and others, which are sort of themselves extensions of InDesk.
But if you support Moden, you're kind of through one more layer supporting Dask.
Oh, but it's better. We don't have this on the readme yet, but we do have a level of support for Dask.
We've not quite put it on the readme yet because we're still kind of defining exactly where the boundaries are.
But it's going to be some kind of partial, lazy-only layer of support.
And it's actually quite a nice way to run Dask.
Like when you're running Dask, there are some things which do trigger compute for you.
There are some things which may trigger index repartitioning.
I think that's what it's called.
And in NowWals, we've just been extremely careful that if you're able to stick to the NowWals API,
then what you're doing is going to be performant.
Awesome. Yeah, that's super cool.
So one thing I think worth maybe pointing out here is you talked about Pandas, Pandas 1, Pandas 2,
and it being an extensive API.
I mentioned the eager versus lazy computation.
But these two libraries are maybe some of the most popular ones,
but they're pretty different in their philosophy.
So maybe just could you just quick compare and contrast Polars versus Pandas?
Yeah, sure.
So Pandas started a lot earlier, I think, in 2008, maybe first released in 2009,
and originally really written heavily around NumPy.
And you can see this in the classical Pandas NumPy data types.
So the support for missing values is fairly inconsistent across types.
So you brought up PyArrow before.
So with the PyArrow data types, then we do get consistent missing value handling in Pandas.
But for the classical NumPy ones, we don't.
Polars started a lot later.
It didn't have a lot of backwards compatibility concerns to have to worry about.
So it could make a lot of good decisions up front.
It's generally a lot stricter than Pandas.
And in particular, there's a lot of strictness and the kinds of ways it lets you interact with its subjects.
So in Pandas, the way we interact with data frames is we typically extract a series as one-dimensional objects.
We then manipulate those series.
Maybe we put them back into the original data frame.
But we're doing everything one step at a time.
In Polars, the primary way that we interact with data frames is with what you've got there on the screen.
PL.col, AB.
These are called expressions.
And that expression, my mental model for it is just a function.
It's a function from a data frame to a series.
And being a function...
It's like a generator or something, huh?
Yeah, kind of.
Yeah.
So although I think when you say generator, like in Python, a generator, it's at some point you can consume it.
Like you can type next on the generator and it produces a value.
But an expression doesn't produce a value.
It's like if you've got lambda x, x times two.
Yeah.
It doesn't produce a value until you give it an input.
And similarly, an expression like PL.col, A, B.
By itself, it doesn't do anything.
The interpretation is given some data frame DF, I'll return you the columns A and B.
So it only produces those columns once you give it some input data frame.
And functions, just by their very definition, are lazy, kind of.
Like you don't need to evaluate them straight away.
And so Polers can take a look at all of the things you want to do.
It can recognize some optimization patterns.
It can recognize that maybe between some of your expressions, there are some parts that are repeated.
And so instead of having to recompute the same thing multiple times,
it can just compute it once and then reuse that between the different expressions.
Yeah, that's one of the big features of big capabilities of Polers is that it has kind of a query engine optimizer in there.
Whereas Pandas, because it's not lazy, it just does one thing, then the next, the next.
But maybe if you switch the order, like first filter and then compute versus compute and then filter,
you might get a way better outcome, right?
That's a massive one, yeah.
So when I was doing some benchmarking, we brought up QDF earlier.
So that's the GPU accelerated version of Pandas.
And that is super fast if you're just doing single operations one at a time in a given order.
However, there are some benchmarks where maybe you're having to join together multiple data frames,
and then you're only selecting certain rows.
At that point, it's actually faster to just do it on a CPU using a lazy library like Polers,
because Polers can do the query optimization.
It can figure out that it needs to do the filter and only keep certain rows before doing five gigantic joins.
Whereas QDF, it's super fast on GPU, but it is all eagerly executed.
It did way more work, but it did it really fast, so it was about the same in the end.
Yeah, but now in Polers, there's going to be GPU support.
And it's going to be query optimized GPU support.
I don't know if the world is ready for this level of speed.
Yeah, that's going to be interesting.
This portion of Talk Python is brought to you by WorkOS.
If you're building a B2B SaaS app, at some point,
your customers will start asking for enterprise features like SAML authentication,
SKIM provisioning, autologs, and fine-grained authorization.
That's where WorkOS comes in, with easy-to-use APIs that'll help you ship enterprise features
on day one without slowing down your core product development.
Today, some of the fastest-growing startups in the world are powered by WorkOS,
including ones you probably know, like Perplexity, Vercel, and Webflow.
WorkOS also provides a generous free tier of up to 1 million monthly active users for AuthKit,
making it the perfect authentication layer for growing companies.
It comes standard with useful features like RBAC, MFA, and bot protection.
If you're currently looking to build SSO for your first enterprise customer,
you should consider using WorkOS.
Integrate in minutes and start shipping enterprise plans today.
Just visit talkpython.fm/workos.
The link is in your podcast player's show notes.
Thank you to WorkOS for supporting the show.
I guess another difference, it's not a massive, you know, in some ways it matters,
some ways it doesn't, is Pandas is based on C extensions, right?
I'm guessing, if I remember right.
And then Poolers is Rust, and they even took the .rs extension for their domain,
which is really embracing it.
Not that it really matters, you know, what your native layer is, if you're not working in that, right?
Like, most Python people don't work in C or Rust, but it's still interesting.
Well, I think it, yeah, it is interesting, but also it can be useful for users to know this,
because Poolers has a really nice plugin system.
So you can extend Poolers with your own little expressions, which you can write in Rust.
And the amount of Rust that you need to do this is really quite minimal.
Like, if you try to write these Poolers plugins as if you were writing Python,
and then just use some LLM or something to guide you,
I think, realistically, most data scientists can solve 98% of their inefficient data frame usage
by using Poolers plugins.
So having a nice, safe language that you can do this, it really makes a difference.
I'm going to write it in Python, and then I'm going to ask some LLM.
Right now I'm using LLM Studio and I think Llama 3.
Anyway, ask it, say, okay, write this in Rust for me.
Write it as a Polers plugin.
Here we go.
All right.
Yeah, exactly.
It's crazy.
It's this new world we live in.
Yeah, yeah, totally.
I mean, like, the amount of Rust knowledge you need to take care of some of the complicated
parts in Polers is really advanced.
Really need to study for that, and LLM isn't going to solve it for you.
But the amount of Rust that you need to just make a plugin to solve some inefficient function,
I think that's doable.
Right.
Yeah, exactly.
It's very different to say, we're going to just do this loop in this function call here
versus there, rather than I'm going to write a whole library in Rust or C or whatever.
Exactly.
Yeah, so there's a pretty different API between these.
And in Narwhals, it looks like you've adopted the Rust API, right?
A subset of it.
Is that right?
The Polers one, yes, exactly.
So I kind of figured we've got a few choices.
That's what I mean.
Yeah, the Polers one.
Yeah, yeah.
We can either, like, just choose the Pandas API.
But to be honest, I found that trying to translate the Pandas API to Polers was fairly painful.
Like, Pandas has a bunch of extra things, like the index, multi-index, and it does index alignment on all the operations.
I just found it not a particularly pleasant experience to try to map this onto Pandas.
However, when I tried to do the reverse of translating the Polers API to Pandas,
it kind of just worked without that much effort.
And I was like, oh, wow, this is magic.
Okay, let's just take this a bit further, publish it on GitHub.
Maybe somebody would find a use case for it.
I don't know.
Yeah, that's great.
Out of the audience.
ZigZackJack asks, how is Narwhals different from IBIS?
All right.
The number one most common question.
Love this.
Is it?
Okay, great.
Yeah.
Maybe we should provide a bit of context for the listeners on what is IBIS.
So IBIS, yes, you can see there on the screen, they describe themselves as the portable data frame library.
So IBIS is really aiming to be a data frame library, just like Pandas, just like Polers.
But it's got this API, which can then dispatch to different backends.
The default one is DuckDB, which is a really powerful embedded analytics database.
I think you covered it on the show.
In fact, I think I might have first heard about DuckDB on Python Bytes.
So listeners, if you want to stay up to date, subscribe to Python Bytes.
Thank you.
Yeah.
One of the shows I almost never miss.
So yeah, I think the primary difference between Narwhals and IBIS is the target audience.
So with IBIS, they're really trying to be this full-blown data frame that people can use to do their analyses.
Whereas with Narwhals, I'm openly saying to end users, like if you're an end user, if you're a data scientist, if you're an ML engineer, if you're a data analyst, don't use Narwhals.
It's a tool for tool builders, like learn Polers, learn DuckDB, learn whatever the best tool is for your particular task and learn it well and master that and do your analyses.
On the other hand, if you're a tool builder and you just need to do some simple operations with data frames and you want to empower your...
If you want to enable your users to use your tool, regardless of which library they're starting with, then Narwhals can provide a nice bridge between them.
Interesting.
Is there any interoperability between IBIS and Narwhals?
We do have some level of support for IBIS.
And at the moment, this is just interchange level support in the sense that if you pass a IBIS data frame, then you can inspect the schema, not do much else.
But for the Altair use case, that's all they needed.
Like they just wanted to inspect the schema, make some decisions on how to encode some different columns.
And then depending on how long your data frame is, they might convert to PyArrow and dispatch to a different library called Vega Fusion, or they might just do everything within Altair.
But we found that even just having this relatively minimal level of support for IBIS, Vax, DuckDB, and anything else, anything that implements the data frame interchange protocol was enough to already solve some problems for users of these libraries.
Yeah.
Okay.
Very interesting.
Let's see.
We'll hit a few more of the highlights here.
100% test coverage.
You already mentioned that you found some bugs in...
I think it's...
Which library was it?
Modian.
Yeah, yeah, that's right.
I think all of them.
I think it's helpful to uncover some rough edge cases in all of the libraries that we have some support for.
You write a library and you're going to say, I'm going to try to behave like you do.
And I'll write some tests around that.
And then when you find the differences, you're like, wait a minute, right?
Yeah, exactly.
Also really love to see the let your IDE help you thanks to static typing.
We'll definitely have to dive into that in a bit as well.
That looks awesome.
Cheers.
Yeah, a huge fan of static typing.
You know, it's a bit of a controversial topic in some Python circles.
Some people say that it's not really what Python is meant for and that it doesn't help you prevent bugs and all of that.
And I can see where these people are coming from.
But when I've got a statically typed library and my IDE is just always popping up with helpful suggestions and doc strings and all of that, then that's when I really appreciate it.
Exactly.
Like, forget the bugs.
If I don't have to go to the documentation because I hit dot and it's immediately obvious what I'm supposed to do, that's already a win, right?
And that's typing gives you that.
Plus it gives you checking.
Plus it gives you lots of other things.
I think it's great.
And especially with your focus on tool builders, tool builders can build tools which have typing.
They can build better tools using your typing, but they don't because it's optional.
It's not really forced upon any of the users.
The only libraries that I can think of that really force typing on their users is Pydantic and FastAPI and a couple of these that like Typer that have behavior driven on the types you put.
But if you're using that library, you're choosing that as a feature, not a bug, right?
Yeah, exactly.
Yeah.
So awesome.
And then finally, sticking with the focus on tool builders, perfect backwards compatibility policy.
What does this mean?
This is a bit of an ambitious thing.
So when I was learning Rust, I read about Rust editions.
So the idea is that when you start a Rust project, you specify the edition of Rust that you want to use.
And even as Rust gets updated, if you write some project using the 2015 edition of Rust, then it should keep working essentially forever.
So they keep this edition around.
And if they have to make backwards incompatible changes, there's new editions like 2018, 2021 editions.
So this is kind of what we're trying to do.
Like the idea, the idea was, well, we're kind of mimicking the Polar's API.
I think there was a bracket I opened earlier, which I might not have finished, which was that the third choice we had was to make an entirely new API.
But I thought, well, better to give to do something that people are somewhat familiar with.
Yeah, I think that's a great choice.
Yeah.
Because when you go and write the code, half of the people will already know Polar's.
And so they just keep doing that.
You don't have to go, well, here's a third thing you have to learn, right?
Yeah.
I'd like to think that now half people know Polar's.
Unfortunately, I think we might not quite be there yet, but it is growing.
No, I think so too.
Yeah.
Yeah, yeah.
I think we'll get there.
So yeah, it's okay.
We're kind of mimicking a subset of the Polar's API and we're just sticking into fundamentals.
So that part should be relatively stable.
But at some point, presumably Polar's is going to make a backwards incompatible change.
And at that point, what do we do in Narwhals?
What do we do about the top level Narwhals API?
And coordinating changes between different libraries, it's going to get tricky.
And the last thing that I want to do is see people put upper bound constraints on the Narwhals library.
I think upper bound constraints on something like this should never really be necessary.
So we've tried to replicate what Rust does with its additions.
The idea is that we've got a stable V1 API.
We will have a stable V2 API at some point if we need to make backwards incompatible changes.
But if you write your code using the V1 stable Narwhals API, then even as new Narwhals versions come out, even as the main Narwhals namespace changes, even as we might introduce V2, then your code should, in theory, keep working.
Like V1 should stay supported indefinitely.
This is the intention.
Yeah.
And you said see the stable API for how to opt in.
So how do you, I'm just curious what the mechanism is.
So for example, import Narwhals.stable.v1 as NW, which is the standard Narwhals.
Yeah, exactly.
So instead of.
I got you.
That's cool.
Yeah, instead of import Narwhals as NW, you'll do import Narwhals.stable.v1 as NW.
And yeah, I encourage people when they're just trying it out, prototyping, use import Narwhals as NW.
If you want to make a release and future-proof yourself, then switch over to the stable.v1.
This is a little similar to the API.talkpython.fm/v1 slash whatever versus, you know, where people encode a different version in their API endpoints, basically.
Yeah, yeah.
In import statements.
I like it.
I like it a lot.
It's great.
Yeah, let's see how this goes.
Yeah, exactly.
Now it's good.
So just back to typing real quick.
Pamphil Roy out there says, a lot of open source maintainers complain about typing because if you want to make it really correct, it's painful to add.
That can be true.
And so, you know, the last 1% is some insane statement.
But it's so helpful for end users.
True, yeah.
You mentioned earlier that everyone seems to be at QuantSight.
Do you know where I met Pamphil?
QuantSight?
He was an ex-colleague of mine at QuantSight, yes.
Amazing.
See, it continues to happen.
Yeah, exactly.
But yeah, I think that totally sums it up for me as well.
You know, it's really great to be using libraries that give you those options.
You know, we do have the PYI files and we have TypeShed and all of that where people can kind of put typing on things from the outside that didn't want to support it.
But if it's built in and part of the project, it's just better, you know?
Yeah.
If you have it from day one, it works well.
I mean, trying to add types to a library that started without types like Pandas, it's fairly painful to be honest.
I bet it is.
Yeah, really cool.
All right.
Let's go and talk through, I guess, a quick shout out.
Just last year, I had Richie Vink, who's the creator of Polars on Talk Python.
If people want to check that out, they can certainly have a listen to that.
And I also just recently had Wes McKinney, creator of Pandas, on.
And I'll link to those shows if people want to, like, dive into those.
But let's talk a little bit through your documentation.
It tells a really good story.
I like what you put down here as, you know, it's not just here's your API and stuff, but it walks you through.
So we talked about why, obviously, install, pip install.
It's pure Python with a pure Python wheel, right?
Yeah, exactly.
Shouldn't be any issues with installation.
Is it WASM compatible?
Do you know?
Could I use it on PyScript, Pyodide?
I don't know.
Are there any restrictions that they need?
There's some restrictions.
For example, I don't think you can do threading.
I don't think you can use some of the common, which you don't have any dependencies, but some
of the common third-party HTTP clients because it has to go through the browser's AJAX layer.
There's some, but not terribly many restrictions.
I'd imagine then that we would only be limited by whichever data frame people are passing in.
Yeah, yeah.
Awesome.
Okay.
That's super nice.
And maybe let's just talk through a quick example here, keeping in mind that most people
can't see any of the code, but let's just give them a sense still of what does it look
like to write code that is interoperable with both or all these different libraries or these
data frame libraries using narwhals.
So maybe give us just an example.
Sure.
So the idea is what we can see on the screen is just a very simple example of a data frame
agnostic function.
We've got a function called my function.
And this is something that users could maybe just use.
Maybe it's something your library exposes, but the user doesn't need to know about narwhals.
The narwhals only happens once you get inside the function.
So the user passes in some data frame.
We then call narwhals.fromNative on that data frame object.
We do some operation and then we return some native object back to the user.
Now the narwhals.fromNative, it's a practically free operation.
It's not doing any data conversion.
It's just instantiating some narwhals class that's backed by your original data frame.
Right.
And I imagine if it's Polar's data frame that gets passed in, it's probably a more direct
pass through to the API than if you're doing operations on a pandas data frame.
Right.
Is there a difference of sort of runtime depending on the backend?
The overhead is really low even for the pandas case.
In fact, sometimes things do get a little bit faster because of how careful we've been about
avoiding index operations and unnecessary copies.
To be honest, some of this will be alleviated in pandas version three when copy on write becomes
the default.
Oh, that's interesting.
Yeah.
In terms of the mapping on the implementation side, it's a bit easier to do the Polar's
backend.
But even then, we do need to do some version checks.
Like in 0.20.4, they renamed with row count to with row index, I think.
And so, yeah, even there, we do need some if-then statements.
But like the end of the day, what the library does is there's a few extra function calls,
a few checks on versions.
It's not really doing that much.
Yeah.
Like you might experience an extra millisecond compared to running something natively at most.
And usually you're using a data frame because you have some amount of data, even hundreds
of rows.
It's still most of the computation is going to end up there rather than if it's first in this,
call that, otherwise call this.
Right.
That's not a lot of overhead, relatively speaking.
I agree.
Yeah.
So, yeah, we see an example here of a data frame agnostic function, which just calculates
some descriptive statistics from an input data frame.
Using the expressions API, which we talked about earlier.
Yeah.
And here's something that I quite like about mkdocs.
So you see where it says, let's try it out.
We've got these different tabs and you can click on like polars, pandas, polars lazy.
And then you can see in each case what it looks like from the user's point of view.
And you can see, you can compare the outputs.
So from the user's point of view, they're just passing their object to funk.
What they're not seeing is that under the hood, funk is using narwhals.
But from their perspective, they put pandas in, they get pandas out.
They put polars in, they get polars out.
That's awesome.
So we talked about the typing.
And in this one, we have a DF typed as a frame T.
Is that some sort of generic?
And does it have restrictions on it?
What is this frame T?
I didn't dive into the source and check it out before.
Sure.
Yeah.
It's a type fur.
So it's just the idea that you start with a data frame of some kind and you get back some
data frame of the same kind.
Start with polars, get back polars, start with pandas, get back pandas, and so on.
And yeah, this version of the function is using the decorator nw.narwhalify.
Narwhalify.
It's a fantastic verb.
Yeah.
So there's two ways in which you can implement your function.
You can do it the explicit way where that's in the quick start in the docs where you write
your function that takes some native frame and then you convert that to this narwhals one.
You say from native, then you do your work.
And then depending on, you could convert it back or in this case, it returns a list of strings
in that example.
Or you can skip the first and the last step and just put this decorator on it and it'll
convert it to or wait, convert it from and then convert it to on the way in and out, right?
Yeah, exactly.
So if you're really strict about type annotations, then using from native and to native gives you
a little bit of extra information.
But I think narwhalify looks a little bit neater.
Yeah, that's true.
So for example, in the first one, you could say that this is actually a pandas data frame because
you're writing the code or something like that.
I don't know.
What is this into frame?
This is the type on this first example.
Yeah.
By into frame, we mean something that can be converted into a narwhals data frame or lazy frame.
How do you implement that in the type system?
Is it a protocol or what is this?
It's, yeah, we've got a protocol.
So I just found some methods that these libraries have in common.
Exactly.
Which wasn't too much.
You can find that.
That's what I was thinking.
Yeah.
Okay.
Yeah.
But if it has enough of the functions of pandas or pollers, you're like, all right, this is
probably good.
All right.
And you can say it's one of these.
That's pretty cool.
Yeah, exactly.
I mean, if any of this is confusing to listeners, we do have a page there in the documentation
that's all about typing.
So people can read through that at their own leisure.
Yeah, for sure.
All right.
Let's see.
I do like the MKDocs where you can have these different examples.
One thing I noticed is you've got the pollers eager evaluation and you've got the pollers
lazy evaluation.
And when you have the pollers lazy, this function decorated with the decorator, the Narwhalify
decorator, it itself returns something that is lazy and you've got to call collect on, right?
So it kind of preserves the laziness, I guess.
Is that right?
Yes, exactly.
This was something that was quite important to me, like not be something that only works
well with eager execution.
I want to have some level of support such that lazy in can mean lazy out.
Yeah.
Eager in, eager out.
Lazy in, lazy out.
Okay.
Exactly.
Yeah.
So the way you do that in pollers is you create a lazy frame versus data frame, right?
But then you've got to call collect on it, kind of like awaiting it if it were async, which
is cool.
Yeah.
Or don't call collect or just wait until you really need to call collect.
Right.
Or pass it on to the next one and on to the next.
Yeah, exactly.
Exactly.
So one of the things that you talk about here is the pandas index, which is one of the
key differences between pollers and pandas.
And you've classified pandas people into two categories.
Those who love the index and those who try to get rid of it and ignore it.
Yeah, exactly.
So if James Powell is listening, I think we can put him in the first category.
I think most realistically, most pandas users that I've seen call .reset index drop equals
true every other line of code.
They just find that the index gets in the way more than helps them most of the time.
And with novels, we're trying to accommodate both.
So we don't do automated index alignment.
So this isn't something that you have to worry about.
But if you are really bothered about index alignment, say, due to backwards compatibility concerns,
then we do have some functions which allow you to do that, which would be no operations for other libraries.
There's an example in scikit-lego of where they were relying on pandas index alignment.
So we've got a function here.
Narwhals may be a line index.
So for pandas-like, it'll do, the index will do its thing.
And for other libraries, the data will just be passed through.
Right.
So you said they're pandas-like.
And pandas-like is actually a type in your type system, right?
Did I see that?
Yeah, yeah.
So we've got is pandas-like data frame function to tell.
So by pandas-like, we mean pandas-qdf-modin.
So the libraries that have an index and follow those kinds of rules.
Yeah, yeah, that's really cool.
Yeah, because at the end of the day, like the idea of writing completely data frame agnostic code
is a lot easier for new libraries than for existing libraries that have backwards compatibility concerns.
And we recognize that it might not be completely achievable.
I think in all of the use cases where we've seen Narwhals adopted,
they're doing most of it in a data frame agnostic way,
but they do have some parts of it where they're saying,
okay, if this is a pandas data frame, we've got some pandas-specific logic.
And otherwise, let's go down the data frame agnostic route.
Yeah, you also have here levels of support.
You have full and interchange.
I think we talked about that a little bit.
So maybe just point people here.
If you want to be QDF or modin, you can fully integrate.
Or if you just want to have enough of an implementation that they can kind of work together, right?
You can do this data frame interchange protocol.
Yeah, exactly.
Or just write to us and we'd be happy to accommodate you without you having to go through the data frame interchange protocol.
Oh yeah, very nice.
Okay.
You mentioned the overhead before, but you do have a picture.
Picture's always fun.
And in the picture, you've got little different operations, different times for each of the operations.
And there's a quite small overhead for pandas versus pandas with Narwhals.
Yeah, exactly.
Like in some of them, you can see it becoming a little bit faster.
In some of them, you can see it becoming a little bit slower.
And these are queries that I think are the size that you can expect most data scientists to be working with a lot of the time.
You've got queries that take between a couple of seconds to 30 seconds.
And there, it's pretty hard to distinguish reliably between like the blue and red dots.
Sometimes one's higher, sometimes the other one's higher.
There's a bit of statistical variance just between running the same benchmark multiple times.
But overall, yeah, we were pretty happy with these results.
Yeah, that's great.
So how well have we covered how it works?
We talked about the API, but I know we've talked about the implementation of how you actually,
why is it basically almost the same speed?
Are you not doing, why is it not going to work?
Yeah, well maybe.
Are you using underwater unicorn magic?
Is that what it is?
That's the secret, yes.
Underwater unicorn magic.
Well, perhaps first I should just say why we wrote this, how it works.
And it's because really I want this to be a community driven project.
And this is one of those cases where open source is more of a social game than a technical one.
I'm not saying that's always the case.
There are many problems that are purely technical.
Now else is a social game in the end.
Like what we're doing isn't that complicated.
But if we want it to work, then it needs to be accessible to the community.
People do need to be able to trust us.
And that typically does not happen if it's a one person project.
So it was really important to me that different people would be able to contribute to it, that it all be as simple and clear as possible.
So we made this page trying to explain how it works.
It's not quite as clear and quite as extensive as it'd like to be.
But a few contributors did say that it really helped them.
So in terms of how do we get this slow overhead?
So we're just defining an expression as being a function from a data frame, your sequence of series.
And then we're just repeatedly and strictly applying that definition.
So there's nothing too fancy going on.
That's like in the end, just evaluating Lambda functions in Python, going down the stack trace.
Like it's pretty fast.
Yeah, that's really cool.
Yeah, so people can check this out.
They want to know.
I think this might be where I saw the pandas-like expression.
Ah, right, yeah.
Pandas-like.
Yeah, pandas-like is this class that encompasses pandas mode in QDF.
The ones that kind of follow the pandas API.
Yeah, close enough for what you need to do.
Yeah, exactly.
All right.
Well, I saw a question out there in the audience somewhere from Francesco, which was basically asking about the roadmap.
Like, where are you?
Where are you going?
Yeah, I should probably introduce Francesco.
He's one of the most active contributors to the project.
So thanks, Francesco, for helping to make it a success.
He was actually also the first person to adopt Narwhals in one of his libraries.
Yeah, I spoke to him about it at a conference.
And he was like, I've got this tiny little time-based CV library.
Let's try Narwhalifying it as an experiment.
Sure, we did that.
Then scikit-learn.
Not scikit-learn, sorry.
Not scikit-lego.
It was this kind of experimental building blocks for scikit-learn pipelines that he maintains.
And then we've just been taking it from there.
So in terms of roadmap, my top priority is helping out libraries that have shown interest in Narwhals.
So at the moment, Formulaic, that opened a draft pull request in which they were trying out Narwhals.
And they tagged me just about some things they were missing.
So I'd like to see if I can take that to completion.
I've got, I think I've got most of it working, but just been a bit busy with conferences recently.
So maybe next month I'll be able to get something ready for review and show that to them.
That would be pretty cool.
It's summer is passing.
The conferences are ending.
It's going to get dark and cold.
Perfect time to program.
Yeah, we'll get back to the situation that I was in when I started Narwhals, which was that it was a rainy Friday.
Not Friday, sorry.
It was a rainy February weekend in Wales.
The rainiest part of the UK.
Yeah, that's exactly the same in Oregon here.
So it's a good time to get stuff done.
Yeah, exactly.
So yeah.
And then I've been speaking to people from Shiny and Plotly about potentially looking into Narwhals.
There's no contract set in stone or anything.
These people may well change their mind if it doesn't work for them.
But my idea is, okay, they've shown interest.
Let's go headfirst into seeing whether we can help them and whether they'd be able to use Narwhals.
If it doesn't work out, we'll just have strengthened the Narwhals API and learn some new things.
If it does work, then great, exciting.
So that's my top priority.
And it's been really pleasing to see the contributor to community develop around Narwhals.
I really thought it would be a one-person project for a long time.
But so many people have been contributing really high-quality pull requests.
It's really been, yeah, AC42.
Okay, one of them is this.
Okay, maybe a couple of them here are like GitHub bots.
This pre-commit CI bot.
Yeah.
Maybe 40, 30, but still, that's a lot.
While we're talking numbers on the homepage, I also want to point out 10 million downloads a month is a lot of downloads.
That's awesome.
Yeah, that's maybe slightly misleading because they pretty much just come from the fact that it's now a required dependency of Altair.
And Altair gets millions of downloads.
Yeah, yeah, yeah, exactly.
But that's how, that's the place of some libraries.
Like Berksoig, I don't think many people go, oh, let me go get this HTTP library or it's dangerous.
They just go, I'm going to use Flask.
Right?
But it's still a really important building block of the community, even if people don't seek it out as a top-level thing they use, right?
Sure, cheers.
Thanks, yeah.
In fact, if we do our job well, then most people should never know about novels.
Exactly.
They just use, just work.
Yeah, exactly.
They just look in their pip list like, what is this whale thing in here?
Yeah, exactly.
Yeah.
So, yeah, it's been really encouraging, really pleasing to see this contributor community emerge around the project.
And I think a lot of the contributors are really interested in adding extra methods and adding extra backends and things.
So I'm trying to leave a lot of that to the community.
So, like with Dask, I just got the rough building blocks together.
And then it was just so nice, like so many really high-quality contributions coming up that brought the Dask support pretty much complete.
We should see now if we're able to execute all of the TPC-H queries with the Dask backend.
We might actually be there or be pretty close to getting there.
Nice.
What does TPC-H stand for?
I don't remember what it stands for, but it's a set of database queries.
I see.
They were originally written for testing out different databases.
So it's a bunch of SQL queries.
But I'm not sure if it was Kaggle that popularized the idea of translating these SQL queries to data frame-like APIs and then running different data frames on them to see who wins the speed test.
But we just figured they do a bunch of things like joins, concatenations, filtering comparisons with dates, string operations.
And we're like, okay, if the Nowels API is able to do all of this, then maybe it's extensive enough to be useful.
Yeah, yeah, yeah.
That's super cool.
It sounds a little bit like the TOB index plus other stuff maybe, but for databases.
I'm not familiar with that.
It's like a language ranking type of thing.
And, you know, one aspect is maybe ranking the databases.
But yeah, no, this is very cool.
Okay.
Got it.
In the end, we're not trying to be fast as Nowels, but we just want to make sure that there's no extra overhead compared to running things natively.
As long as you're not much slower than the stuff that you're operating with, like, that's all you should ask for.
You can't make it go faster in the extreme.
Like, you did talk about some optimizations, but you can't fundamentally change what's happening.
Yeah, we could do some optimizations on the Nowels side.
But to be honest, I'm not sure I want to.
And part of the reason is because I want this to be a pretty simple project that's easy to maintain.
Yeah, sure.
And that's really just low overhead.
Add extra docs and tutorials coming.
That's fun.
Yeah.
Are you looking for contributors and maybe want to write some tutorials or docs?
I would love this, yeah.
I mean, it drives me crazy when I see so many projects where people have put so much effort into making a really good product, but then the documentation is really scant.
Like, if you don't prioritize writing good docs, nobody's going to use your product.
So I was really grateful to my company.
They had four interns come on who really helped out with making the docs look amazing.
Oh, that's cool.
Like, if you look at the API reference, I think every single function now has got a, like a doc string with an example.
At the bottom, I think there's API reference.
Yeah, if you search for any function in here, yeah, in the search box at the top, some, I don't know, series dot something.
Yeah, see, for any of these, we've got like an example of, okay, here's how you could write a data frame agnostic function, which uses this method.
And let's show that if you pass pandas or polars, you get the same result.
And if there's some slight differences that we just cannot get around, like in the way that they handle missing values, then we've got a very clear note about in the docs.
Yeah, that's great.
Maybe someday support for DuckDB?
I would like that.
I don't think we have much of a choice about whether or not we support DuckDB.
Like DuckDB is really on fire now.
It really is.
Yeah, I think it might be a question of either we have some level of support for DuckDB, or somebody else is going to make something like novels that supports DuckDB, and then we become extinct.
But besides, to be honest, DuckDB is amazing.
I just find it a bit painful to write SQL strings.
And so if I could use DuckDB, but with the Polar's API that I prefer and I'm more familiar with, then...
Yeah, I 100% agree.
It looks super nice.
But if you look, it has a SQL example, and then the Python example is just SQL, quote, quote, quote.
Yeah, exactly.
Here's the SQL embedded in Python, you know what I mean?
So you're kind of writing SQL no matter what.
Yeah, and then the error messages that you get sometimes are like, oh, there's a pass error near this keyword.
And you're like, what on earth is going on?
And then you're like, oh, yeah, I forgot.
I've got an extra comma at the end of my select or something.
I don't know.
Yeah.
So DuckDB is a little bit like SQLite, but for analytics rather than relational, maybe.
I'm not sure if that's...
I think it's primarily aimed at analysts, yeah, analytical kinds of things, yeah, data scientists and people.
What we are going to struggle with is that in DuckDB, there's no guarantees about row order or operations.
But on the plus side, when I look at what Altair are doing with data frames, when I look at some of the other libraries that I've shown interest in now,
they're often just doing very simple things.
They're not doing things that depend on row order.
So if we could just initially just support DuckDB for the operations that don't require row order.
So for something like a cumulative sum, maybe initially we just don't support that for DuckDB.
Like in the end, if you want to do advanced SQL, just use DuckDB directly.
Like, as I said earlier, I don't recommend that end users use novels directly.
But even just having some common operations, ones that aren't row order dependent, I'd like to think that this is already enough to solve some real problems for real people.
Yeah, I know you said it's mostly for library builders.
But if you were building an app and you were not committed to your data frame library, or you really wanted to leave open the possibility of choosing a different data frame library,
using narwhals to isolate that a little bit might be nice, right?
Yeah.
So yeah.
Yeah.
If anyone tries to do this, and I'd love to hear your story.
I did hear from somebody in our community call.
We've got a community call every two weeks, by the way, if anyone wants to come and chat with us.
I did hear from somebody that, like, at work has got some teams that are primarily using pandas, some teams that are primarily using polars,
and he just wanted to build some common logic that both teams could use.
And he was using narwhals for that.
So I think there are some use cases beyond just library maintainers.
Yeah, absolutely.
Maybe you're building just an internal library, and it needs to work with some code you've written in pandas,
but you maybe want to try your new project in polars, but you want to still use that library, right?
That would be a cool use case as well.
Yeah, yeah.
To not lock yourself in.
Yeah, I'm pretty sure you've brought up on the show before that XKCD about, like, the spacebar overheating.
I don't remember which number that one is, but in the end, with a lot of open source projects,
you put it out with some intention of how it's meant to be used.
Yes.
But then people find their own way of using it.
I believe it was spacebar heating.
Workflow, this is it.
Yes.
Love this one.
Yeah, it looks like something out of a changelog with feedback or something that says,
changes in version 10.7.
The CPU no longer overheats when you hold down the spacebar.
Comments.
Longtime user 4 writes, this update broke my workflow.
My control key is hard to reach, so I hold the spacebar instead.
And I configured Emacs to interpret a rapid temperature rise as control.
That's horrifying.
Look, my setup works for me.
Just add an option to re-enable spacebar heating.
I've seen it so many times, but it still makes me laugh each time.
It's incredible.
All right, Marco.
Well, congrats on the cool library.
Congrats on the traction.
Final call to action.
Maybe people want to start using narwhals.
What do you tell them?
Yeah, give it a go.
And please join our Discord and or our community calls.
We're very friendly and open.
And we'd love to hear from you and see what we can do to address whatever limitations you
might come up against.
This has been another episode of Talk Python to Me.
Thank you to our sponsors.
Be sure to check out what they're offering.
It really helps support the show.
This episode is brought to you by WorkOS.
If you're building a B2B SaaS app, at some point, your customers will start asking for enterprise
features like SAML authentication, SKIM provisioning, audit logs, and fine-grained authorization.
WorkOS helps ship enterprise features on day one without slowing down your
core product development.
Find out more at talkpython.fm/workOS.
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.
Bye.
you
Thank you.