You've built an awesome set of APIs, and now you have a wide array of devices and clients
using them. Congratulations. Then you need to upgrade an endpoint or make a change that is
meaningful in some way. Now what? That's the conversation I dive into over the next hour
with Stanislav Zmiev. We're talking about versioning APIs on this, the 450th episode
of Talk Python to Me, recorded January 8th, 2024.
Welcome to Talk Python to Me, a weekly podcast on Python. This is your host, Michael Kennedy.
Follow me on Mastodon, where I'm @mkennedy, and follow the podcast using @talkpython,
both on fosstodon.org. Keep up with the show and listen to over seven years of past
episodes at talkpython.fm. We've started streaming most of our episodes live on YouTube.
Subscribe to our YouTube channel over at talkpython.fm/youtube to get notified
about upcoming shows and be part of that episode.
This episode is sponsored by Neo4j. It's time to stop asking relational databases to do more than
they were made for and simplify complex data models with graphs. Check out the sample FastAPI project
and see what Neo4j, a native graph database, can do for you. Find out more at talkpython.fm slash
Neo4j. And it's brought to you by Sentry. Don't let those errors go unnoticed. Use Sentry like we do
here at Talk Python. Sign up at talkpython.fm/sentry.
Stanislav, welcome to Talk Python to Me. It's great to have you here.
It's good to be here. Yeah, it's good to have you here. I wonder what version that we're going to end up shipping of this episode.
We're going to talk about API versions.
API versions, API versions.
It's good to have many of them, you know?
Yeah, it's one of those problems that, you know, as soon as you create an API and people start to use it,
they don't want to keep rewriting their code.
So, but you need to evolve your API over time as it gets new features, as maybe more importantly,
you realize like, oh, that was a bad choice.
How do you fix this, right?
So that's kind of the theme of this episode.
We're going to talk about concrete tools that you can use to add versioning to various API frameworks in Python
and some of the ideas behind it.
So it's going to be a ton of fun.
Let's start, though, with your story.
Tell us a bit about yourself.
Previously, I have been a back-end engineer for six or even seven years now.
But recently, I have shifted into infrastructure.
Right now, I'm a platform engineering tech lead at Monite.
What I do is I make architectural decisions.
I mentor the teams.
I help the teams with Python-specific questions.
But mostly, I build Python-specific infrastructure.
And my main job is making our business teams happy.
I try to do that in my open source work as well.
I like to make my own open source projects.
I like to contribute to big projects, small projects.
It's all over the place.
And we're going to be mentioning one of them today.
Yeah.
You build a really cool library that we're going to talk about.
And that'll be a lot of fun.
It looks really useful.
And some other third-party ones as well.
So I wanted to ask you a little bit about what is the difference between being a back-end software developer,
which I think a lot of people get the sense of, like building APIs, building the back-end, at least, aspect of websites and so on,
versus, say, building infrastructure in Python.
Like, what are these two worlds?
How are they the same?
How are they different?
So when you're building them, especially when you're building products, it's very similar.
However, in back-end, there is already a lot of infrastructure around you.
There is already a lot of people who do portions of your job.
So, like, there are people who can sell your product.
There are people who are thinking about how to, you know, how to move it forward, how to evolve it.
There are people who are, you know, designing it, thinking about it.
But when you're a platform engineer, you've got to do all of that yourself.
And also, one big portion of any library framework or anything else, any developer tools development,
is the fact that unlike an API, well, a web API, a REST API, which has a very strict sort of interface
where the developer wouldn't be able to use it in a million ways.
A library, especially in a language as introspective as Python, the developers can do crazy things with your stuff.
So you've got to think forward for all of these use cases.
And you've got to be sure that you don't expose extra private interfaces.
Because once you do expose them, it won't be nice to break them in the future.
I guess it's a little more restricted with APIs than it is with handing out packages, right?
Because people, they can just dive into the packages and do whatever you want.
And the API is more or less limited by what the website's willing to process, right?
Yep.
On the flip side, you have code on the internet.
And anytime you have code on the internet, that's always a little stressful, right?
Just like whatever is out there, it's incredible.
If you have a website, you pull up the logs, how much abuse your website is taking constantly.
True.
The great thing about building packages is that you're not worried about what happens with them after.
You know, you just skim them.
And then if it breaks, it's not you.
It's not on your servers, hopefully.
But also, as I mentioned, you've got to both be its designer.
You've got to sell it.
You've got to people through that.
You've got to document it.
You've got to, as a platform engineer, you do all of the parts that backend engineers are
abstracted away from, or at least in most bigger companies, abstracted away from.
Yeah, super interesting.
All right.
Let's talk about, I guess, one more thing for your introduction before we move on.
You've worked on quite a few interesting projects here.
So maybe give us just a little bit of background on some of your open source work.
I'll just talk about, I guess, the most popular ones.
The coolest one was PathWalk.
If you remember, in Python, there is OSWalk.
But then in Python 312, I added PathWalk.
I have also added the type hints for that into TypeShed.
I think some other things.
And I am, I think, top 10 contributor of Tortoise ORM, the asynchronous ORM that claims to be the
fastest, or at least one of the fastest in Python.
And a few smaller projects, which I don't think there is anything particularly fun to talk about there.
It's just mostly bug fixes and small features.
But then my other projects, the ones that I maintain, are way cooler.
For example, Pydantic Duality enables you to have multiple versions of your Pydantic models for
almost free in terms of performance and without any code generation.
And Cadwin, the project we're going to be discussing today, implements, I guess, the fullest and most
sophisticated API versioning paradigm there is at this point.
Excellent.
Yeah.
A lot of cool stuff.
Pydantic Duality looks, yeah, Pydantic Duality looks really, really cool.
So a lot of neat stuff here.
A lot of different ways people might know about you.
So yeah, Cadwin is the one we're going to be focusing on for here.
So let's start with talking about APIs.
You talked about API-first companies.
What's that?
API-first company, just like any other term that gets popular, is a buzzword, obviously.
But the truth behind it is an API-first company is any company whose main product is an API
and the SDKs associated with it.
And as a result, API-first company has to document their API really thoroughly, think about their
API, both the internal one and the external one, and take great care of that because it's
their main product.
We have a lot of API-first companies right now.
But with every year, new industries are joining.
And if you've ever heard of open banking, things like that, banks are going to join us soon.
In some countries, they already have.
And Monite is doing similar things and tries to bring that innovation, that open banking,
that API-first approach into banking.
Yeah.
So the company you work for Monite, they build, you all build APIs to basically facilitate
other people doing invoicing and bill pay in their apps.
Is that correct?
Yeah.
You could call us a B2B2B company.
Because essentially, many banks, many marketplaces are very outdated in terms of how they handle
invoicing and payments.
And we try to take all of that, combine that, and offer that as a white label product to them.
So we do this for them.
They integrate with us once, and then they forget it was ever a problem.
They just give our product as theirs straight to their clients.
Yeah.
That's excellent.
So this seems like kind of in that category of API-first.
Oh, yes.
Yeah.
Okay.
It's cool that you have concrete experience with it, right?
Of course.
Give us some of the laws.
We coordinated a little bit beforehand.
And there's some interesting laws for designing good APIs.
Like one of them is the reverse Conway maneuver, which to me sounds like a jujitsu sort of
move that you might try to pull.
What is this?
Oh, yes.
So when you are designing any system, you got to think about the way it's going to end up
and the laws that are going to govern them.
And Conway's law is one of the main laws that do that.
To explain that to people in simple terms, if you have four teams designing a compiler,
you're going to have a four-pass compiler.
Essentially, the way you set up communications within your company, the way you set up your teams,
your structure is going to be reflected in your product.
And you can't do anything about it.
You can spend money.
You can coordinate people.
But at some point, Conway's law is going to affect you.
And it's going to be harsh.
So one of the ways to go around it is to first think of an interface that you want to provide.
And it applies to anything, not just APIs.
APIs is just a really good example of that.
And then design your company and the communications within your company, the teams within your company,
to reflect the interfaces that you want to get in the end.
And that's reverse Conway maneuver.
Okay.
Because your code and your APIs and all those things are going to reflect your company structure
and culture.
So instead of having that being implicit, make it more explicit by saying, well, this is the
endpoint that we want, the end result that we want.
How do we work that backwards to end up structuring teams or whatever to get this right?
Precisely.
Yes.
Yeah.
It is a little bit like jujitsu.
Anyway.
But there are more laws that do that.
Of course, there is REST, which I guess not really laws, but principles.
Yeah.
Which most people, or at least most companies I saw, don't really like to follow it to the
letter.
But if you do, if you do follow REST, if you try to make your API as available as you can
and as standardized as you can, then it's much easier for new people, especially junior
developers to learn.
And then that makes it much easier for your clients to integrate with you, which is great.
There is also, there are also a few, I guess, more advanced things.
For example, when you are designing an API, when you're doing microservices and when you
have many services talking to each other, which in the end provide your final product to your
customers, a good idea is to omit any internal APIs.
And make all of your APIs, or at least the majority of your APIs external.
That is also one of the things about API first.
Once all of your APIs are external, it means that your clients will be able to build a product
that is of similar quality to yours.
We're using your APIs, using your data, which provides them with infinite capabilities of using
you, which is great.
Yeah, that is great.
It's certainly frustrating when you see, you know, it's not on the API level, but you
see things in like the iOS platform or in Windows or wherever you're like, why can't I do this?
Like, these are private things that either Apple or Microsoft or whoever are keeping to themselves
and clearly are important for building apps, but you can't use them, right?
So you're suggesting that by having basically whatever APIs you need, I guess there's probably
limits, right?
Like you don't want to expose, say you're like HR API if you're not an HR company, right?
But in general, having your APIs just be all public, right?
It probably makes them better as well because they just get so many more use cases.
They get treated with things like versioning and stuff more seriously.
Exactly.
Excellent.
It's a complex topic that could span, you know, we could discuss it for hours, you know,
which parts of your API do you make public?
Which parts of your API do you start versioning?
And if you're combining your APIs using a gateway or a federation, you know, it's a long story.
But I guess with a federation, you wouldn't really make it public.
Sure.
Let's dive into that a little bit.
Let's talk about API versioning.
We know what versioning is in Python.
We have lock files.
We pin the version.
We might say we're using Pydantic 2.0.5 or whatever it is.
And that leaves the Pydantic team free to do whatever they want.
And we can trust that our code is going to stay the way it stayed.
I guess API versioning is that, but for web endpoints rather than software links to packages,
right?
It's a little different though, in some regards.
For example, when you're packaging your, I don't know, software and you just send it
to clients, thing is you need to have versions for every single thing.
For example, if we are doing a bug fix and we need to ship that to our clients, that is
going to be a separate version.
But in web, when you're hosting it on a server and your clients just make requests to your
servers, you don't need that.
You don't need to ship bug fixes as separate versions.
You can just ship them because they're not going to break anything for your clients and
your clients will be able to automatically get them.
Same goes for minor versions.
Minor versions that just add features don't really need to be for a version on APIs.
That leaves us with just major versions, which is why many companies like Stripe at Monite
use dates for their versions?
Because we don't really need all of the power of semantic versioning.
Yeah.
Sure.
More calendar versioning because all you really need to say is this one is different than before,
right?
It doesn't.
Exactly.
Stripe does a lot of interesting things.
We're going to talk a little bit about what they're up to.
But you're right.
When I think about APIs that I've created, sometimes I've had other versions, right?
You know, one of the main areas is in the apps that we have over URL work.
It does.
In the apps for consuming the courses, right?
We had two completely different versions of the app.
One got completely, completely rewrote it.
And many of the API endpoints were just fine.
We could just use them over and didn't have to change anything.
But three or four were like, you know, this really would be better if it was different.
And so added like a slash v2 or something in there, right?
But really, really limited.
So the thing that's interesting, though, that you point out here is like until that happened,
I'd shipped many, many versions of that API.
And long as the schema didn't change, I didn't bother even specifying that it had a version,
right?
It was just like slash course, my courses or whatever it is, right?
Like just give those to me.
As long as it kept giving the courses back, it was good.
That's the thing.
As long as you extend stuff, it's going to be fine.
But as you mentioned in the beginning, you can't really do that forever.
At some point, you're going to make wrong design decisions.
At some point, even if you did everything perfectly, you know, your product is just going to evolve.
It's just going to evolve.
And then you'll need to use old endpoints.
You know, you can't do that.
In some breaking ways.
Of course, there are ways around it.
You can use API evolution.
You can just, you know, add fields constantly and duplicate the old functionality.
But after a few of such versions, your API will look terrible, which is why we use versioning.
And which is why when your company's main product is an API, when you are an API first company, you can't do anything without versioning.
You can live for a short period of time.
For example, a pre-Series A company, I don't think should deal with versioning.
But then after a series A, after you gather, I don't know, 15, 20 clients, it will be really hard to survive without versioning.
Yeah.
And it's a really quick and easy way to make loyal customers angry by making changes to your API that just start breaking code.
Even if you wait for your clients, even if you, you know, warn them, it's still not nice when you're trying to use some API.
And then every three months or every month, it breaks everything.
You know, you wouldn't like that.
Even with a warning.
You put in all the work to do the integration.
You got it working.
You tested it.
Your job is, your product is not the integration.
The integration facilitates what you're trying to build.
And every time you got to go back and rewrite that stuff, it's not great.
I was working, I have MailChimp for our newsletter marketing side of stuff, the courses and use them for a long time.
And I got a message like, hey, we're retiring this API you're using.
And their API is dreadful.
Like their Python library API is dreadful.
It's like star args, star star kwo args.
You better pass the right things.
But there's no enforcement or instruction about what goes in there, right?
And so some random dictionary that was going in there was no longer right and had to be like recreated.
So they just decided, you know, we're going to just take different stuff and we're just going to shut down that endpoint.
And when they did, all of the aspects of my website just started breaking.
Like it wasn't crashing, but just anything around, hey, put me on your newsletter.
Like, nope, you couldn't be on the newsletter because it, you know, people started sending me a message.
Michael, I'm trying to sign up to be on the newsletter.
But it just keeps saying that something went wrong with our site.
We're sorry.
I'm like, oh, boy.
Go look.
And they just turned off that endpoint.
And it was just like, nope, not doing that anymore.
And it was super frustrating.
It took me eight hours to rewrite it because, like I said, there was no real guidance on like, oh, you just passed the dictionary.
Like, well, what goes in the dictionary?
You know, it was like some word of one of the keys changed.
I was so, it just took forever.
It was frustrating.
So what we're going to talk about here is a lot about how, how do you not do, how do you not pull a MailChimp?
How do you set it up so that your code keeps running?
You know, the reason why they did that is they're like, well, we feel like we wrote it wrong before.
We want to fix those for, to make it better, but we don't want to maintain two versions.
That's probably the heart of it.
And that's a sane idea.
It is.
Until you have half a million users of the old API.
And how many of those, like, so for me, yeah, I was frustrated.
Yeah, I'm like, all right, well, I had plans today for constructive work, but I'm going to go rewrite what I did four years ago.
And then I'll do something again tomorrow.
But, okay, that's like a pretty good outcome.
A more common outcome is probably I hired a consultant to build me an app and integrate MailChimp, and they're gone.
And now my thing that I built a year ago doesn't even work.
And I don't have the skills.
I got to go hire somebody again to dig back in there and fix it, right?
Like, that would be really frustrating.
Yeah, that's crazy.
And it could be devastating to some clients, especially if the changes are huge, especially if it's not just a single endpoint.
Or if there is only one endpoint, and it does a lot of things, then, oh my God, it's scary.
It is.
This portion of Talk Python to me is brought to you by Neo4j.
Do you know Neo4j?
Neo4j is a native graph database.
And if the slowest part of your data access patterns involves computing relationships,
why not use a database that stores those relationships directly in the database, unlike your typical relational one?
A graph database lets you model the data the way it looks in the real world, instead of forcing it into rows and columns.
It's time to stop asking a relational database to do more than they were made for and simplify complex data models with graphs.
If you haven't used a graph database before, you might be wondering about common use cases.
You know, what's it for?
Here are just a few.
Detecting fraud.
Enhancing AI.
Managing supply chains.
Gaining a 360-degree view of your data.
And anywhere else you have highly connected data.
To use Neo4j from Python, it's a simple pip install Neo4j.
And to help you get started, their docs include a sample web app demonstrating how to use it, both from Flask and FastAPI.
Find it in their docs or search GitHub for Neo4j Movies Application Quick Start.
Developers are solving some of the world's biggest problems with graphs.
Now it's your turn.
Visit talkpython.fm/neo4j to get started.
That's talkpython.fm/neo4j.
Thank you to Neo4j for supporting Talk Python To Me.
Before we move on to maybe some of the approaches, Alex, Alexey out in the audience says,
One more law.
Hiram's law is also great.
With sufficient number of users of an API, it does not matter what you promise in the contract.
All observable behaviors of your system will be depended on by somebody.
It is one of my favorite laws about any system.
That's true.
That is very true.
And it's a scary law, you know, because at some point you'll have to version everything, which is why it's a good idea to make things, you know, I guess to describe all the behaviors and to try to handle all of the possible behaviors early.
So that you know what is going to become a part of it.
Because at some point you'll just not be able to stop it.
And then you break something that was never intended to work.
And then a hundred of your users are like, oh, we were using that.
We were, it wasn't a bug.
It was a feature for us.
Yeah.
We needed that.
We found that's the only way we could use it.
Yeah, for sure.
All right.
Let's talk about some of the techniques you put together.
A nice thing here, like how to maintain hundreds of API versions and survive.
So what are some of the ways in which people version APIs?
First of all, it's really hard to really figure out how people do this and how to do it.
Because 90% of the time when you're going to be researching API versioning, what you're going to see is people having tough decisions between putting it into a header or putting it into a path.
But then when you ask, well, how do you version?
Nobody really has an answer.
Well, there are a few ways.
First of all, you can go from the lowest level and version everything, including your database.
You just make a separate copy of your app.
You can host it on a separate server, even a separate Kubernetes namespace and forget about it.
And, you know, you have two completely separate apps.
And now you need to have double the people for the maintenance or at least a smaller separate team to maintain the second product.
This approach is really nice if you have a product that cannot break.
Like there needs to be zero possibility of that breaking.
You know, if you are on a nuclear power plant, you'll probably do that.
You know, you can't have the old one broken and, you know, the new one working.
But it's the most expensive approach, obviously.
You can, however, limit that.
You can start versioning just the application, but not the database.
Okay, now we have the same thing, you know, a separate branch in our code, a separate, not in our code, in our repository, a separate Kubernetes namespace, whatever.
But now it's still expensive.
It's still a whole separate copy of everything you have built.
And now you have to make sure that the new one or the old one doesn't break the database because the database might have changed.
But some of the one of the versions of code might not know about that.
And then you realize that you go.
Yeah, the database.
This aspect of it sounds especially tricky, especially a relational database with super strong schemas.
Like, you know what?
That used to be a varchar and now it's an int.
And so, nope, it's not like it's just not going to work, right?
There's really little flexibility.
So even if you run the two things in parallel, then you almost have to have two databases.
And then like Emil points out and then you have to sync the databases.
And like, it's non-trivial.
Oh, yes.
And then you start realizing, okay, why do I need a separate branch and a separate namespace?
Let's just keep them within the same app.
Because as long as, you know, you have separate paths for this, even you can build separate swaggers from them.
You can have them within the same branch, within the same repository, just in different directories.
And you can just copy all of your code.
And now I call that versioning by suffering.
Because you think, oh, I'm going to version less this time.
You know, now I'm not versioning the database.
Now it's not a separate branch.
It's going to be much easier to make fixes in both places.
But now you have to support both of the code bases.
And once you update anything, any of the related resources, for example, the database models, you have to update them twice.
If you update any of the dependencies, you have to update them twice.
And that can get painful.
And after a while, if you have more than two of these versions, you're going to be asking yourself, okay, what is the difference between these versions?
And if you weren't careful with documentation and code review, oh, you're in for a treat.
Yeah.
But that's just the beginning.
You can version much less.
Now, instead of the whole directories, you can version just separate endpoints.
And I think that's the way that I recommend for everyone.
Everyone who is doing API versioning who wants to, you know, to be with the cool kids, I recommend this approach.
As long as you are a small company or an individual and you want something really simple, that's a good way.
You just copy a single endpoint that you have changed, create another schema for that, and then create another branch in your business logic.
Or maybe even no branch.
That's even better, you know, using the same business logic.
But then at some point, once you want to support many versions or your versions become too different, that's where you get a problem.
Because now all of a sudden, you have all of these, oh, I have this endpoint in this version, that endpoint in that version.
Oh, it's going to be bad.
Really bad.
Again.
We've been talking like there's the old version and the new version.
But in practice, somebody starts to depend on the new version, and then you evolve the new version, and it just, it combinatorially explodes and it's out of control.
Yeah.
Precisely.
It's going to be really bad for your developers.
Your clients are going to be fine with it.
Your developers are going to hate you.
Although not as bad as with the previous approach.
Now, the next one you get to is you start thinking, okay, what if instead of duplicating all of that stuff, I am just going to change the schemas.
So, okay, if I do need to rename my endpoint, I am going to duplicate it.
But most of the time, you're not.
Most of the time, you're just changing some field in some schema.
You're renaming it.
You're extending it, changing its type, you know, stuff like that.
That's like 90% of your use cases.
Okay, what if we just change the schemas and then have some serializers?
For example, you know, Django DRF loves that.
They love, you know, the serializers and re-serializers.
And you can just have each, we have five versions, and version one serializes into version five, version two serializes into version five, yada, yada, yada.
And so our client sends us a request, we re-serialize it into the correct version, then we return a response, we use the same business logic,
and then we serialize it back into each version.
Great.
Now, we have solved all the problems of the previous approaches.
Now, you need to realize that when you're doing something like this, you are in a little bit of danger of creating bugs in the new versions,
and these bugs cascading to the old versions, which hopefully you have tests to handle that.
But at least your clients are happy because, you know, you can support versions for a long time, and your developers are happy because the burden of support is now on the serializers.
But you still have to support these serializers.
It's still a lot of stuff.
You still have to, you know, put every field in there, you know, and take care of any problems that happen.
And each new version will change basically all of your serializers.
A little bit, yes.
But you won't be able to support 20 versions like that.
It's going to be crazy.
And now that's where we get to the final approaches, the approaches that I guess are the most sophisticated in the industry.
If you are making a backend for frontend, for your own frontend, then you can just use GraphQL.
GraphQL basically requires no versioning, and that's one of the benefits of it.
I would recommend that if you have developers that are, you know, good with GraphQL, that's great.
You can omit versioning altogether.
But if you're not, if it's a public API, then that's where Stripe's approach comes.
I think they were the first ones to publish an article about that.
It was Brander Leach, the author of this article.
He has supported API versioning at Stripe for a while, and he has helped us with building our API versioning approach.
Essentially, what Stripe did is said, okay, let's imagine that our developers are only supporting one version, the latest one, just like with serializers.
But instead of serializing every version to the latest one, let's say that each version knows how to serialize into the next version.
Kind of like database migrations.
Let's say that I return response from the latest version.
This response, or at least they call them, I think, version gates.
These version gates, version gate for fifth version, knows how to convert back my response back into the fourth version.
The version gate for the fourth version knows how to convert it back into the third version.
And all of a sudden, you get simple version gates.
Because, you know, if it's just field renaming in a single version, then it's just a single line.
It's, they're really simple.
But at the same time, they're immutable.
Because these version gates, they're not describing the whole schema.
They're describing the difference between two versions.
And because we know that we don't want to, you know, change the old version.
We want the word, the old version to work.
Which is why the difference between version two and version three is going to stay.
It's not going to change.
Which is why these converters are also immutable.
Which means that you don't need to spend much time supporting them.
Right.
Once you get, if you're on version three and you get the transition from one to two set, like that's not going to change ever, right?
Precisely.
Well, at some point, you need it for optimizations or for returning extra data.
But it's such a rare use case that I haven't seen it so far.
And I hope that I won't.
So this is Stripe's approach.
And one of the things that's really interesting that Stripe does and is laid out in this article that you're bringing to the table here is,
they also implicitly assign you a version when you first make a call, which was really interesting.
I use Stripe and I didn't realize that when I've made calls into Stripe that the first time you make a call,
it pins you somewhere like on your user account on their side, pins you to that version of the API.
And you never know you're on a different, you're on a particular version, but they keep you on that version.
That's wild.
That's great.
Imagine you log into Stripe, you make a call,
and then every time you open a documentation in your account, you're going to see your version.
Every time you make a call, you're going to get response from your version.
And then because they keep versions for years, for example, we are using, I think, a three or four year old version of Stripe's API and they still keep it.
According to this article, they keep all of their versions, all since the company's beginning.
I'm not sure how true that is.
They keep years worth of versions.
That is very true.
Now you might see that as a negative.
Like you might think, well, I'm pinned to this version.
Like how do I escape the old version if I want the new version?
But you can pass it in a header or there's other ways to say I explicitly choose something else.
But by default, you're just, you're living in the past.
That's really cool.
You integrate and then you live in the past for as long as you want.
The thing is, yes, you can change it using a header.
Monite has a similar structure.
You can change it in your partner's portal.
And you can also, or I guess developer's portal.
And you can also specify, for example, if you receive webhooks from Stripe, you can specify,
okay, I want this webhook of this version and that webhook of that version.
That's cool.
Yeah.
It's a really neat interface that if you don't want to think about versioning, you don't.
You forget about it.
But the second you want to, the second you want to have the latest API, they give you all the tools to work with it nicely.
And that's the thing.
This approach allows you to keep years worth of versions or maybe even decades worth of versions, actually, without your developers getting overburdened.
Because there is still a single version of the business logic.
They work on a single version of schemas and they just need to make sure that when they release a new version, they write a converter for it.
And your clients are also happy because they never need to switch.
It sounds really nice.
I suspect there's not complete ability to just ignore the old versions because, you know, maybe you want in your new version, you're going to imagine this piece of data doesn't even exist.
And we're going to remove it from the database.
But if it needs to be there for the old ones, you know, there's probably some level of you're constrained a little bit, but it sounds a lot better still.
You're still constrained.
But remember, API versioning, if we translate it, you know, application programming interface versioning.
We're not versioning the app.
We're versioning the interface to the app.
Yeah.
So a lot of times it's like, what fields are we passing or something, right?
If you are trying to change the data between versions, if the data is different, entirely different, I mean, then you're probably not doing API versioning.
Then you probably should be calling it application versioning.
And actually, Stripe has, I guess, a hack for that too.
If you take a look at their API, they still have a V1.
But that's the thing.
They don't use this V1.
They use date-based versioning that is in the headers and their settings.
But the V1 is precisely for this situation.
If someday they will need to have something else, if someday they will need to break everything and, you know, change the data required or any other crazy changes, they will be able to do it using that V1.
Yeah.
It's just there in case.
Exactly.
Yeah.
Cool.
All right.
Well, let's talk about your project.
Let's talk about your project that makes this possible in Python because the Stripe examples, I believe this is Ruby that they all have here, right?
It is really nice to read until you try to understand it.
Until you try to understand what it actually does.
And it's crazy.
I know.
When I was reading this today, I was like, you know, I'm pretty glad that I'm not writing Ruby right now.
Okay.
All right.
Let's talk about Python, though.
You've got a project called Cadwin that takes the Stripe philosophy and applies it to versioning for the most popular API framework for Python, FastAPI.
Tell us about it.
Here's the thing.
When I started developing it, we were using versioning by suffering.
So we were using directory copies and none of us really enjoyed it.
So there needed to be a solution.
And I went to Stripe.
I went to other companies.
I spoke with them.
I tried to understand the nitty gritty details.
As I mentioned, Brander Leach extended us huge help when he, I guess, described how he thinks about API versioning right now.
And I built it.
The first version was just like Stripe.
It was exactly what Stripe did to the letter.
But then at some point, we started applying it at Monite.
And I realized, wow, Stripe had a lot of legacy.
So they didn't implement some things.
For example, in Stripe's article, you can see that they're not using any schema validation or at least, how do I describe it?
Their business logic just receives a hash map, a dictionary.
It's not statically typed.
But FastAPI uses Pydantic.
So it's a bit more complex than that.
Okay, we added support for that.
It's a bit more complex.
It's a bit more, I guess, sophisticated now.
But then I also realized that Stripe, they don't migrate requests.
They only migrate responses.
Because a migration of a request is usually not just a breaking change.
It's a change in a business logic.
Because when you return a response, you can just return the whole thing and then, you know, just play around with it.
But when you are changing the request, your business logic also needs to change.
And that's what they did.
You know, they have extra ifs and extra levels of business logic to handle that.
Well, apparently, it's not necessary.
Apparently, requests are also migratable.
And that's the second feature that greatly distinguishes us from Stripe.
This thing can statically, well, by statically, I mean, it co-generates your schema so that you can see everything before you run your app.
This thing can migrate both requests and responses.
And it supports everything that Stripe does.
And we have pretty big plans for extending it.
And as an added bonus, we also have pretty big documentation.
Well, not compared to Django or FastAPI.
Django is, you know, older than me and FastAPI.
It's pretty amazing in terms of its documentation.
Sebastian Ramirez is an expert at making tutorials.
And if you take a look at our reference documentation to the left and our, how do I describe it, recipes documentation,
they basically describe to you every single use case that you could have with API versioning.
Every single problem that you could solve with it.
And then we try to apply our approach to it.
So it's not just the framework.
It's all the ways you could use the framework.
Someday, hopefully, we will also extend it beyond FastAPI and Pydantic.
And we also have sections for theory for people who are not Pythonistas, who just want to learn how to do API versioning, but fail to find any resources.
So Cadwin also becomes this, I guess, hub for API versioning resources and anything related to them.
It seems like it would probably be pretty easy to adapt to anything that uses Pydantic for its data exchange.
Oh, yes.
Yeah.
So like, for example, in Flask, Flask doesn't do the model binding type stuff on the endpoint, but you can just take the input data and just star start into a Pydantic model.
And then, you know, now you're in Pydantic world, right?
So it sounds like maybe something along those lines might be a good bridge.
There's a lot of magic involved in trying to interface with FastAPI simply due to its internal complexity.
Right.
This portion of Talk Python to Me is brought to you by Sentry.
You know Sentry for their error tracking service.
But did you know you can take that all the way through your multi-tiered and distributed app with their distributed tracing feature?
Distributed tracing is a debugging technique that involves tracking requests of your system, starting from the very beginning like a user action, all the way to the backend, database, and third-party services.
This can help you identify if the cause of an error in one project is due to the error in another.
Every system can benefit from distributed tracing, but they're especially useful for microservices.
In this architecture, logs won't give you the full picture, so you can't debug every request in full just by reading the logs.
Distributed tracing with a platform like Sentry gives you a visual overview about which services were called during the execution of certain requests.
Aside from debugging and visualizing your architecture, distributed tracing also helps you identify performance bottlenecks.
Through a visual like a Gantt chart, you can see if a particular span in your stack took longer than expected and how it could be causing slowdowns in other parts of your app.
Learn more and see some examples in the tracing section at docs.sentry.io.
To take advantage of all the features of the Sentry platform, just create your free account.
And for all of you Talk Python listeners, use the code TALKPYTHON, all one word, and you'll activate a free month of their premium paid features.
Get started today at talkpython.fm/sentry-trace.
That link is in your podcast player show notes and the episode page.
Thank you to Sentry for supporting Talk Python to me.
So some good questions, but I think they'll be somewhat answered if we just maybe talk us through your tutorial here.
Like how do we use Cadwyn and how does it manifest?
How does it work at runtime?
Stuff like that.
First, before using Cadwyn, you know, you write your regular app, but then you just structure it a little bit differently.
When you are versioning, you need a directory that you will consider versioned.
And this directory will be duplicated for every version.
No worries, you're not going to support the version code.
It's going to be code generation all the way through.
And you can use it to generate connectors for your application, or you could use it to generate schemas or enums, you know, the different version of them and anything else related to them.
So you pick such a directory, you call it latest as for your latest version.
Then you define a single version bundle.
If you go a little bit down, you will see somewhere in here after defining versions.
Yes, we define our version bundle with a single version, whichever version it is for you, whichever date it is for you.
And then you run code generation.
It generates a directory that simply imports from your latest.
And that's where you can start doing the magic.
Now you just use our versioned app.
If you take a look, we have a versioned API router.
Then you use the Cadwyn version app.
You generate version routers.
And then it's going to generate a really nice Swagger for them.
If you open it somewhere, I think, yep, this one.
So now your dashboard, the one on docs, is not the Swagger page, but the dashboard of all your versions.
And that's it.
After that, you can simply repeat the steps above.
So you write a migration, write that converter that describes the difference between the versions.
We have it somewhere there.
If we had an address as a string and we want to change it to a list, we say, okay, schema, base user, field addresses didn't exist.
You have a user object, which is a Pydantic model.
The user object in the initial version just had a field called address, which was a string.
But you said, you know what?
People might have multiple addresses that they live at.
So let's make it a list of strings and make it addresses, right?
Which seems like a minor change, but clearly that's going to be a major problem for the things that assume there's an address and it's no longer there.
Right.
So you create this version change derived class that has a upgrade downgrade basically feature, right?
You simply describe how do you migrate your schemas?
How do you change your schema classes, your bidentic models?
And then you describe how you convert your requests and response.
And that's it.
This class is the only thing you write for the new versions.
Then you run code generation, run your app.
That's it.
Everything works.
Yeah, that's super cool.
So a couple of questions that were comments that are interesting here is Emile asks, is it like database migrations?
And Mike Fila points out, it's really, really similar to like Alembic, right?
You say, generate me the changes.
Alembic is a little different in that it looks at the two different database schemas and tries to actually write the migration code to do the DDL changes.
But in principle, it's like every time there's a version change, there's some bit of code that is generated that will run.
And then the other one is about, you know, how does this affect performance?
And I guess the only real performance change is like there'll be a couple of steps to the serialization, but they're often small.
Like in this case, the new version takes the old data and says, well, we're just going to have a list with one address.
So we're going to stick the one address into the list of addresses.
And vice versa, you just set the address field to be the first thing in the address list.
And like, that's a really low amount of code overhead to go back.
And hopefully people are moving towards the new versions eventually, you know, that kind of thing, right?
But if they don't, then this code just runs to transform the dictionaries that goes back to it, right?
Imagine you have like Stripe, I don't know, 13 years worth of versions.
Now, if you are a successful company, and if you are alive, you are probably a successful company.
After 13 years, you know, if you are a successful company, that most of your clients will probably be newer ones.
The longer you exist, the more clients you have, and the more clients you have, the more, you know, newer clients you have, the newer API versions they will use.
Even if they're old clients, they might decide to, they might rewrite their own app and get a new version, right?
Like, they could upgrade their code, because their code is going to be 13 years old or 12 years old at this point.
They're like, they need to change too.
At some point, yes, your old client might, you know, from the very first version might request something, and then we'll go through all converters.
Well, not all, all converters that are related to the specific request and response, and that is probably going to be in the tens, not even in the hundreds, even after 13 years.
But your clients from the latest versions, or even from the middle versions, will not suffer that.
So the majority of your clients will actually see almost no performance difference.
And even your old clients, even your clients for 13 years old will probably, even if they do see a performance difference, it's not going to be multiple orders.
It's not going to be 2x, 3x.
It's probably going to be maybe 20%, maybe even 10%, depending on how slow your app is.
And that's considering how many years have passed is crazy fast.
Yeah, absolutely.
That's very cool.
So what about the URL?
What about the endpoint?
How much do consumers of your API have to actually think about, I'm using this version or that version?
Is it passed as a, is the version passed as a header?
Is the query string part of the URL?
The version is passed as a header.
And Cadwin, by default, requires it to be.
But if you have some developer portal, if you want to, you know, store your version in some database,
then even though it's a little more advanced, you can write yourself your own middleware that will, you know, check if the client already has a version and then pick that version and put it into the context bar.
And it's possible.
So by default, you're going to have, no, no, no, middlewares are the more advanced sections, not in the tutorial.
There's this other one we're going to talk about in a section.
It has just an example of all, you basically just, in FastAPI, when you create the app, you know, like app equals FastAPI, you just say middleware equals a list of classes that implement these behaviors.
So what you're suggesting is, like, you just create a class and you pass it in at FastAPI startup, and it just goes, is there a version being passed to me?
If not, let's see who the user is based on their API key, pull that from a database, and maybe just set that header right there before the request and then let it fly, right?
Something like that?
Yep.
And then your API will essentially be Stripe-like, where your client won't need to think about versioning at all.
But by default, of course, the clients need to always pass it with a header, which might sound like something really unnecessary and hard for the clients.
But if you imagine that you're writing code, it's exceptionally easy to just put it into your, I don't know, requests or HTTPX client and forget about it, you know?
It is pretty easy.
If you're consuming APIs in the fashion that we're talking about, you're probably already putting an API key or some kind of thing into the header as well, right?
Like, so you're already messing with the header anyway.
Just put another value in there.
Precisely.
All right.
So right now, this is a FastAPI thing, but you're considering opening up to other areas, other platforms, other web frameworks?
It has a, what is it called, a milestone for that, which I am planning to extend with tasks, with plans.
First, you know, it's like with Pydantic, you know, at some point it separated the core when it was moving to Pydantic 2.
And that's one of the plans for Cadwin, you know, separate the core without breaking the public interfaces.
And then at some point use this core to build more Cadwins, you know, Cadwin Django, Cadwin Flask, whatever.
Or it doesn't even have to be a part of Cadwin.
The approach is there so anyone can use it.
Because 99% of complexity in implementing Cadwin was actually figuring out these patterns and understanding how to do it.
Because unlike a web app, unlike, I don't know, many apps that we develop daily, it doesn't, these things haven't been done.
People have migrated stuff, but it was mostly, you know, oh, change this dictionary without a proper DSL or without code generation, you know, without the niceties that would make your IDs happy.
And that would make, you know, auto-completion happy.
Yeah, you are a contributor to TypeShed, which is awesome.
So you must care about, you know, conveying the type information of the different versions and stuff like that.
That's tricky, right?
And of course, working on FastAPI and Pydantic, those are very type.
Oh, and the shirt says TypeHints for Life.
I love it.
I don't have any cool slogan in my eyes to have gray stripes, but I should.
Very cool.
So yeah, want to speak to the TypeHint type information aspect of this whole thing?
Essentially, as I mentioned, Stripe did it using, you know, simply dictionaries.
And it was mostly untyped.
As probably many people know, Stripe, after having so many problems with scalability in terms of like the team scalability and problems with typing,
they have decided to adopt Sorbet.
Well, not adopt, create Sorbet, which is essentially a type checker.
It's like a type checker and DSL for Ruby for TypeKins, which is really nice.
It has both the runtime component and a static component, as far as I remember.
But they still, most of the time, work with untyped code, which essentially can lead to many errors.
Cadwin tries to make sure that you still work with your schemas.
You still know which schema you work with.
It's always the latest.
And Cadwin tries to make sure that you're not going to have any type errors if you don't want to.
And you're going to have all of the niceties of having this dynamic request response conversion thing.
So we're trying to take the best of both worlds, have you support very little code, but like make sure that you can type in everything without any issue and you're not going to have problems and your type checker is going to check everything for you.
And your VS Code is going to see which field something has, which type something has.
Excellent.
One of the big problems with like all the runtime modifications, like one way you could have done this is that app startup, it could look at all of those things and it could just dynamically generate all the different versions and all the things, you know, using meta programming.
But then the editors hate that.
Yeah.
It does that with the endpoints because endpoints are not your editors.
They don't care about endpoints.
You never call your endpoint from your code.
No, you usually call the business logic from it.
Yeah.
Which is why I decided to do it in runtime.
You don't really need it.
But for schemas, yes.
For schemas, having them generated and then having people guessing and then having their IDs just put everything in red.
That is horrible.
I wouldn't want anyone to have that.
Yeah.
I don't like it either.
Not at all.
It's right up there with the star, star, star, star, KW orgs.
Have a good time guessing in my world.
I don't like either of them.
Awesome.
All right.
Well, let's just maybe touch real quickly on a couple of other choices that are out here.
You know, Cadwin looks awesome.
But we also have, for example, we have from Dean Way, we have FastAPI versioning, which has a very different philosophy.
It has kind of the V1, the V2 style that you might maintain multiple versions of.
You want to just maybe speak to your thoughts on this one real quick and that people can take that if they like this style.
I think it's either the first or the most popular.
It's definitely the most popular, but I think it might even be the first library in FastAPI to implement versioning, implement proper versioning.
It has a few issues.
First of all, it only as long, as far as I remember, it is mostly duplication based.
So it doesn't automate anything for you, which is a good thing if you know what you're doing and you're not going to have a lot of versions.
If you have 10 or 20 versions, if you want to keep them for a long time, this is the, you know, endpoint point copying approach that I mentioned that with time is going to kill you, which is a problem here.
The second problem is that it, as far as I understand, kind of recommends semantic versioning, though it doesn't bind you to it.
You can use dates, though you have to make sure that they're dates yourself.
And you can use just simple integers.
So you're not bound by semantic versioning, but it has a few examples with that.
Aside from these two, it's an amazing library.
And for anyone who just wants to version a small app, you know, just add a single version that is going to be, you know, the old version is going to be deleted in like two months.
Use that.
Use FastAPI versioning.
It's an amazing library and you don't need anything else.
Catwin is for use cases where you need to keep a larger number of versions or if the changes between your versions are much trickier.
Yeah, like this might be pretty good for my example I talked about with the courses app that we built, right?
We literally have two versions.
It's been around since 2018 and there's two versions.
You know, like that would be a decorator and I'm probably good to go.
I don't need to think too much about it.
Precisely.
No code generation, no nothing.
You just, you know, you just put it in and you go.
Another one.
If you're a Flask person, you could have Flask rebar, which seems to have a real similar philosophy to what you just described, but, you know, works for Flask.
I feel like it's a little more complex than that.
It is also the parent point.
So it also allows you to duplicate single endpoints.
It also allows you to do arbitrary prefixes.
So you can have integers, you can have dates, you can have, you know, whatever.
Old, new, whatever.
Yeah.
Old, new.
I guess some people do that.
If you want.
Yeah.
Latest, latest.
Yeah.
Latest, really latest, final.
Like a new folder, new folder.
Yes, exactly.
But yeah, it's really similar.
And I guess.
They probably use zipping up folders for version control as well.
I'm just naming them by the date.
Final, final, final.
Let's not use, let's not use Git.
Let's, let's just copy them.
As for versioning, that's like a really good metaphor.
If you are a person that just needs another version of their Google Doc, you wouldn't use
Git.
Well, unless you use Git for everything else.
And same here.
You don't need a huge combine for using Flask.
And Flask usually, at least in my experience, tries to build simpler APIs.
And you wouldn't use, well, most companies that do API first don't do, don't use Flask.
They try to do, you know, Django DRF or FastAPI, which is why this library seems like a perfect
choice for that.
Yeah, indeed.
It seems really nice.
One of the things that we were speaking of duplication, one of the things that they just
jump right into to demonstrate here.
And I imagine the FastAPI version one as well could do it.
But there's one endpoint that doesn't change.
And there's one that is different.
So they copy the one that's different.
But for the one that doesn't change, they just put a V1 and a V2 decorator on that one.
And it, it remains unchanged, basically, right?
Cadwyn tries to escape this kind of verbosity.
And essentially, you have all endpoints everywhere.
And if you want to, you know, omit some endpoint or change some endpoint or add some endpoint,
into the new version, you just add a version change and say, oh, this endpoint didn't exist
in the old version.
That's it.
Very cool.
Yeah, because if you had 20 of these, you'd have 20 decorators in the little function.
Right?
That's where it starts to get.
Not a huge issue for Flask.
I haven't seen anyone building, I don't know, 50 or 200 endpoint apps with Flask.
I'm sure people do that.
But I think it's the minority.
Other side of the fence, Django REST framework, DRF, you already mentioned them a little bit.
And so Django, they already have like versioning built into the framework itself, right?
They have versioning interfaces built into.
So like, it's not, they're not implementing versioning for you, but they give you all the
tools to implement your own versioning.
Yeah.
Okay.
So they kind of have the building blocks for it.
Excellent.
Exactly.
All right.
Well, I think we're pretty much out of time, but thank you for being here.
It's been really interesting.
Oh, it was for me as well.
Thank you for having me here.
Yeah.
You've given everyone a lot to think about, you know, before we wrap it up, two quick
things, you know, notable PyPI package.
I mean, we've already given Cadwin a shout out.
Anything else that like, oh, I came across this and it was super cool.
Maybe it deserves a little bit of a shout out.
Anything that like you use a lot that you would recommend?
For us in our company, we started using one more package of mine.
It's called pytest Fixture Classes.
Okay.
Essentially, when you are building groups of fixtures that create some objects for you,
for you to use in your tests, and then all of a sudden, one of your tests needs an object
that's a little different, you know, just a little bit.
And then everything just falls apart.
Yeah.
A factory fixture pattern that you can see in pytest allows you to, you know, make a function
within a function and one of them is a fixture and that's all cool and nice, but then it's
not typed.
You can use, you know, the callable syntax and then you're not going to have the keyword arguments
there.
And otherwise you can write a protocol and it's a lot of code.
What pytest Fixture Classes does is it allows you to type everything there.
So you write the same amount of code, but it's all typed.
And now you have, you can have groups of fixture classes building on top of each other, which
is a nice pattern to have, but a really bad pattern to abuse.
Very cool.
All right.
Well, nice, nice recommendation.
People can check that out.
Emil, an audience already says, amazing thing.
I'm already using it.
So final call to action.
People have APIs.
They've listened to us talk for an hour and they're like, oh, we should be versioning our
APIs.
We probably should be doing something here.
What do you recommend people do?
How do they get started?
How do they go further?
So first you need to decide whether your use case is simple.
How many versions are you going to support?
If you're not planning to support more than two versions, or if you are supporting versions
only for your internal microservices or only for your front end, then consider using the
simple tools.
Consider using the FastAPI versioning, you know, fast rebar, things like that.
But then once you realize that you need to support versions for months and you need three
or more versions and you're providing your versions as a public, you know, to the public,
then I advise moving straight into Cadwin or Cadwin-like frameworks.
If you use FastAPI, you can just use Cadwin.
It has a bit of a learning curve, but within making one version, you will realize just how
simple it is.
And if you're not using Cadwin, that's fine as well.
I have all of the resources that I use to build it and all of the resources, you know,
describing different ways of API versioning there in the repository in the theory section.
So you can just read that.
And even if you don't like that approach, we describe everything else there.
So you'll be able to pick the one you like.
But my personal recommendation, don't pick the directories.
They're painful.
Awesome.
Really good advice.
Super cool project.
So thank you for being here, Stanislav.
And talk to you later.
Talk to you later.
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.
It's time to stop asking relational databases to do more than they were made for and simplify
complex data models with graphs.
Check out the sample FastAPI project and see what Neo4j, a native graph database, can do
for you.
Find out more at talkpython.fm/Neo4j.
Take some stress out of your life.
Get notified immediately about errors and performance issues in your web or mobile applications with
Sentry.
Just visit talkpython.fm/sentry and get started for free.
And be sure to use the promo code talkpython, all one word.
Want to level up your Python?
We have one of the largest catalogs of Python video courses over at Talk Python.
Our content ranges from true beginners to deeply advanced topics like memory and async.
And best of all, there's not a subscription in sight.
Check it out for yourself at training.talkpython.fm.
Be sure to subscribe to the show.
Open your favorite podcast app and search for Python.
We should be right at the top.
You can also find the iTunes feed at /itunes, the Google Play feed at /play,
and the direct RSS feed at /rss on talkpython.fm.
We're live streaming most of our recordings these days.
If you want to be part of the show and have your comments featured on the air,
be sure to subscribe to our YouTube channel at talkpython.fm/youtube.
This is your host, Michael Kennedy.
Thanks so much for listening.
I really appreciate it.
Now get out there and write some Python code.
I'll see you next time.