Talk Python To Me, episode number five, with guest Mike Baer, recorded Thursday, April 9th, 2015.
Hello and welcome to Talk Python To Me, a weekly podcast on Python,
the language, the libraries, the ecosystem, and the personalities.
This is your host, Michael Kennedy.
Follow me on Twitter, where I'm @mkennedy,
and keep up with the show and listen to past episodes at talkpythontome.com.
This episode, we'll be talking with Mike Baer about SQLAlchemy.
Let me introduce Mike.
Mike Baer is the creator of several prominent Python libraries, including SQLAlchemy,
Mako Templates for Python, Alembic Migrations, and Dogpile Caching.
He's been working with open source software and databases since the mid-1990s.
Today, he's active in the Python community, working to spread good software practices to an ever-wider audience.
Mike is a semi-regular presenter at PyCon US,
and has also spoken at many smaller events and conferences in the United States and Europe.
Follow Mike on Twitter, where he's Zeke.
That's at Z-Z-Z-E-E-K.
Welcome to the show.
Thanks. Thanks for having me.
Yeah, I've been a longtime fan of SQLAlchemy.
And, you know, when I started this podcast, I was thinking,
who do I have to have on the show?
And you were definitely on the list.
That's very flattering.
Yeah, you built some great stuff.
So, before we get into the details of SQLAlchemy and how we should use it and why it's so awesome,
maybe let's take a trip down memory lane, step back a little bit,
and just, you know, how and when did you get into Python?
I got into Python after kind of hesitating to get into it, meaning I knew about it,
and it seemed like something that was appealing to me.
But I kind of resisted mostly because of the white space thing.
This is back in probably 2003.
I was working at Major League Baseball.
Most of my programming career before Python was doing a lot of Java and a lot of Perl.
And I really liked the idea of object-oriented languages,
and I liked the idea of scripting languages.
So Java was very object-oriented.
Perl was not too good at objects.
But it was a scripting language.
So I really wanted there to be some scripting language where I could write much cleaner code than you get in Perl.
Yeah, so you kind of had half of what you wanted in one language.
Yeah.
But you wanted to bring it all together, right?
And Python really looked like it.
Python really looks like it, but it's got that stupid white space thing.
And I just didn't want to get into it.
And then one day when I was working at baseball,
we were rolling out this client application called WinCVS, I think it was.
It was a Windows CVS client.
And we had to roll it out to people that were not programmers.
And we needed to add hooks to it so that people could tag things and send things to different servers by tagging.
And WinCVS's scripting language was Python.
So I had to learn, you know, 10 lines of Python to do this script.
And basically I spent like two hours with the white space thing.
And I was like, oh, we do that white space anyway when the code is clean.
And I'm actually totally fine with it.
So let me just learn Python now that I've spent 10 minutes getting used to white space.
That's really funny that it was like a real barrier to you, right?
And then it just came down.
It really bugged me because I was a really sloppy coder back then.
Python really made me a much cleaner programmer.
Yeah, that's a good point.
I hadn't really thought about that.
But it definitely does make you think about that.
And, you know, a lot of the modern editors make it almost transparent to you.
But I think there's the mental concept of it.
And then there's the reality of it.
And the reality is that it's actually not a big deal at all.
It's super smooth.
But when you're coming from, say, Java or C# or C++ or something, then it seems like a huge deal.
Yeah, you get over it really quickly.
Excellent.
Oh, you bring up WinCVS.
Boy, I used to use that thing back in the late 1990s or something.
That brings back some memories.
Yeah, that's kind of what it was, yeah.
That's awesome.
Let's talk about something more modern than WinCVS.
Let's talk about SQLAlchemy.
Sure.
So can you just tell, I suspect that, you know, 90% of the listeners are fans of and familiar with SQLAlchemy.
But there's going to be some folks who don't know or maybe they know it as a buzzword.
Can you just tell everyone what SQLAlchemy is?
So SQLAlchemy is this library that is, it's all about Python.
And it's all about SQL and databases and interacting with databases.
So it's basically when you need to work with a relational database, say you're going to work with like Postgres or MySQL, there's all kinds of things you need to do with the database.
You need to write, you know, programs that talk to it.
You need to get results back.
You need to create schemas.
You might need to be given some existing database and look at it.
There's scripting and migrations of schemas you want to do.
You probably are writing applications that have higher level business concepts that you want to map to that database.
So SQLAlchemy started out with a really ambitious goal of having a place, a way to do that, a way to do all those things in Python.
Not just being able to map like an object to a table, but all the features of managing schemas and creating schemas and reflecting them and working with SQL scripts and everything else.
Working with data type problems, making all kinds of different database backends look as similar as is feasible.
You know, like data types in Oracle are very weird compared to those in MySQL and SQLite's a whole different story.
Those databases are always going to be different, but you can at least try to get some semblance of sanity across all of them.
So that's why if you go to SQLAlchemy's website, the first thing it says is like the database toolkit for Python.
It doesn't say it's an ORM.
That's just one component.
It's a toolkit that has kind of helpers for whatever you have to do with a database.
SQLAlchemy can definitely have a role.
That's excellent.
You have at least two really nice sort of comprehensive walkthroughs, one for the core and one for the ORM component.
That's SQLAlchemy.org, right?
Yes, SQLAlchemy.org.
So when did you get started with, when did you create SQLAlchemy and what inspired you to create it in the first place?
So I was kind of writing the first iteration of SQLAlchemy in late 2005, and our first release was early 2006.
And really when I was writing SQLAlchemy, it was not the first time I was creating a tool like that.
When you work, or at least in the old days, when you used to work in the 90s in all these various internet shops, we had really little tools to work with.
I mean, there were a lot of tools, but we were, you know, using Perl and libraries to talk databases were pretty crude.
And I was using Java from day one when it was, you know, version 1.0, and there was literally no libraries before Hibernate.
So throughout the 90s, you know, you get into the habit of writing little database access layers yourself.
And every job you have, you're going to go and, now I'm going to write a really better database.
You know, the database access layer I wrote last time sucked.
Now I'm going to write a really good one.
And you say that statement to yourself like five times, right?
Yeah, every job you have, it's like, okay, this time we're going to get it right.
We're going to write the ultimate, you know, set of tools, you know.
And then as you're doing this, you know, as the 2000s rolled along, ORMs, you know, Hibernate suddenly was around.
And I was doing a lot of Perl at MLB, and there were some ORM-ish things with Perl that looked terrible to me.
They looked really simplistic.
I had worked, by the time I was at MLB, I was working, that was, they were a big Oracle shop.
I had already had a lot of gigs where I had worked a lot with Oracle.
I had worked a lot with Postgres.
I had worked a lot with Sybase, Microsoft SQL Server.
I had worked a lot with not just MySQL, but its earlier MSQL incarnations in the early 90s.
So I had, and I had written database code in Perl, Java, and C, C++, you know, because back in the early 90s, you know, we didn't, Perl wasn't always available for some cases.
So I really had iterated a lot on this whole back and forth with the database, sending a SQL string, getting a result back, getting data back from the result.
I knew a lot about that.
So, and I always wanted to have a system that would be the last time I have to do this, you know.
Yeah, I had that feeling.
A long time, it was like, I was writing things in Java.
I was going to write like a big toolkit in Java, like the big, you know, web framework, everything you need Java thing.
And it never really happened.
But eventually when I got into Python, that's when I really began doing it.
I, you know, Python had kind of not a lot of libraries around.
The only library that was feasible for database access on Python was SQL Object, which is a great library and was a huge influence on SQLAlchemy.
And I started doing it and it was kind of based on the latest techniques I iterated at baseball.
Like the idea is that if you want to talk to a database table, you make a data structure that represents what the table looks like.
And we call that table metadata.
But one critical thing I did with SQLAlchemy was I actually decided to read a book beforehand.
So I had this book called Patterns of Enterprise Architecture by Martin Fowler.
Yeah, that's a great book.
And I read that book and half of the book was like, oh, that, oh, this, oh, this.
All these things that I've done.
And he kind of put names to them.
And then as he did all that, there were some other patterns that I was not familiar with, like the unit of work pattern.
I was like, wow, look at that.
I've never heard of that.
That's pretty cool.
And does that appear as the session in SQLAlchemy now?
Yeah, the session.
Yeah.
And actually session is from Hibernate.
Hibernate's got the same thing.
So I, you know, really made a go at it.
And I said, I'm actually going to read a book beforehand and really try to get all these patterns right.
And that's kind of how it started.
And it took a very long time for SQLAlchemy to be any good.
I mean, I think when I first released it, it had a lot of the special things going on that made people like it.
But it was not executed very well.
But, you know, over the years, over really 10 years, it's gotten very refined.
But it's taken a long time.
Yeah, it's quite a mature product.
And that's cool.
Yeah.
Did you use it internally before you actually released it?
Or was it kind of, I'm building it out in public?
Yeah, no, it was funny.
When I was starting in Python, I was writing open source.
The first thing I wrote was this template language called Mighty, which was basically almost a line for line port of a template thing in Perl called HTML Mason.
That was the first open source thing I published.
And then Mighty was kind of an embarrassment eventually.
And I wrote Mako to replace it.
But I was writing these libraries at first without having any job.
The gigs I was doing were still Java jobs.
I was still doing, after I left baseball, I was still doing Java work.
Python was certainly used in the mid-2000s, but it was not as dominant as it is today.
I didn't actually get to use SQLAlchemy for a real gig until version 0.5.
So maybe four years into it, I finally was on a gig where like, hey, we can do this thing in Python and we can do this.
And everyone was like, okay, use the Python thing.
Yeah, use that toy language.
Yeah, yeah, yeah.
It was kind of like that, yeah.
And it was actually the website for a TV show called Charlie Rose, which is on PBS.
Oh, yeah, yeah.
Great.
Yeah, we wrote, I don't know if it's still up in that incarnation anymore, but at the time, this is maybe 2007, it was written using early version of pylons.
And that was the first time I used SQLAlchemy on the job myself.
And SQLAlchemy improved by leaps and bounds on that gig because I had like, oh, this sucks.
Oh, look at this thing.
This thing is terrible.
How could people have been using this for three years?
Yeah, that's excellent.
Yeah.
So it's weird.
Yeah.
It was written kind of in a vacuum.
Like I only knew what my users told me for several years.
Yeah, very cool.
The previous show that's not out yet, but was recorded just before this is a guy named Mahmoud Hashemi from eBay PayPal.
And the whole show is about sort of enterprise Python and sort of using it as a real proper language.
And I think, you know, it's probably that time frame and that era.
There's a lot of similar ideas about, hey, we could actually build real websites and, you know, web scale professional apps with this language.
It's great.
Yeah.
I mean, Python as enterprise was something that I think, you know, it existed in the early 2000s.
Like I think a lot of people were using, I mean, people were using like Zope or Plone, I think was the early, you know, incarnation of enterprise Python.
But I think, you know, really you got to hand it to like, I mean, first Ruby on Rails got people more comfortable with scripting languages for enterprise.
And then Django really helped a lot too.
Django really brought a lot of people to Python.
And I think the critical mass kind of started happening for Python kind of in 2005 on forward.
Absolutely.
And I think, you know, to shoot your horn and everybody else's a little bit, the stuff that's in PyPy, all those packages out there make it really hard to not consider Python.
The fact that you can just pip install magic, you know, and build stuff so quickly.
Like, why would I start from scratch, you know?
Right.
Absolutely.
We now know what SQLAlchemy is, if we didn't.
Can you kind of give us like a, I know it's hard to talk about code, so don't get too detailed.
But can you give me a little walkthrough of like what is involved in getting started?
Like what do I do in code to maybe make a basic query or connect to the database?
So there's different ways that you might want to use the system.
I mean, there's the level, the layer of I want to just make a query.
And that's pretty easy.
I mean, SQLAlchemy has a lot of different levels that it can be used at.
Like if you want to just connect to a database and make a query, that's like a really two line.
You make an object called an engine, which you give it a URL for a database.
And then the engine can just accept queries as strings directly.
And right there, you've already saved maybe 10 lines of code versus what it would be with the raw Python database.
Yeah, that's fantastic.
Maybe we should take a step back and talk about the layers.
And then we could talk about, you know, well, this layer, that layer, it looks like such and such.
So you have the core and you have the ORM.
Can you talk about that a little?
Yeah, so I actually wrote SQLAlchemy in this inside-out way where I didn't even worry about the –
I mean, I kind of knew what I wanted to do for the ORM, but I didn't even worry about that for a few months.
I wrote first just the engine, just like the concept of here's how to connect to a database using the Python DB API.
And here's a way that we can execute a query and get results back in a way that's a little bit nicer than what the raw Python database API gives you.
It's a little bit nicer and a little more consistent.
Then the next thing I did, which, you know, was soon after, was I wrote the table metadata system, which I had already done many times at different jobs.
You know, kind of looked in Fowler's book.
So you wrote – you create this concept of a Python data structure that mirrors what structures you have in your database.
So when you have a relational database, it kind of – without using it at all, it has a fixed schema.
You have, like, tables that are kind of – they're not – we might call them physical tables, which they're really not physical.
They're just on a disk.
But they're tables, and the tables have columns, and they're kind of like the things that are going to store your data.
So you model a mirror of all that stuff in Python so that you can write Python code that refers to this object structure that can then relate directly to how the database is built.
So that's called database metadata.
So it means it's information about the structure of your database.
Then from that, the table metadata objects in SQLAlchemy have all these methods on them, like select and update and, you know, table.select.where.
And when I say, you know, x.y.z, the dot in Python means that we're calling a method on an object.
And if you have this pattern where you say an object.call this, and then you get something back, and then you say .call that,
that's a pattern called method chaining, which means that you keep calling methods on this object that keeps returning a new copy of itself.
So that's a common pattern used in database query libraries because the method chaining is kind of like you're building up this data structure
that looks like basically a syntax tree.
A syntax tree basically represents like some code, but it represents the code as a set of objects connected together in a tree.
Yeah, that's fantastic.
I really like the fluent API that you put together there where basically you start with a query, and whatever you do to it,
what comes out of the query and just lets you build up.
Here's a where clause.
Here's an order by.
Here's a distinct or whatever.
Yeah.
At this point, I mean, that pattern is pretty commonplace these days.
I mean, when I first did it, it was not quite as commonplace.
I mean, basically Java Hibernate kind of has that pattern, but not in a nice way because Java is just not as slick of a language as Python.
And some of the, you know, it was a pattern that was around.
I think SQL object had it as well, you know.
It builds on that, but then we also did this thing.
I took this thing right from Ian Bicking's SQL object, which was that you overload Python operators like the double equals operator or the greater than operator,
so that when you have these little objects that refer to columns and values in a database, you can compare them with the Python comparison operators,
and they kind of auto-magically create another object like a comparison.
And if you go through the ORM or the core tutorial on SQL object, the core tutorial would really show how this works at the expression level.
The ORM tutorial kind of refers to it, but it's more about how to get the ORM going.
But what I'm talking about here is how the core query system works.
So basically these Python objects can kind of expressly be combined to create SQL queries.
And that's the core, really.
That's really what you get, as well as the fact that it returns objects, it returns Python objects.
Like if you query for dates, you'll get a Python date object back.
If you query for intervals, you can get a Python time delta back.
And there's all systems for building your own custom types if you want to get JSON back, things like that.
And then the ORM builds on top of that.
So if you're starting out, I mean, if you really want to learn the library from the ground up, I would start with the core and then move to the ORM.
If you want to get some quick results for a program you're writing, you might want to start with the ORM first and then do a deeper dive later.
I've kind of switched those directions around over the years, and I think there's just two ways to learn.
You can learn from the outside in or learn from the inside out.
And it's kind of your choice as to do you want immediate gratification or do you want more fundamental knowledge first?
Right.
Well, and how much are you going to be depending on the library?
Right.
If it's your core business app and it's really important that this thing keeps working, maybe you should deeply understand it.
If it's just a little blog you're throwing together, do it quick and then come back and learn it more deeply.
Right.
Start with the ORM and then go to the core to understand that.
Yeah.
Well, everyone I've seen, everyone's got a different way to do it.
So I just try to present both.
And also for a really long time, the core didn't have that name core, and that was a problem.
It was just called SQLAlchemy, and then there was SQLAlchemy ORM, and people didn't really, you know, obviously understand.
There's this whole thing with SQLAlchemy that is not the ORM.
So I introduced the word core probably about, you know, five years ago to say, hey, there's this whole other thing.
And I divided the docs into like two big left and right sections so that you can see there's this whole other thing.
There's only the core, and it's like an entire set, the right half of the docs.
Yeah.
I think you have them almost as columns next to each other.
I do.
I kind of imitated.
I think Django's documentation at the time had that thing going on with the two columns.
So I did that to say like, look, this is whole core thing.
If you don't like the ORM, because I knew the ORM was going to be, the ORM is more opinionated.
I knew it would be controversial.
I knew that I personally didn't like ORMs very much.
And this was going to be the ORM that I was actually going to like.
But I knew all the other commercials like me were going to hate it.
And those people still around today who hate ORMs.
So I wanted to say like, look, I get it.
Don't use the ORM.
Use the core part.
And then when you kind of use it for a long time, you realize, hey, this redundant thing
I'm doing over and over again could just kind of be automated by the ORM.
Then you can use the ORM when you see there's a need for it.
So that's kind of how I saw it being used.
But other people go the other way.
Sure.
So you've sort of talked about the learning perspective of when I might use the core versus
the ORM.
But are there like performance or other patterns?
Like what other considerations do I choose between the core and the ORM model?
There's a lot of performance.
There's a lot of, yeah.
So kind of the unspoken drama of SQLogamy for all the years it's been out has been the performance
drama.
Because coming from a Java background, you know, in the 90s, early 2000s, at least back then,
when we did Java programming, we really didn't pay too much attention to the way our code
was written might be slower or faster.
I mean, obviously, you know, if you're doing like order of N, if you want to do something
once rather than N times, things like that.
But just if you do two nested loops or you do this special library call instead of the
loop, is that faster or slower?
You don't see that much in Java because all the standard libraries in Java are written in
Java as well.
But with Python, it's mostly CPython.
There's this big gap where if you do something in a for loop, it might be very slow.
And you do the exact same thing with like the zip call.
It's a million times faster because the zip call is written in C.
So there's this whole thing with Python where you have to kind of use the standard library
as much as you possibly can because you want as much data processing to happen in native C
functions rather than in Python.
So I didn't know that in the first few years of writing SQLAlchemy.
And I didn't really know much at all about what was fast and what was slow.
And that's pretty common when someone starts with Python.
So up and through to version three, the thing performed horribly.
And then there was a lot of blog posts and other competitors that came out to try to challenge
the performance of SQLAlchemy.
And I went on kind of a multi-year mission to improve the performance of the system.
So these days, and the most recent incarnation of that has been more performance improvements
in version 1.0, but also a lot of new documentation that I've added in version 1.0 that refers specifically
to the notion of performance.
It's actually in our FAQ now.
There's a whole section of my performance.
There's entire sections of how to profile an application.
It's, and there's a lot of sample tests, example suites in version 1.0 where you can compare
different methods of doing the same thing for their performance slash ease of use tradeoff.
So I have examples of, you know, if you want to do this query, how fast is this query using
the core by itself, how fast is this query using the ORM, but loading individual columns,
how fast is this query if we use the ORM full blown, and what are the tradeoffs, and how
can we switch between those two systems?
So that's really interesting.
I have a couple of performance questions I'd like to ask about.
First, you said you went on this multi-year mission to speed it up, which is, that's a lot
of dedication, and that's awesome.
What kind of performance gains did you get?
Did you like double the speed, or what happened?
Over the course of, since version 3, I'm sure we've quadrupled the speed of it, because
it was really, really slow in version 2 and 3.
I mean, there's things, there's all kinds of Python idioms that change performance so
dramatically, and it's kind of a way of life for me now, but if you don't know, if you come
from like C#, it's probably going to be really painful to learn.
Like, if you do things like, if you want to check if a key is in a dictionary and add
a value, if you do that check with using, if you basically try to get the key, and then
it raises a key error, and you catch the key error, and you say, okay, let's put the value
in now, that's going to be way, way, way, like five times slower than doing it by just doing
a if key in dict first.
Right, absolutely.
Assuming the dictionary is usually not going to have the value.
Yeah, because it's actually the throwing the exception that is the raise, yeah, it's super
expensive.
Yeah, crazy.
So that's one.
Then there were things like, if you have an object, and you want, there's this pattern called
the proxy pattern, it's a programming pattern, where if you have an object, it has a bunch
of attributes, and as you call those attributes, it's actually sending the message to some other
object that's kind of embedded into it, it's called a proxy, and that's a very common pattern.
If you'd use the, there's a really easy way to do the proxy pattern in Python, which is
use this magic method called get at her, it's double under get at her.
And you get at her is past the name of the attribute trying to get, and then you say,
oh, let's call that attribute from our little nested object.
Crazy slow if you do it that way, compared to if you actually put explicit descriptors on
the proxy class that go directly to the inner proxy class.
So very early SQL alchemies, I use a lot of get at her, and people said, hey, this get
at her is crazy slow.
This thing is using get at her everywhere, and it's super slow, because they're all function
calls, and function calls are very expensive in Python.
So, got rid of all the get at her.
You do this procedure in programming, you do a lot with Python, which is called inlining.
Inlining means if you have some operations where method A calls method B, calls method C, calls
method D, you inline them.
You take all the code and you unwrap it into one big function, so that there's no function
calls within it.
Inlining is something that you kind of get someone for free if you use a newer interpreter
like the PyPy interpreter, which has a just-in-time compiler that does a lot of inlining for you.
That's really interesting.
I was going to ask you how SQLAlchemy works with PyPy, and have you tried Pysten from the
Dropbox project?
I haven't used Pysten yet.
Pysten looks like, I mean, I haven't looked at it much.
It looks like kind of the next incarnation of a thing called Unladen Swallow, which, if you
remember, that was, I think, a Google project where they were trying to use the LLVM compiler,
and I think that's what they're doing with the Pysten thing.
Oh, interesting.
The same one that Apple uses, yeah.
Yeah, I don't know what I'm talking about, but there's a compiler just-in-time platform
called the LLVM, and I don't know much about it.
Yeah, that was Chris Lattner's project, and he's the guy who created Swift, and I think
they're using it there for Swift as well, so interesting.
Yeah, I haven't used Pysten.
Yeah, I mean, I see some news about it.
I know Guido works at Dropbox now, so I guess we'll see.
Yeah, that should give Pysten a little more credibility than maybe just a random project,
right?
Yeah.
Michael here.
Thank you so much for listening to and spreading the word about Talk Python TV.
The response to the podcast continues to be wonderful and humbling.
I have a quick comment about supporting and sponsoring the show.
I'm still looking to line up stable corporate sponsorships, but I wanted to tell you about
a community-based campaign I'm launching to allow listeners to directly support the show.
We are running a Patreon campaign.
You might not have heard about Patreon, but it's kind of like Kickstarter for things like
podcasts, which release frequent small deliverables rather than one-off large engineering projects.
Visit patreon.com/mkennedy.com/mkennedy and watch the video to see how you can donate as little
as $1 per episode to support Talk Python To Me.
This is your chance to ensure that the Python community continues to have a strong public voice.
Consider supporting us today at patreon.com/mkennedy and thanks for listening.
It works with pypy though pretty well, Sequel Alchemy.
Yeah, it took a long time for pypy because the pypy itself had a lot of quirks years ago.
And yeah, pypy is on our continuous integration system.
pypy doesn't use the same kind of garbage collection that CPython does.
And garbage collection is kind of a big deal when you write a database library because it affects
when you have reference cycles getting garbage collected.
A lot of our tests would fail because they were relying upon things being garbage collected automatically.
And that doesn't really necessarily happen as much with pypy.
So basically, if you have a test that doesn't tear itself down correctly and you still have a connection open
that access a table and you run Postgres, the teardown of your test will refuse to drop a table
because Postgres still has a lock on it.
So there's a lot of issues like that that pypy keeps us very honest on.
So that's good.
pypy seems to use more memory, but it runs definitely much faster.
And I think it's a great interpreter and I'm really hopeful for pypy.
Yeah, that's really interesting.
So we're into it.
Yeah, for sure.
Cool.
So one thing that I think is nice about the ORM version or the ORM layer is I can get these classes back from my database
and make changes to them and just basically call commit on the session.
So can you talk about the change tracking?
So that's the unit of work.
So this is the thing.
When I first read SQLogamy, that was the part of this whole thing that I was not even familiar with myself
because I read Fowler's book and he's like, you know, enterprise software uses this unit of work thing
where you just kind of accumulate the changes and then you just push them all in one shot.
And I was like, wow, that's crazy.
And, you know, I could see how it worked.
I'm like, that's really, you know, pretty easy.
You just track it so that all your inserts and your deletes and your updates get kind of put into a big bucket and you sort them out.
And I wrote a very simplistic version of that for SQLogamy 0.1.
It was really bad because I really didn't quite know how to write it.
I was like, I mean, I knew how to write it, but I did this kind of coding, which I did more back then probably that I maybe would term seat of your pants coding,
where you kind of know that this part connected that part and this part connected that part.
But my brain couldn't see the whole thing at once.
And it produced, if you look, you can get the code, go look at old 0.2 of the unit of work and you will be like, what is that?
It's crazy.
But when I first saw it work, I was like, that's amazing.
Yeah, it's super nice.
It's really easy.
Because I was logging to SQL.
Yeah.
And I was like, that's, I think I said, this, that's when it got really, got me really jazzed.
Because like, this thing's going to be big.
When I saw it do that commit the first time and I saw it, all the bros go, I was like, wow, people are going to love this thing.
Yeah, that's really cool.
You know, I actually don't know the answer to this question, but do you have some sort of like optimistic concurrency tracking?
Like if two people pull it back, one person makes a change and saves it, the other one takes that object, tries to save it.
What's the story there?
There's a little bit of pattern that we have in there that I lifted entirely from what Hibernate does.
There's a pattern using a version counter that I don't know that it's that widely used because it kind of interacts with the isolation level you're using in your transaction.
I mean, really, the optimistic concurrency is something you can get in your database just if you set your isolation levels the way you want, if you set them higher.
The version counter idea is that you load the row from the database and the row will have a special column that's the version and say the version is five.
Do I have to indicate that?
Sorry, do I have to indicate that specially on my class?
Yeah, there's a whole, yeah, it's in the mapping option.
So you have a column that you declare as part of the versioning and then the mapper will actually load that in that number or it could be a date stamp if you want.
And it will add, when you go to update the row later, it will actually add that column to the criteria for the update statement.
So it'll be like update row, set whatever, where primary key is blah and version equals five.
And then if the update does not match any rows, that means version five is gone.
It means someone else changed version five underneath you.
And then they'll throw an error.
And that's basically one simple way of doing optimistic concurrency control.
It only applies to updates and deletes.
It doesn't apply to inserts.
Inserts is a whole different ballgame.
People want to be able to insert some value.
And if another thread tries to insert the same value, they want to use the one that's existing.
That's a hard pattern.
There's ways to do that in SQL, but none of them are automatic.
You have to kind of roll it the way you want to roll it.
Yeah, that's interesting.
But I think even that level of updates is really a nice feature to have.
Yeah.
So you talked about the mapping a little bit.
When I create these classes, basically what I do is I create a class that derives from some kind of declarative base instance I've created.
And then I declare the columns.
And how do I do that?
So the declarative system is, again, that was a system that I added to SQLAlchemy back when I did version 5 and I worked for the Charlie Rose website.
We didn't have any declarative system built.
And we had a cheap one that was built into the library.
And then we had a third-party thing called Elixir, which didn't quite do what I needed.
So I wrote a new version, kind of the third version of the declarative system, which is basically you have this base class that you can inherit from.
The original idea of SQLAlchemy was that you write – you would declare table metadata using just core.
You have a table object.
The table object has these column objects.
The name of the table would match the table in your database.
The names of the columns would match the names of the columns.
They have little data types like a string or a date time or an integer.
And then you would take your class, which would just be kind of a blank Python class, and say map.
You'd map the class to the table.
So the declarative idea is how most ORMs that we deal with today look, where you don't see those two separate things.
You see kind of this class that has, like, class, my class.
And then it has, like, id equals integer, and then first name equals string, and last name equals string.
It looks like Django.
It looks like every other ORM you see.
So the declarative thing was added to do that.
So you basically use the concept of a Python class.
You say class, and then you my class, and then you put a colon.
And then you indent, because Python, we're doing white space.
And then you basically declare variables that refer to the columns that will be in your database table.
And then as far as how that creates the database or the database exists already, that's kind of a separate concern.
But you're basically defining what your schema will look like in line with your class definition.
And it's a pretty common pattern that is familiar to everyone at this point.
Yeah, I think it is a pretty familiar pattern.
It's really nice.
And it also ties into, when you're talking about doing the queries, those columns, basically, you can do operations on them, right?
So I could, say, create, like, a query of book.
And I'd say order by, you know, book.name.desc or descending or something like that, right?
So they kind of play multiple roles, letting you do the queries as well as defining the schema and the constraints and all that, right?
Yeah.
So a funny story also is that it wasn't like that in the beginning.
It used to be that you'd have your mapper and your class, but the class itself wouldn't have these attributes directly on it.
It used to be you had to still talk to the table object.
So if you had, you know, class user and it mapped to a table object called users, you would still have to query in terms of users.c.firstname.
It wasn't actually on the class object because I didn't feel comfortable doing that for a while.
And even though SQL object did do that, I kind of was hesitant to do that.
But then once, I think maybe in version 4, I said, hey, you know, actually I can put the attributes on the class and have them be the actual column objects that we – they're actually not column.
They're actually a wrapper around the column, but it looks like the column object.
So, yeah.
And then that led to this whole new thing called the hybrid property, which is like the fact that Python has this amazing symmetry in that everything is ultimately an object.
If you have a class, it's still an object.
So if you say myclass.foo versus an instance of myclass.foo, they look the same in code.
And that kind of blew my mind.
That was kind of a big discovery I made about Python like maybe five years into it that Python has this – I mean, I knew I loved Python's object model all the way through.
But really, the hybrid thing really blew my mind.
And there's a – I think I wrote a blog post a long time ago that I could probably point you to where I show – look at this hybrid thing where you can have a class with a method that does something at the class level and something else at the instance level.
And it just was amazing.
And I use it a lot on some subsequent jobs, and we did really amazing stuff with it.
It's one thing to sort of realize that that feature is there.
It's the other to say, let's use that to make a data access layer really work super well and make it – you define one thing, and it has this dual purpose, which is really cool.
Yeah.
There was a guy – I think his name was Ant Asma, and he was working on SQL for me a long time ago, and he first showed me a pattern that had that hybrid thing.
And I was like, wow, and I really dove into it in the coming years, and I really realized it's a very handy pattern.
Excellent.
So you're talking about creating these columns and stuff.
What if I've got a field that's – or a column that's not nullable or it needs a default value?
How do I do that?
Yeah.
So the table metadata part – so say we're back in core.
We're not doing over them again.
So we're back in core – one of the goals of the core was that it would be this comprehensive system of building a whole schema from scratch.
So meaning if you have a blank database, you can build up all of your objects in Python and then say, you know, metadata.createAll.
And that will emit what we call data definition language, DDL, which we know as createTableStatements and createSequence and createIndex.
It will emit all those things for you.
So in order to make that feasible, we have to also have support for attributes like if a column is nullable or not, if a column is a primary key, if a column is indexed, if there's a unique constraint on a set of columns.
So SQL Alchemist schema system has objects and flags that represent all of these concepts.
And it's pretty comprehensive.
And we have full support for all kinds of data types and numerics and floats.
And we have server defaults.
And we have every common thing except for triggers.
We don't have a system for triggers.
You can define your own system for triggers.
And we don't have store procedures.
So all the main things that you normally, your day-to-day database schema stuff like tables, columns, constraints, foreign keys, indexes, nullability, and defaults are all first class.
Other things you can define like sequences we have.
Other things like triggers and store procedures, you can define your own Pythonic little objects that will do that for you but more in a database-specific way.
So anyway, when you do the ORM and you're doing declarative mapping, you're still using core column objects in your declarative mapping.
So when you make a column object, you can still specify all the table attributes as well as you do your declarative mapping.
So that's kind of the answers.
They kind of meld together.
And it was not that way in the beginning, but it evolved that way over the years to work that way.
Yeah, it's really smooth.
So I have these classes, and I can map them to existing databases.
And you also said it emits DDL, which I can create my tables from, right?
Yeah.
That's just a function call, isn't it?
Go to my declarative base.
Was it create all, something like that?
Yeah.
So the metadata object is kind of this collection of tables and other things, mostly tables.
And you could say metadata.createAll, and you give it an engine to point at, like a database connection.
And it will go on that database and see which tables don't exist and create all the tables that don't exist yet.
That'll create them from scratch.
What if I say add a column?
Right.
So the whole thing with adding a column and doing that, we call that migration.
So, you know, as we go through this, it's funny.
Nothing in SQLAlchemy was there in day one.
Everything was like I had to figure it out and learn it.
Like I actually did a talk for SQLAlchemy version 4, and they asked me in the audience, what do you do about database migrations?
I'm like, well, I don't really know much about migrations, Automata.
I used to do them.
I do them by hand.
So I don't really have a migration tool.
So some people wrote a tool called SQLAlchemy Migrate.
And that went pretty well for a while and is still in use today.
And then I eventually wrote a newer version called Alembic.
So that's kind of a separate package that you can download.
And Alembic builds on top of SQLAlchemy as does SQLAlchemy migrate, which is still in production.
And that defines a whole bunch of new operations on top of SQLAlchemy that are specific to being able to add columns, to alter the types of columns, and alter the nullability, you know, to remove columns, rename them, things like that.
And my database migrations is a whole big issue unto itself.
So it took me several years to really get my head around that, to, again, use that in production on the job because the best way to write open source code is to be, you know, what we say, eating your own dog food on the job.
So I've used Alembic a lot.
Alembic is now really widely used.
It's very prominent within OpenStack, which I work for.
And that's kind of the path to doing migration is kind of separate from SQLAlchemy core itself.
It's kind of built on top.
Yeah, that's cool.
How about relationships?
Like if I've got a bookstore and I've got categories and there's books in the categories and reviews about the books and so on.
So that aspect of object relational mapping is really where object relational mapping starts to look like this concept that is really going to change.
The way a relational database looks compared to the way an object jointed data model looks.
So in relational schemas, if we want to have a table that refers like a row in this table, it's like say it's an account row or something, and it needs to refer to like some other related concept like an address.
In relational database, we use something called a foreign key constraint, which means that we're going to – you don't have to actually use the constraint, but you would say that this particular column or set of columns in our account table actually refers to the primary key of a different table somewhere else.
And that's called a foreign key.
And the constraint in the database makes sure that the value you put in those columns actually exists in the remote table.
So in ORM, ORMs have this concept of either making a one-to-many or a many-to-many or a many-to-one relationship, which models in object-oriented terms what that looks like in terms of collections.
So if I have – in all of our tutorials, we have this concept of a user object, and the user has one or more email addresses, and it's a really old example, but I keep going back to it because I just know it really well.
So your address table in your database will have a foreign key called here's an email address, and what user is this – what user is the owner of this address?
It'll have a column called user ID or something, which will refer to the primary key of the user table.
So the user table kind of refers to – I mean, the address table refers to the user table.
In your model, you want to have the user object has a one-to-many to the address class.
So it's kind of – if you define a one-to-many, it's kind of defining it in the other direction, like user points to address.
But in your relational database, it looks more like address points to user.
But it turns out that every one-to-many relationship in object-oriented land is the same as the many-to-one in the other way.
So SQLAlchemy basically lets you make the one-to-many and the many-to-one at the same time using this concept called a backref.
This is a little bit rambly, but if you look at our tutorial, it's laid out in a much more organized fashion.
So, yeah, there's basically an object – there's a function in SQLAlchemy or called relationship where you associate it with a parent class and then you give it a target class.
And relationship will make sure that the foreign key setup that is expected exists between those two tables.
And it will kind of mediate that pattern on behalf of the object model to the database schema.
Yeah, that makes it really easy to almost not even worry about that.
So is that like a lazily evaluated thing?
So if I pull back a book, it's not going to go pull back all of its reviews instantly unless I touch that.
Right. So that was one of the patterns in Fowler's books called lazy loading, which is the concept that if you have an object and you have some kind of method or attribute that represents data from your database, you might have the object and it didn't actually load that other data.
It's only when you touch that attribute or method that it suddenly goes, oh, I have to load this stuff and I have to go out to the database and load more information.
That's an efficiency pattern called lazy loading.
It means you can load a lot of objects and not have to go through all the time of loading all the related things.
All ORMs do lazy loading, just every ORM I've ever seen.
But lazy loading is also at the core of this somewhat controversial concept in ORMs called the N plus one problem.
Right. That's probably the most common performance problem people run into is they don't realize they're doing this N plus one query.
So I get one book or I get one book.
So that's the one.
And then I try to touch the other items or a set of books.
I loop through them every time I touch that property.
I'm doing one more query.
So for N books, I do N more queries against the database, right?
Yep.
That's the N plus one problem.
And every article ever written that is against ORMs will have that entire example as the cornerstone of its premise.
So the other thing that SQL Alchemist had from day one, besides the unit of work, the other cool thing it had was this thing called eager loading, which means that you can, if you kind of know you're loading these users and you want the addresses also, you can say to the query, hey, load the user.
But then also, can you also load the addresses at the same time instead of me doing the N plus one problem?
And in that way, you will only do, if you do what's called joined eager loading, it will actually do a join, an outer join.
It will load your user row.
And while it does the select for the user row, it will do an outer join to the address table so that it gets zero or more address rows at the same time.
And then the query object knows how to organize the product that we get back.
Because if you do a join in SQL from one table to a many, you'll get the one row repeated.
So the ORM knows how to deduplicate those rows on the left side of the join.
And that's called eager loading.
And one of the tradeoffs of eager loading is that when we write a query, we say, hey, Mr. Object Database thing we don't know anything about.
We want users.
And then later I want addresses.
If I have to say, hey, I want users, but I'm also going to want in the future these addresses, that is what you would call a leaky abstraction.
Because it means that the way the system is going to do its load, you need to tell it up front how to do its job to some degree.
You have a little bit of awareness that this is not just like some weird LDAP database.
This is actually a relational database.
And these two things are in two different tables.
One of the things SQLAlgmy embraced very early on, and one way that is very different from most other ORMs, is that it did not try to get around that issue by pretending the database doesn't exist.
It's like, you know what?
This thing is to automate our work.
It's to make our lives easier and write less code.
It's not there to hide that there's a database.
So we need to be aware of things like if we want to load the 1 and also the N and not have N plus 1, we need to give the query a hint up front to do that.
So that's ego loading in a nutshell.
Yeah, I really like that philosophy that you guys take looking, you know, saying, look, we're not going to hide this from you.
We're going to make your life easier and less error prone, but we're not going to treat you like an idiot or hide this stuff.
Or, you know, you can still get to what you need to.
That's great.
Right.
How do I do that in code?
So what do I say on my query to say I want to eager load books and reviews?
So modern SQLAlchemy, I mean, this is, again, APIs have changed dramatically.
Modern SQLAlchemy, you would say query for a user class, and then there is a call on the query, which is a method chaining system.
You say query.options, and then you add to the options function a bunch of constructs called join load or subquery load or immediate load.
There's all these little directives we have that you can say, hey, query for user.options, join load user.addresses, join load address.related something else.
And you can kind of give these directives in the options of the query as to what other things it should be loading onto the primary thing that you're loading from.
Oh, very cool.
You've been talking about the different versions of the API.
How have you dealt with versioning that API over time?
Well, I mean, it's been 10 years.
And what I kind of did in the early days, you know, we were first 0.1, and then we were 0.2, and 0.3.
And back around 0.3, someone wrote an O'Reilly book for us, and we're like, you should be 1.0.
And I'm like, no way.
We are so early.
Like, this thing is totally not at all how it's going to be.
So with 0.1, first of all, I didn't even publicize the library at all in those early days.
I just maybe announced it on one mailing list.
And people just came, kind of just came and started using it.
They started using it in production.
And 0.1, 0.2, I made enormous, enormous changes that just totally ripped the rug from everybody.
But we were 0.1.
Sure.
Did it make you nervous to hear that they were, like, so heavily dependent on this new thing you created?
People got way too dependent too fast.
And in early days, I think Reddit started using SQLAlchemy, maybe version 0.4 or 0.5, you know.
And there were some messages from them years ago.
I don't know where they were, so whatever.
Where they were kind of complaining about SQLAlchemy changing its API too much.
And really, the API, I made the biggest changes before version 0.5.
0.5, we really started settling down to this new kind of thing with the query, with the attributes linked to the class.
And then we had to keep the API changes all the time.
And from 0.5 on forward, even from 0.3 and 4, I always made sure that there was a backwards compatibility path.
I made sure that everything that changed was always announced.
And if something happened that was unexpected, that was a regression, and we would address it.
And it was almost never that I said to someone, you're stuck.
You'll have to just change your code now.
Like, I always made sure that there was a backwards compatibility path.
But it meant you had to kind of, especially in the early days, it meant you had to really kind of watch when we did the next major number, which is really the minor number.
But we call it the major number.
You needed to watch what was changing.
You needed to test your stuff ahead of time.
Things are better now.
Now that we have pip is much better.
And we can put up beta releases that won't automatically install for people.
And I always put very, very, very comprehensive migration notes for every version.
You can see them going back on the site, probably back to version 0.4 or 0.3, where I say, here's the things that are going to change.
Here's the things to look out for.
Here's the things to test.
And after version 5 and version 6, version 7, it changes less and less each time.
People went from version 7 to 8 with very few hiccups.
You know, 8 to 9, 9 to 1.0.
There's really nothing you have to change in your code anymore.
We like people to test because it might be surprises that we didn't expect.
And we fixed those.
But APIs very rarely break in any kind of backwards incompatible way.
The only time it's a problem is that if there's something that was broken in SQLAlchemy 8 or 9 and people were relying upon the broken behavior.
That's the hardest one is when people rely upon broken.
And they didn't know it was broken, so you can't blame them.
When we fix things that were broken, we'll usually make it raise a warning in the current version they're using.
Like 0.9 has something that's broken.
We'll have it admit a warning, so they'll update, upgrade to 0.9.9.
And all of a sudden they see these warnings.
It'll say, this will be an exception to 1.0.
So they'll kind of know to change it there.
But you do need to be watching the versions and the changes.
I've worked really hard to make all the change logs very easy to find on the site.
I mean, other projects I can never find what changes.
It's very hard to find.
I think our site is very transparent, and you can see everything happening in many ways.
And it's very highly linked as far as the development log.
Yeah, that's excellent.
It is super comprehensive.
So one thing I think is interesting is maybe talking about some of the notable users of SQLAlchemy.
I know you've talked a little bit about a couple of them.
But you've got a whole list at sqalchemy.org slash organizations dot html, right?
Yeah.
So, yeah, I think I first saw MongoDB was doing that.
They had this enormous list of people using their stuff.
And I'm like, I want that, like a big wall of honor of all my – when some big company uses your product, it's like, wow, I'm important.
I did this important thing.
So up there, there's a lot of big – Reddit was the huge one.
Reddit only uses the core.
They don't use VRN.
Their code is very unusual in the way they do things.
And I'm glad they don't use the ORM because they would really be killing it.
Yelp uses a lot of SQLAlchemy.
I've been – I visited Yelp.
They use the ORM a lot.
Uber, you know, uses SQLAlchemy.
Hulu.
Dropbox is there, right?
Yeah, Dropbox.
Yeah, Dropbox is another one that uses the core.
They're another – you know, they actually wrote some blogs that were a little bit critical of the ORM, which I was kind of annoyed about.
But they use the core still.
They use it.
Yeah, a lot of animation studios use it.
There's a lot of companies that use it who I know use it, but I don't really exactly have permission.
You know, one of them is, you know, kind of a Mickey Mouse operation anyway.
A lot of companies that are up there in organizations, you know, Fedora uses it.
And, of course, now I work for Red Hat and I work for OpenStack.
And OpenStack is this cloud computing platform that's open sourced.
And a lot of companies like PayPal and IBM and AT&T use it.
And that thing uses SQLAlchemy all throughout.
It's super heavy SQLAlchemy throughout OpenStack code.
So it's a very widely used library.
Yeah, that's really excellent.
You must be proud of it.
Yeah.
Cool.
And you should be.
So what about the whole async movement with, like, Twisted and AsyncIO?
How does that play with SQLAlchemy?
So the async thing is something, another Python story where it's something that I wasn't really that interested in.
But it's a big deal in Python, the async.
Because when I first came to Python, the two, you know, 800-pound gorilla things that existed were Zope and Twisted.
You know, the old school, you know, the old guard of Python is Zope and Twisted.
So Twisted is great.
It's still out there.
Async, the problem with what we call explicit async is that it's not really compatible with this idea of lazy loading that we see Martin Fowler first described.
The concept that when you access something on a class, it's going to automatically, behind the scenes, go and fetch data from a database.
It means that it's going to do I.O.
And when you do things with the async model, the async model is all about that input-output operations should never be implicit.
They should always be something that you code into it and say, hey, this is about to do I.O., so please stop what we're doing, do the I.O., and then let the whole interpreter go and do something else.
And we do that in the newest version of Python's async I.O., which is in Python 3, you do a yield.
And it's very slick, but it still is a statement that is an interruption in your code.
Right.
You have to explicitly say, I'm waiting here, I'm waiting here, I'm waiting here.
And at any of these points, you can take that thread and go do something else with it until I'm done talking to the database or something, right?
Right.
So people have wanted SQLAlchemy to work a twist for a long time.
I've looked into it.
I actually had some clients pay me to look into it.
And every time I look into it, it's like, eh, it's really not worth it because we have to completely rewrite the whole core engine part of this thing to be this kind of inside-out callback routine.
This is with classic Twisted.
How about the async I.O.?
Right.
So with async I.O., there's a big blog post I just wrote in the past couple of months that's about – it's called Asynchronous Python and Databases.
And it's gotten a lot of views.
And it kind of sums up the whole situation.
I went into async I.O.
I looked at it.
I think it's really great.
And it's definitely a huge improvement.
Async I.O., I actually could write – a lot of SQLAlchemy could be made to work very seamlessly with async I.O.
In that it wouldn't require that much code to be replaced.
What's nice about async I.O. versus the older callback style is that there's kind of some ways you can have the async thing be explicit on the beginning of where a method does something.
And then up the call chain at the end.
And you can kind of get away with the calls in the middle not knowing that they're doing async somewhat.
There's a little bit of Python syntax helping here.
So I looked into how I could make the core do async I.O. for real.
And actually, it can be done.
I started writing parts of the pool.
And it would be a lot of work, but it could be done.
But in this blog post, I wrote, like, do we really need to do it?
Because when you use async packages, even when you use Twisted, and Twisted has been doing this from day one for databases,
when you have code that really works better with threads and traditional, you know, preemptive, not preemptive, but traditional threading,
you throw that code into what's called a thread pool.
And then the asynchronous part of your at library can actually call into routines that are invoked synchronously within a thread pool,
but they are given kind of an asynchronous facade.
And I think that is the best way to do it.
If you're writing a lot of database code, you really don't need to worry.
You really shouldn't have to be worrying about deferring to I.O. within the database,
because you really, in the database code, you're in a transaction.
You definitely need to do A, B, C, D, and E.
It's not really a mystery when the I.O. has to happen.
It's very kind of boilerplate-y.
And one of the entire reasons that SQL Alchemin exists is to reduce the need to type repetitive boilerplate.
That's the whole point of it.
You don't need SQL Alchemin to do anything.
You can do it all by hand, but the point is to reduce boilerplate.
So to that extent, I think you're better off putting your database code in a thread pool,
or using a system like eventletg event that is an implicit system.
And the blog post really goes into this in great detail as to show the benchmarks, why I think it's better.
And I think a lot of people agree with it.
Cool.
Okay.
You'll have to send me the link to that blog post.
I'll put it in the show notes.
Excellent.
So I think we've been talking for a while.
It's probably a good place to wrap up the show.
And a question I ask the guests on my show on the way out the door is, you know, there's a ton of packages on PyPy and out there in GitHub and stuff.
What ones are notable to you?
What ones are your favorites?
Seek walk me, of course.
Yeah.
On PyPy?
What's your favorite PyPy package?
I use pytest quite a lot.
I recommend pytest.
I recommend the mock library, which is actually part of the Python standard library now.
Mock and PyPy are the ones I see myself downloading most of the time.
You know, what else?
There's probably others, but they're not coming to mind at the moment.
I'd have to look and be like, oh, yeah, I use that thing a lot.
You know, I mean, obviously, NumPy is the big one.
Yeah, NumPy.
Yeah, NumPy is good.
Like I said, I think Sequel Alchemy is certainly one of the top ones out there.
Yeah, yeah, it is.
So before we call it a show, is there anybody or a thing you want to give a shout out to and call attention to?
Oh, well, my cats for helping me with the show today.
And, you know, just I'm really appreciative of the people that come to see my talks and the people that talk to me on Twitter and tell me how great I'm doing.
And even the people that, you know, naysay me as well.
I get good feedback from them.
So I'm really glad that everyone is kind of following along with what I'm doing.
Excellent.
Can people come and contribute to Sequel Alchemy?
It's on Bitbucket, right?
Yeah, I mean, it's a big code base and people do contribute all the time.
I get pull requests all the time.
through both Bitbucket and GitHub and you can send a pull request.
We haven't done any sprints in a long time.
I mean, that's something that we could do again if I had people to help me with that.
You know, and people in the OpenStack community help out.
So it's a very open project.
If you go to the website, there is a toolbar that has community links and it has a link called Develop.
And it'll show you all the ways that you can get involved with helping develop Sequel Alchemy.
Awesome.
Mike, thanks so much for being on the show.
It was a really interesting conversation.
Yeah, thanks for having me.
You bet.
This has been another episode of Talk Python To Me.
This is your host, Michael Kennedy.
I want to say thank you for listening.
And if you want to support the show, be sure to check out our Patreon campaign at patreon.com slash mkennedy or talkpythontome.com slash sponsor.
Remember to check out the show notes at talkpythontome.com where you can find all the links from the topics we discussed in the show.
And before you go, take a moment to become a friend of the show.
Visit talkpython.com, click on friends of the show, give us your email address, and you'll be in the group.
Smix, take us out of here.
Stay tuned.
Bye.
.