So, we have Samuel here to talk about Pydantic 2 and how it leverages REST superpowers.
Thank you very much. Can you hear me at the back?
Great. It's a bit about me. I'm Samuel. I've been a software developer for 10 years, among other things.
I've been doing open source quite a lot for the last five years, mostly Python projects, but moving a bit into REST over the last few years.
The most high profile Python project that I maintain is Pydantic, which I started back in 2017 and has subsequently kind of taken over my life.
I've been working on it full time for the last year.
So, what I'm going to talk about today, I'm going to give you a bit of an introduction to Pydantic, some hype numbers for some vanity, but also for some context of why making Pydantic better is worthwhile.
I'm going to explain why I decided to rebuild Pydantic completely.
I'm going to talk a bit about how I've done that with REST, and I guess most importantly why doing it in REST is the right choice.
I'm kind of preaching to the converted, but hey, what I'm not going to do is a like, hello world.
This is how you would build a Python extension in REST. There were lots of other talks on that. They're great.
And also the PyO3 documentation is amazing, so I think it's more interesting to go into a bit of depth on the challenges, the advantages than just to do the hello world example again.
What is it? Well, Pydantic is a data validation library in Python.
It's not the first. It's definitely not the last. It started off as a side project like so many open source projects.
Nothing special. I maintained it my spare time. People came along occasionally, said nice things, reported bugs.
Occasionally said not very nice things, and then something weird happened, and its usage went crazy.
So the first thing that happened, which you can't really see on this graph in 2018, my friend Sebastian Ramirez started the fast API project, which is a web framework in Python,
which uses Pydantic and has now got, I don't know, how many thousand stars, 60,000 stars or something.
It's got a lot of attention. You can see fast API growth there. That got a lot of people, I think, to first find out about Pydantic,
but something else happened at the beginning of 2021 to cause Pydantic's download numbers to go crazy.
Now, I'm well aware that Aaron Armin's speech at talk earlier kind of pre-trolled me before I'd even made my talk, saying that download numbers are a terrible metric,
but they are the only metric, so that's what we have to use. It's also worth saying that I have actually looked at Pydantic's downloads in terms of as a dependency and as a direct download.
It's not that easy to do with PyPI, but it looks like about 15 million downloads a month are from, as a dependency of another package,
and the remaining 25 or so million are people installing Pydantic directly, so it seems like people are using it not just as a dependency of another library.
I've included Django on there as the middle line, because it's the most high-profile, most well-known web framework in Python.
Not to be critical of it, it's amazing, it's changed my life. I mean, no disrespect by saying we've overtaken it, but just that Pydantic's usage has gone mad.
In terms of how it's used, it's used by lots of organizations you would expect, all the fang companies, something like 19 out of the top 25 people companies in NASDAQ,
but also by organizations which you wouldn't expect, like JPMorgan, use it quite a lot, I don't know in what regard.
But it's quite interesting, if you have an open source project, if you look in analytics at the referrers,
lots of those big, very security-centric companies forget to turn off the referrer header from their internal systems,
and they name their internal systems things like github.jbmorgan.net, so you can see which companies are using your dependencies by looking at those referrers.
So, for example, Apple have no public demonstration of using Pydantic at all, but six different enterprise instances of github within Apple use Pydantic,
and you can even see which ones they are. They're like maps.github.maps.apple.com, github.serie.apple.com, etc.
It's also used by some cool organizations. It's used by NASA for processing imagery from James Webb, and it's used by the international panel on climate change
for processing the data that they give to the UN on climate change every month, which is the stuff that I'm most proud of and why I want to make Pydantic better.
So, what's so great about Pydantic? Why are so many people using it? The short answer is I don't know, because you can't go and ask those people,
you can't look at a graph, but it can't really tell you, but we can kind of look at Pydantic and what people say, and we can kind of guess at what's made it popular.
So, this is, I know we're in the Rust room, we've got some Python code, don't worry, we'll get to Rust later. This is some Python code that demonstrates what Pydantic does.
So, we have a model which kind of represents a talk, which has four fields in this case. Obviously, title is a string, attendance is an integer,
the number of people who came, when, which is a date time, or none, and has a default value of none, and then the mistakes I make, which is a list of two pools with the time they were made at and a description.
So, and then lastly, last line, we instantiate an instance of talk using that data, and if there was a mistake, we'd get an error, if there wasn't a mistake, we'd obviously get the instance.
The first thing that makes Pydantic special, and the reason that people like it, is because we use Python type hints to define the types.
That's become reasonably commonplace now, there are a whole suite of different libraries that do the same thing, either because it's obvious or because they're copying Pydantic, but Pydantic was the first to do that,
because type hints were kind of new in 2017, and obviously, the main advantage is it's easy to learn, you don't need to learn a new kind of DSL to define stuff, but it's also compatible with static type checking with all the rest of your code, with your IDE.
Once you defined your model, and if Pydantic's worked correctly, then you know you've got a proper instance of talk.
The frustration that caused me to create Pydantic was that type annotations existed, they sat there in the code, you could read them, but they did nothing at runtime, and so, effectively, could we try and make them work?
The second and slightly more controversial thing that Pydantic does, which I think is one of the reasons that people find it easy to use, is because we default to coercion.
So, you can see a tendance there, although it needs to be an integer, it's defined as a string. Pydantic will automatically coerce from, for example, a valid string to an integer, but it'll also do other coercions that are a bit more commonplace like coercing a string as an isodate format into a datetime object, and same for the durations.
Some people hate that, some people complain about it a lot, I suspect that lots of people who don't even realize they're using it, they process environment variables, or JSON, or URL arguments, and they're always strings, and Pydantic just works and they don't even see it.
A few other reasons I think we're quite popular, we're fast-ish, we're friendly-ish on the bug tracker, I don't promise not to ever be cross with people, and we're reasonably feature-complete.
So, that was Pydantic, it's great, lots of people are using it, what's the problem? Well, it started off as a side project for me, it wasn't designed to be successful, and the internals stink.
I'm very proud of what Pydantic is doing in terms of how it's being used, I'm not proud of what's under the hood, and so I've been keen for a long time to fix the internals.
Also, second way in which I'm in kind of trouble before my talk was talking about API compatibility, we're going to have to break a lot of things in Pydantic V2 to get it right, but that's the right thing to do, I think, to get the future API to be correct and stable and not break again.
And while we're building V2, why don't we do some other stuff, so make it even faster, it's already quite fast, but if you think about that number of downloads, you think about the number of CPU cycles globally every day devoted to doing validation with Python,
but that's currently with Pydantic, that's currently all in Python, that's probably quite a lot of carbon dioxide that's being released, effectively unnecessarily, because we could make Pydantic significantly faster. Strict mode, I already talked about, because while often you don't need it, there are legitimate cases where you want strict mode.
We have functional validators, which is effectively running some Python code to validate a field, they're useful, but they would be more useful if they could operate like an onion, so like middleware where you take both a value and a handler,
and call the handler if you want to, once you've done some processing of the value, that would be super valuable, another thing we could add, composability.
So Pydantic, as I showed you earlier, is based on the Pydantic model, often your root type doesn't need to be a Pydantic model or shouldn't be a Pydantic model, it might be a list, it might be a tuple, it might be a list of models, it might be a type dict, which is a common new type in Python,
and then lastly, maintainability, since I maintain Pydantic, I want maintaining it to be fun, so about a year ago, last March, I started as a kind of experiment, could I rebuild some of it in Rust, a year later, I'm still working on it full time, and we're nearly there.
So what does it mean to validate Python data in Rust? What's the process? Well, phase one, we need to take a Pydantic model and convert it to a Rust structure, so unlike libraries like CERD, we're not compiling models in Rust,
the compiled Rust code doesn't know anything about the models it's going to receive, because obviously Python developers don't want to be compiling Rust code to get their model to work.
So we have to have a, in Rust terms, dynamic definition of our schema, which we can then use for validation.
The way we build that is effectively these validators, which are structs that contain both characteristics of what they're going to validate, but also other validators recursively such that you can define complex structures.
So in this case, our outermost validator is a model validator, which effectively just instantiates an instance of torque and sets its attributes from a dictionary. It contains another validator, which is a type dict validator, which contains the definition of all the fields, which have effectively the key that they're going to look for and then a validator that they're going to run.
The first two are reasonably obvious. I've added a few constraints to show how you would manage those constraints. And then the third one, the when field is obviously a union, which in turn contains a vect of validators to run effectively in turn to try and find the value.
And then the last one, which is the kind of more complex type, contains this list validator, which contains tuple validator, which contains two more validators. And we can build up effectively infinitely complex schemas from a relatively simple, I say relatively simple principle at the outset, which is we have a validator.
It's going to contain some other stuff. So what does that look like in code?
I said I was going to show you some Rust code. I'm going to show you some Rust code because I think this is the most clear way of explaining what it is that we do.
So the root of everything is this trait validator, which contains effectively three things. It contains a const, a static string, which is used for defining, as I'll show you later, which validator we're going to use for a given bit of data build, which is a simple function to construct an instance of itself in the generic sense.
And then the validate function that goes off and does the validation.
We then take all of those, well, we then implement that trait for all of the common types that we want. So I think we have 58, 48 or so different validators, and then we bang all of them into one massive enum.
Then the magic bit, which is provided by enum dispatch, which is a Rust crate that effectively implements a trait on an enum if every member of that enum implements that trait.
Effectively, it goes and does a big procedural macro to create an instance, an implementation of the function, which is just a big match, choosing which function to call.
But it's significantly faster than dine.
And in fact, in some cases, it can abstract away everything and be as fast as just calling the implementation directly.
So I said earlier that we needed to use this constant, the expected type.
We use that in another effectively big enum to go through and we take the type attribute out of this schema, which is a Python dictionary.
And we use that to effectively look up which validator we're going to go and build.
And again, I've shown a few here, but there's obviously a bunch more.
This in real life is not implemented as a big match statement like this.
It's a macro that builds this function, but it's clearer here if you get the idea.
So I showed you earlier this validate function and I kind of skipped over the input argument.
So the input argument is just an implementation of a trait.
That trait input is like the beginnings of which are defined here.
And it effectively gives you all the things that the validation functions are going to need on a value.
So is none strict string, lack string, int, float, et cetera, et cetera, but also more complex types like date, date time, dictionary, et cetera, et cetera.
And then we implement that trait on both a Python value and on a JSON value,
which means that we can parse Rust directly without having to go via Python.
That's super valuable for two reasons.
One for performance reasons.
So if our input is a string and if we were to then parse it into Python objects and then take that into our validator and run all of that,
that would be much lower than parsing in Rust and then running the validator in Rust straight away.
The other big advantage is to do with strict mode.
So I said earlier that people want strict mode, but they say they want strict mode, but often they don't.
So what people will say is, I want totally strict Python, why isn't it strict?
And then you'll say, well, do you want to load data from JSON?
And they say, yeah, of course I do.
And you say, well, how are you going to define a date?
And they're like, oh, well, obviously I'll use a standard date format.
But that's not strict then.
You're parsing a string.
And they're like, oh, that's fine because it should know in that case it's coming from JSON.
Well, obviously, how are we going to do that?
By parsing JSON directly and in future potentially other types, we can implement our strict date method,
both on that JSON input.
We can say, well, we're in JSON.
We don't have a date type.
So we're going to have to do something.
So we're going to parse a string.
And effectively, the strict date implementation for JSON will parse a string.
And therefore, we can have a strict mode that's actually useful,
which we wouldn't have had if we couldn't have had in pydantic v1,
where the validation logic doesn't know anything about where the date is coming from.
Even if we have a parse JSON function,
all it's doing is parsing JSON to Python and then doing validation.
So then that's all very well.
That defines effectively how we do our validation.
What's the interface to Python?
So that's where we have this schema validator rust struct,
which using the PyClass decorator is also available as a Python class.
And all it really contains is a validator,
which of course can in turn contain other validators, as I said earlier.
And its implementation, which are all then exposed as Python methods, are new,
which just construct it.
So we call the build validator and get back an instance of our validator,
which we then store and return the type.
Actually, this is much more complicated.
One of the cleverest and most infuriating bits of pydantic core is that we,
as you can imagine, this schema for defining validation becomes quite complex.
It's very easy to make a mistake.
So we validate it using pydantic core itself,
which when it works is magic,
and when it doesn't work leads to impossible errors,
because obviously all of the things that you're looking at as members of dictionaries
are in turn the names of bits of validation.
So it's complete hell, but it works,
and it makes it very hard to build an invalid or not build the validator that you want.
And then we have these two implementations,
two functions which do validate Python objects,
as I said earlier, which call validate,
and same with JSON, where we parse a JSON using SERD to a JSON value
and then call validate again with that input.
This code is obviously heavily simplified so that it fits.
It doesn't fit on the page, but nearly fits on the page.
So not everything is exactly as it really is,
but I think that kind of gives you an idea of how we build up these validators.
The other thing missed here,
we also do the whole thing again for serialization.
So the serialization from both a pydantic model to a Python dictionary
and from a pydantic model straight to JSON is all written in Rust,
and it does useful things like filtering out elements as you go along,
and it's effectively the same structure, uses the same schema,
but it's just dedicated to serialization rather than validation.
So what does the Python interface then look like?
So what I didn't explain earlier is that pydantic v2,
which is going to be released, fingers crossed in Q1 this year,
is made up of two packages.
We have pydantic itself, which is a pure Python package,
and then we have pydantic core, which is almost all Rust code.
We have a little bit of shim of Python to explain what's going on,
but it's really just the Rust code I've been showing you.
So what pydantic now does,
all that pydantic effectively takes care of is converting those type annotations
I showed you earlier into a pydantic core schema
and then building a validator.
So looking at an example here,
we obviously import schema validator,
which I just showed you from, that's come up at the wrong time,
from pydantic core,
and then the base level,
the schema for the base validator is model,
and it contains, as I said earlier, a class,
which is the Python class to instantiate,
and another schema, which in turn defines the fields.
And that inner validator is then defined by a type dict validator,
as I said earlier.
So this is completely valid Python code.
This will run now.
So yeah, we have a type dict validator which contains fields,
which in turn are those fields which I showed you earlier.
So title attendances of type int when I talked about earlier.
The most interesting thing here is,
if you look at the when validator,
it gets a bit confusing.
It's schema is of type default,
which in turn contains another schema,
which is of type nullable,
which is the simplest union, either a value or none.
The default validator contains another member,
which is the default value, in this case none,
and the inner schema is then nullable,
which in turn contains another inner schema,
which is then the date time.
So that's how we define effectively default values
and null or nullable.
So one of the other mistakes in Pyrantic in the past
was that we kind of conflate,
effectively Python made a mistake about 10 years ago
where they used, they had a alias for union of something and none,
that they called optional,
which then meant that I didn't want to have a thing
called optional, but was not optional.
And so we conflated nullable with optional in Pyrantic
and rightly it confused everyone.
And so the solution, the solution from Python
was to start using the pipe operator for unions
and to just basically ignore optional.
They can't really get rid of it,
but they just pretend it didn't really happen.
My solution is to define default
and nullable as completely separate things
and we're not going to use the optional type anywhere in our docs.
We're just going to use union of thing and none
to avoid that confusion.
And then I think mistakes, I hope it kind of makes sense to you.
Again, it's this like schema within schema within schema,
which become validator within validator.
And we take our code, as I showed you earlier,
run validation.
In this case, we call validate Python.
We've got some Python code,
but we could just as well have a JSON string
and call validate JSON.
And then we have a talk instance,
which lets us access the members of it as you normally would.
So where does Rust excel in these applications?
Why build this in Rust?
There are a bunch of obvious reasons to use Rust,
performance being the number one,
multi-threading and not having the global interpreter lock
in Python is another one.
The third is using high-quality existing Rust libraries
to build libraries in Python instead of implementing it yourself.
So I maintain two other Python libraries written in Rust
watch files, which uses the notify crate to do file-watching
and then RTOML, which, as you can guess,
is a TOML parser using the TOML library from Rust.
And the RTOML library is the fastest Python TOML parser
out there.
And actually watch files is becoming more and more popular.
It's the default now with u-vehicle,
which is one of the web servers.
But perhaps less obviously in terms of where Rust fits in best.
Deeply recursive code, as I've just showed you,
with these validators within validators.
There's no stack.
And so we don't have a penalty for recursion.
We do have to be very, very careful,
because, as I'm sure you all know,
if you have recursion in Rust and you don't catch it,
you just get a segfault.
And that would be very, very upsetting to Python developers
who've never seen one before.
So there's an enormous amount of, as a significant amount of code
in Pydantic Core dedicated to catching recursion,
we have to have, is it two or three different sorts of guard
to protect against recursion in all possible different situations,
because it's effectively the worst thing that we can have,
is that there is some data structure
that you can pass to Pydantic,
which causes your entire Python process to segfault,
and you wouldn't know where to even start looking.
So that's a blessing.
The lack of a stack is a blessing and a curse.
And then the second big advantage,
I think, of where Rust excels,
is in the small modular components.
So where I was showing you before,
these relatively small, in terms of code footprint validators,
which in turn hold other ones,
there's obviously no performance penalty
for having these functions in Rust.
I say almost, because we actually have to use box
around validators because they hold themselves.
So there is a bit of an overhead of going into the heap,
but it's relatively small, particularly compared to Python.
And then the lastly complex error handling,
obviously in Python, you don't know what's going to error
and what exceptions you're going to get in Rust.
Putting to one side the comment about panic earlier,
you can in general know what errors you're going to get
and catch them and construct validation errors
in the case of Pydantic, which is a great deal easier
than it would ever have been to write that code in Python.
So the way I want to think about the future development
of Python is not as Python versus Rust,
but effectively as Python as the user interface for Rust,
or the application developer interface for Rust.
So I'd love to see more and more libraries
do what we've done with Pydantic Core
and effectively implement their low-level components in Rust.
So my dream is a world in which,
thinking about the lifecycle of a HTTP request,
but you could think the same about some NL pipeline
or many other applications,
we effectively, the vast majority of the execution
is Rust or C,
but then all of the application logic can be in Python.
So effectively we get to a point where we have 100%
of developer time spent in high-level languages,
but only 1% of CPU dedicated to actually running Python code,
which is slower and is always going to be slower.
I don't think there's ever a world in which
someone's going to come up with a language that is as fast
and as safe as Rust, but also as quick to write as Python.
So I don't think it should be one versus the other.
It should be building the low-level, building the Rails,
perhaps a bad term, but the Rails in Rust,
and building the train in Python.
It doesn't work, but you get where I'm coming from.
Anyway, on that note, thank you very much.
A few links there, particularly thanks to the PyO3 team
who built the bindings for Rust in Python, which is amazing.
And if you want a laugh, there's a very, very funny issue
on GitHub where a very angry man says
why we should never use Rust.
So if you want to read that, I then took some time
to take them to pieces, which was quite satisfying,
although a waste of time.
So have a look at that.
Questions?
First, especially for the sanitation,
are you thinking to publish a library of Rust?
The job is already done,
and you could have a public API in a library
ready to validate Rust data.
I don't understand quite what...
So you wrote the library in Rust,
so could you publish just an API to validate JSON,
for example, from Rust, instead of through Python?
Absolutely, you could, and it would be useful
if you wanted to somehow construct the schema
at runtime fast, but it's never going to be anywhere
near as performant as said,
because you were not compiling...
We can't do anything at compile time.
Secondly, it's currently all completely intertwined
with the PyO3 library and the Python types.
So there is a future nascent possible project,
Tidantic, which is Pidantic for TypeScript,
where we take the PyO3 types,
we effectively replace them with a new library
which has a compile time switch between the Python bindings
and the JavaScript bindings or the Wasm bindings,
and then we can build Tidantic.
That's a future plan, but a long way off.
Right now, it wouldn't really be worth it,
because you would get lots of slowdown from Python
and from compile time.
So we need a completely different library,
just for us, like you're saying.
Yeah, SIRD is amazing.
I don't think I'm going to go and try and compete with that.
At least, it's great for that application.
Thanks for the talk.
Recently, I think the Python library cryptography
introduced Rust, and had some complaints
from people using obscure build processes
where Rust didn't work.
Are you expecting anything from that?
So I will actually bring up now.
Now I'm going to get into how to...
Effectively, go and read that issue,
where, among other things, I...
Oh, how do I get out of this mode?
So, rant, rant, rant, rant from him.
Effectively, I went through the...
just over a quarter of a billion downloads
over the last 12 months of Pydantic,
and I worked out looking at the distribution
of the different operating systems
and, like, libc implementations, et cetera,
that 99.9859% of people would have got a binary
if they had installed Pydantic Core then.
That number will be higher now,
because there will be fewer esoteric operating systems.
Most of the other ones, most of the failed ones,
if you look, are actually installing,
say, they're installing Python onto iOS.
I don't know what that means,
or whether it could ever work, but...
Also, the other thing I would say is,
Pydantic Core is already compiled to WebAssembly,
so you can already run it in the browser.
So I understand why people complained,
but I think it's not a concern for...
it's a straw man for most people.
So that's why you slapped down.
Yeah.
And, again, if there's another...
if there's a distribution that we don't...
if we release 60 different binaries,
if there's another one, we'll try and compile for it
and release the binary.
There's a question right at the back, I think, just to...
I'll get back to the talk rather than...
where are we?
Is there a way to use the Django models as Pydantic models?
Say again?
To use the Django to have, like, a binding
or to translate the Django model directly into a Pydantic model?
There's no way at the moment.
There's a number of different ORMs,
I know of, built on top of Pydantic,
which effectively allow that...
if you were wanting specifically Django,
there's a project called DjangoNinja
that makes extensive use of Pydantic.
I don't know that much about it,
but if you actually wanted Pydantic models,
you'd probably want some kind of code reformat to convert them.
So I look at DjangoNinja,
I'm sure what they're doing is the best of what's possible right now.
Okay, thank you.
If you had additional time, say, after finishing Pydantic,
are there any other projects where you'd like to follow this vision
of, like, a Rust core with Python user space or, like, API?
Yeah, there are a number of ones.
So there's already OR JSON, which is a very, very fast
if unsafe in the sense of littered with unsafe JSON parser,
which is very, very fast.
The obvious one is a web framework where you do,
like I kind of showed here,
like the HTTP parsing, the routing, all in Rust.
That's not very easy using ASGI.
There are already a few projects doing that.
So that would be the obvious one, but there's no winner yet.
Currently, the best low-level web framework is Starlit,
which FastAPI is built on,
but I think it does use Rust for, it uses a Rust library
for HTTP parsing or a C library.
So some of it's already happening, but no obvious candidate right now.
What I would say, though, is libraries like Rich,
no criticism of Will, but, like, Rich is incredibly complicated.
It's for terminal output.
It's not so much performance critical,
but it's really quite involved in complex logic.
I would much prefer to write that logic in Rust than Python.
Yeah, I think there are lots of candidates.
Tonya online is asking,
what do you mean by Python as the application layer?
So I guess I could have added some example code here,
but you can imagine a Python function,
which is a view endpoint in a web framework,
which takes in some validated arguments
from done by the Pydantic.
You then decide in Python to make a query to the database
to get back the user's name from the ID,
and then you return a JSON object containing data about the user.
If you think about that,
all of the code outside the Python functions,
excuse me, could be written in a faster language,
whether it be the database query accessing the database,
TSL termination, HTTP parsing, routing, validation,
but effectively using Python to define as a way
to effectively configure Rust code or configure compile code.
Yes, hello.
I have a question, just a Pydantic one.
Is there any support or are you planning any support
alternative schema types like Protobuf,
or JRPC, or Avro?
Possibly in future.
What I have a plan for is,
I don't want to build them into Pydantic.
Pydantic's already big, but there is a,
obviously you can parse them to Python now,
parse them and then validate them as a Python object.
There is a plan effectively to take the,
this, that's the one.
Which you would then construct in Rust,
parse as a Python value into Pydantic core,
which would then extract the raw underlying Rust instance
and then validate that.
And that would allow you to get basically
a Python validation effectively,
but without having to,
us having to either have compile time dependencies
or build it all into Pydantic core.
I think that's our last question that we have time for.
One comment we did get from Matrix was that
this code is a bit small on the,
on the display, so if you upload the,
I will do, yeah.
Perfect.
So if you're watching the stream,
the slides will be uploaded and you can read the code.
Oh, I'll put them on Twitter as well, but yeah, definitely.
I'll upload them as well.
Awesome.
Thank you very much.