 All right. Can everyone hear me? Hello. Hello, everyone. I'm Max. I'm an engineer at Dropbox. I've been there like the very kind introduction mentioned for a long time. I speak, actually speak French, so this is very exciting for me because I get to hopefully practice it a little bit this conference. Although funny enough, you'll notice I probably, to you, I'll probably have an accent in both languages. So why am I here? Well, Dropbox loves Python. So in fact, our very first lines of code ever were written in Python. Some of our key infrastructures also written in Python. Our apps for Windows, Mac, and Linux are also running Python. Basically, we're fans. We've used it all. We've used async, core, multiprocessing, pickle, sadly. We've done many, many things with Python. We really like it. Python is kind of widely known as a general purpose language. It's kind of the phrases its batteries included. It has a really strong standard library. We liked it because as a startup, you know, it's great. It's really good for prototyping and you can get going really quickly. It's pretty popular for backend development, of course, and for a bunch of other use cases as well. But what you may not know is that you can build client side and user apps in Python. Now, this is a bit less common. It's not the road most travel, but this is something we've applied. So you want to make an app. What exactly is an app? So I guess for the purpose of this talk, let's define an app as some software that you write that is installed on a user's device by that user. That complies with that device's platform. So in this case, by platform, I mean basically the operating system. The platform is important because it defines both what the app is. So that means the format that the app has to take and also what the app can do. So that usually means the frameworks or the APIs that the app can use. Now, Python's batteries are kind of great in a sense, but they're also bad in this case because they hide all of this from you. So when you're using Python, you have an interpreter and you have scripts. And you get to not really have to care as to whether you're running the script on a Mac or on Windows most of the time anyway. So to build an app using Python, you kind of have to look behind the curtain. So this isn't exactly a common use case for Python. And it's not really directly served by its standard library. So crossing the gap from a script to an app can be pretty challenging. But it is absolutely possible and we've been able to do it at Dropbox. So there are many, many topics to cover, but there are four large areas I'd like to explore. And what I'll do is I'll walk you through our journey into building an app and I'll point out some of the more interesting decisions and lessons that we've learned. I also want to share some of our favorite Python libraries that are open source that we've been using. And it's actually quite fitting that we're doing this at EuroPython because many of those were written by people in Europe, actually. So first up, we'll talk about integrating with the platform's frameworks. So this is where the app or where the OS defines where the app can do. And that's the first step into building an app. Second, we'll look at how to package an app into a format that is recognized by the OS. Third, we'll talk about what happens after you've packaged and how you can monitor your app and sort of evolve it gradually over time. And finally, we'll go into beyond Python and I'll get to what that means exactly in a minute. So let's start with the first section. So for an app to work, it needs to be able to use the platform's frameworks. So to build an app, that basically means you'll need to use the OS's APIs directly. We've been calling this kind of unlocking the platform's binaries or the platform's batteries. So why would you want to use OS APIs? You know, there's perfectly good standard library in Python. Well, each platform has very specific APIs and some of them have use cases that only really make sense on the type of device supported by that platform. So we found out very quickly that POSIX module or OS wasn't quite enough for us. And that's really because it kind of turns into the slowest common denominator. So Python supports so many OS's and platforms that the sort of results standard library that it has is a bit of the generic look of something that works on all of these. In some cases, you're going to have use cases where you're going to want to use the platform's APIs directly. And this, you know, your mileage may vary. But for us, this was very, very common. And for example, since our job is to sync files, and to actually smooth out the differences between Windows and Mac and Linux, it's actually important that we see what those differences are. So all of this is to say, really, it's likely that if you're building an app, you're going to need to at some point use some OS APIs directly. So how should we go about using those APIs? Now, most platforms, sadly, don't directly support Python as a first class language. To use those APIs, you're going to need bindings. And what we mean by that are ways of projecting non Python APIs into Python. These are great because they let you use those APIs from Python as though Python was a first class supported language. But bindings aren't free. That's our first lesson there. Bindings are actually relatively costly, if you're not careful. And also one really important thing, the native compilers and tool chains are always going to have a little bit of an advantage, no matter how much glue you're able to introduce. So keeping that in mind, let's start with the most common types of bindings, C. So almost very common, almost every single platform has a C API. We've used a lot of C types. I think there are tens of thousands of lines of C types probably in our code base. This is kind of a canonical way to do this in the Python, the C API. And these worked really well at first. But they didn't really aged well. And the reason for that is something that I think the CFFI team explains really well in their docs, which is C types operates at the API level. And what you really want most of the time is to operate at the API level. So what we mean here is that because you have to write these bindings manually, it's really easy to make a mistake because these are generally meant to be used with a compiler, not meant to be used with this type of jumping. So let's take an example. Here's an example of some very simple, simple C API that you might want to use. What you need to do, and if you know C types, this won't really be neat to you. But to make this usable from Python, you have to manually write bindings that kind of teach Python what this means and how to use it. So the equivalent C types look something like this. So once you do this, you're able to call the function. And this is basically something where you tell Python, hey, this structure exists, it has these fields, this is a function, this is a result type, the arcs types, etc. But the warning here is that you don't have any compiler. In fact, you have complete control here. Really, once you've created this, and you call a function, Python just kind of jumps and hopes for the best. So if you make a mistake, which turns out is actually pretty easy if you use the wrong type, for example, it might actually still work, but then cause a crash later down the line. So the worst case scenario here happened for us when we moved on Mac OS from 32 bit to 64 bit support because almost all pointers changed size. But then this had very subtle effects on structs. And the only way for us to really detect where we made mistakes was to wait for the segmentation faults to come in, which was not great. So C types is the basic way, but there's a much better way. The alternative here is to write Python extensions. And this is much better. Now, these are written in C, they use the Python C API. But they're great because they let you leverage the compiler, which is safer. So rather than just jumping and hoping for the best, you actually generate, and these are great examples of libraries to let you do this, CFFI by PIPI is amazing. We also use Scython in some cases where you need really complex bindings. But these are great because they generate a C extension, and then the C extension is compiled. And it actually uses the real header of the C library that you're looking for. And so this is, this means that if you do make a mistake with the type, the compiler will yell at you. And one of the nice things you learn about software engineering is that the earlier you're yelled at by a compiler, the better. Quick honorable mention for Scython, it's actually really, really great. We use a lot of CFFI, but that's mostly because we don't need the complexity that Scython sort of gives us the ability to deal with most of the time. So that covers C, but that's nowhere near the end of the story. Many platforms actually have non-C APIs. Some platforms have C++ APIs. And these are actually quite common, but binding C++ is much harder than binding C, because C++ is very, very complex. Because of things like mangling, templates, et cetera, it has a stable ABI, but it really is quite complex. So there are solutions here, things like Swig, SIP, Boost Python, but these tend to be really, really bulky. Now our favorite so far is PyBind 11 by Wenzel Jacob, who's actually, I think, works at EPFL here. This is incredible. This library is amazing. It uses C++ 11, 14, 17 features that's header only, and it generates a Python extension for you using the types that you're actually linking against. It's really fantastic and it's been really helpful for us. So now that we've talked about generic APIs, let's go into each platform. So let's start with Mac. Mac is one of the OSes we support. When, while it does have a POSIX API, because it's BSD-like, the vast majority of its APIs are actually written in Objective C. Now these are object-oriented. They have a runtime. They have memory management, so you have to do ref counting. So not really the best thing to try to integrate as it's at the C types level. Even though you could technically do that, that's probably a bad idea. There's an awesome project that we've used very extensively. It's called PyOpC by Ronald Assur, and who I think I saw at the conference actually, so. And this allows you to use directly Objective C in Python as though it were a first-class language. And let's look at what this would mean. So this is some sample Objective C that lets you show a notification. We use this quite a bunch to show little warnings when you sync files, for example. And this is the Objective C version, but this is what it looks like in PyOpC. So it's actually quite similar. Of course, it has to deal with the fact that there are no square brackets in Python, but the equivalent here shows you that the power of having Python act as a first-class language is very similar. With something like this, you can actually go and read the Apple docs and just start writing code directly as though you were writing Objective C. And this is really powerful. The result is something like that. Let's save a minute to talk about Windows, because Windows is the most popular in the world, sadly. If you are targeting Windows as we do, there are a lot, a lot, a lot of APIs that you can use. Some are in C, but many are object-oriented using component object model or COM, which if you're not familiar is a multi-language standard that lets you do object-oriented APIs. It's generally used with C++, and it gives you access to things like the Shell API. So if there's any Windows programmers out there, this is how you like make shortcuts, for example, on the desktop. Now you could try to bind those using C types, but this is so much more complex than calling a function. It is very verbose, don't try it. We have, we tried that very briefly and it did not go very well. What is much better is to leverage the work done by experts, and the legendary Mark Hammond and PyWin32 is something that we've personally loved. This gives you really simple access to not only the C APIs, but also more complex COM APIs in Windows. This API is almost 20 years old now, this library. It's actually on PyPI now, which is great. We actually use this to manage things like ACL. So there are some structs, for example, in the Windows APIs that are very, very complicated, and this is just provides these nice pythonic wrappers around all that. And this is just great. Now Windows being Windows, there's always another new API lurking around the corner. The newest Windows APIs coming from Windows 10 are based on something called WinRT or the Windows runtime. This is actually very similar to COM, but they added a whole bunch of enhancements. COM is almost 30 years old, and so this is a much more modern replacement. This set of APIs includes things like coroutines and asynchronous functions. This is also where most of the new APIs are being introduced by Microsoft. So things like the machine learning APIs, WinML, are actually only available through this. This can be a bit challenging because there aren't really bindings, there weren't until recently. There's a brand new project out of Microsoft called X-Lang that actually introduces Python bindings for WinRT. Highly recommend you check that out. It's really impressive, it's very cool. We're not using it yet, but this actually also integrates with Python's async IO coroutines and lets you call all of these directly, which is really really sweet. So now that we've looked at how we would use APIs, let's dive into an example. So that's probably the most common type of platform integration you'll want to do is to try to write a GUI. So you'll want to write a user interface. Now this is because you know most of your apps, or most of your users, aren't going to interact with your app the way a developer would. They're going to want some sort of visual interaction, things like with buttons and windows and stuff like that. This is actually a great example of a battery that isn't really included in Python. There is Tickle, there is Peek-n-Tur, but that is kind of the exception. So this is the preferences window in Dropbox today. It's actually all written in Python. In fact it's all written in PyOpC. This is the Coco version. So how do we go about building this? And this comes, this leads me to sort of the main key choice that we had to make, which is what library do you pick? And there's kind of two classes of UI libraries you might want to use. There's the native ones. So this is where you would use bindings to write or to directly use each platform's UI library. So typically each OS is going to have its own UI library that is dominant. Your other option is to use a third party library. So you bind to that, but then that library does the job of integrating with each OS's UI. Now the trade-off here is convenience versus control. So third party libraries are much more convenient because they almost always cover more than one platform. So that means you can write your Python once and then it will work on more than one OS. Picking a native library, however, gives you only that platform, but it gives you much more control. But what that means though is that if you use Coco on Mac and then you use Win32 on Windows, you are going to be writing it twice. So what we actually decided to do is both, which is a bit weird, but bear with me. So on Mac we use Coco and we use PyObjective-C. So we use a native library on Mac and then on Windows and Linux we actually use Qt or Qt, and then we use PyQt's bindings to do that. So why do we pick a third party library for one OS and a native library for the other? In our case it's because Dropbox's UI is tiny, or at least at the time it was. We only have a little icon, we have a couple of windows, we don't have a basically a very small footprint, and we wanted the field to be very native on each platform. We wanted people to feel like this was really a part of their OS. And on Mac, Coco is by far the best, but on Windows Qt actually gave us the quality that we needed. And so basically we were able to combine the two and in solution that was generally acceptable. So to conclude this section, I want to talk a little bit about what makes an app different from a script. So the sort of main point here is that an app is going to have a very different shape to a script. First, it's going to be long-lived, whereas scripts tend to be shorter-lived. Really your app can stick around as long as the user needs it. They, you know, that can be arbitrarily long. Second, you're going, the shape of your app is going to be more reactive, rather than proactive. So for example, you're going to be spending a lot of time waiting for the user to do something, so that you can do something in response. What that usually means is that your code is going to be callback driven, and so you're going to write callbacks in Python, basically to do things once the UI tells you something happened. And finally, you're going to enter the world of threading, and the reason for that is that in almost all UI frameworks, the main thread is special. It's there to deal with the user's input, and so generally that's going to mean doing work on other threads. So let's look at this visually, because this is mostly just rambly. So a script is a very simple straight line. It kind of starts, and it stops when its job is done. Typically you give it input before you start, and you receive the output after the script ends. Now an app is a bit different. An app isn't going to stop deterministically. It's going to stop when the user's done with it. So it might not terminate for a while. So really the shape of your app is going to be a loop. So the app will basically just keep repeating something over and over again. And at every run of the loop, it will process inputs from the user and produce outputs. It'll keep doing this over and over again. And that's kind of the usual shape of an app. Now what that means though is that while your main thread is busy doing this, the actual work that you want your app to do is going to happen on another thread, or in some sort of background system of some kind. Now we'll get back to threading a bit later, but fear not, all of these GUI libraries typically have helpers to help you sort of manage threading in this way. So let's recap. Every platform is different. You are probably going to need to use some platform name of APIs, if not a lot. Bindings are definitely not free. There's some costs associated with each of them. But they're off the shelf or open source solutions that can really, really, really reduce maintenance costs. And finally, well, apps are not like scripts and you're probably going to be multi-threaded. So now that we can integrate with the platform and now that we have the shape of an app, your customer is still going to be the end user here. So to obtain your app, they're going to need to be able to get it somehow. And that means we're going to package the app in a way that's easy for a user to get. So how do we get into user's hands? Before we get into that, one quick detour. So we talked about using third-party bindings, and I think we've demonstrated so far that PyPI is your friend. There are tons of great packages out there that can help control cost. But using third-party code in this case means shipping it to. And so it's really important to take precautions when you do that. For example, with licensing. End user software is very different from cloud-based software in the licensing world. So securely taking dependencies is actually really, really important. And we have a whole process at Drawbox to deal with this. What's one thing... The important thing to remember with third-party depths is that they become part of your app. It is part of your code now. So we came up with four rules that we apply when we deal with these. So first, build everything yourself. So don't trust binaries. Now this isn't a huge problem with Python, because Python's interpreted, but if you have a choice between a binary or a source code, pick the source code. The reason for that is that the second rule, which is you can audit that code. So you can look at it and really see what it does. You should really try to spend the time to assess the code you intend to use before you ship it. Third, pin the versions of your dependencies and check the hashes. This is really important to protect from remote breaches. So if you don't do this, it's very easy for you to make a new build one day, realize there's a new version of some dependency you're using. But if that version is compromised, now your app is compromised too. And finally, make sure to audit the licenses. So you should really understand and accept the terms of all the licenses of each package you use, because if you're not careful, that can be really bad. So let's take a look at... This is a very small snippet of our requirements file. Drawbox is actually a Python package, so it has a requirements.txt. This shows a couple of our dependencies, and you'll notice the version selector, so we always fully select exactly which version we want. And we also pass the hash parameter, which prevents the package from being randomly tampered with. So if someone were to somehow man in the middle between us and PyPI, and say, oh no, actually version 1.12.0.6 is actually this, then we would detect that and we would fail. So as another tip, isolating your environment is a pretty good way of maintaining control. So pip-end and virtual-end were great. We actually have an internal tool we use to wrap virtual-end. And this is good because it means all of your developers are going to have a consistent view of all of your dependencies. It's really important that everyone gets the same thing. Okay, so how do you take all this first party code and all this third party code, and you turn all that into an app? Now as a reminder, each app format varies from one platform to the next. So there are basically four things that you want. One, you want to build the sort of native entry point. That on Windows, that's a .exe. On Mac, that's a .app. Second, you actually want to ship Python itself so that means the Python DLL or the Python framework. Third, you want to take all of your .pys and turn those into PYCs. And finally, you want to compile all your Python extensions. You want to make shared objects or PYDs. This is a lot of work, but thankfully there are solutions out there. And the most common solutions to these are what we've been calling the freezer libraries. And these basically, what they do is they turn your script into a platform-specific binary. They do all of those four steps for you. I think one of the most popular ones right now is PyInstaller. It's cross-platform. It seems to have really caught on. On Mac OS, actually, for many years we used Py to App. Also by the same author as PyOpsy. One quick tip, though. Some of these will give you the option not to ship Python itself. That's actually a very bad idea. So if you decide not to ship Python, what that app will do is it'll try to use the system Python if there is one. And that can be very dangerous because it means other packages can be available to your app. And since Python is very dynamic, those can actually tamper with how your app executes. So it's really important to isolate and control your Python environment. One other random thing while I'm at it, which is you should also try to remove Dock string. So by default, I think if you pass an optimized command to compile when you make a PyC, it will not remove Dock strings. So we obviously use Dock strings internally as a form of commenting. And so you might want to actually remove those somehow as a post-commit because otherwise people can probably go find your PyCs and read all of your comments, which could be bad. Unless you actually want them to reverse engineer your code, which is perfectly fine. Now, a bit of foreshadowing. Freezers do have a little bit of or a few limitations and we'll get into those in a moment. So once you have all of this, it's time to distribute. So I won't go into too much detail here because at this point it's not really Python specific. It's like shipping any other app. But again, this is going to be platform-specific. So it's going to be specific to each OS. Like on a Mac, for example, you can distribute directly. So you can zip up the .app and just offer that. And there's pretty good affordances in the OS to install that. You can also go through the App Store. On Windows, the sort of typical thing is to further bundle everything into an installer using MSI or some other technology like that. There's also the Windows Store, if you're daring. And finally, one quick tip. So automatic updates are really great. We used that very early on. But being able to automatically deploy new updates to your users is really key when you're dealing with bugs and you're dealing with crashes. It's great to not have to rely or to have to make the user download an update and apply it to be able to fix their installation. And there are some great third-party tools like Squirrel or Sparkle that let you do that. And those will work fine once you've packaged your app or packaged all your Python into a native app. So to recap the packaging section, so dependencies become part of your app. That's kind of the first thing we found out really quickly. Second, Python itself should be a part of your app too. Freezers are great because they let you automate this whole thing. And finally, distribution is going to be platform-specific. So while there are some tools that can help you there, once you've turned your Python code into a native app, that's where you have to start looking at each platform a bit separately. So let's talk about monitoring now. So once you have real users using this thing, that does not mean the job is done. In fact, some of the most interesting work happens once you have something out there. So we'll start with something a bit obvious, logging. So logging is pretty important. It's really the key to understanding how your app's actually behaving in the wild. The logging module is the obvious choice here, but there are many alternatives. Now apps, one thing to remember though is that when you're actually dealing with an end user application, the apps don't generally have standard streams. So standard out or standard error aren't generally readily available. In fact, on Windows, if you try to print to those for too long, you'll eventually crash because you'll run out of buffer space. So in this case, the thing that we suggest is to redirect those to a file somewhere that you can conveniently scoop up as part of like a bug reporter or something. Some platforms also have specialized logging. So for example, on Windows, there's an output debug string function that lets you route these to a specialized debugger or the debugger inside Visual Studio. On Mac, there are APIs you can use to target the console. But in fact, many freezers will actually do this streamer direction for you, but this is something to keep in mind. So also important is crashing. So to an end user, a crash is not just a frustrating experience. It is also bad for your brand and negatively affects how people perceive your app. Now a fun misconception is that because you're using Python, that means you can't crash. That is definitely not true. Things like exceptions that bubble up all the way to the top of the thread, for example, can cause a crash. There are also native crashes, so you can actually have low-level crashes in inside Python itself or inside extensions. A common cause of these can be things like bugs and bindings, which is something we discussed earlier. Now in our case, not running is bad because it means your files aren't syncing. And also with millions of installs, there's no such thing as a rare bug. So really, you should try not to rely on users' reporting errors to you. If you can set up automatic crash reporting, that's really better. Now, one of the best and simplest solutions for this is Fault Handler. Now this is now a part of the standard library, which is great. And what that lets you do is catch low-level signals. What we did for a long time is we basically would intercept any kind of crashing signal, we would collect as much information as we could, and we would upload a tiny report to our server saying, we crashed, here's what we could find out, and then we would use that to debug. Now over time, Fault Handler wasn't quite enough. It doesn't catch everything, especially on Windows because signals are kind of more of a Unix thing. And it turns out you can't really rely on a process that's currently dying to really report reliably. So what we actually did is we moved to Crashpad, which is something that's a Google Project that runs separately. So it's a little helper app that runs alongside yours, and that's really much more reliable. But there's a whole bunch of solutions out there, things like PL Crash Reporter, for example, or there's also complete turnkey solutions, like Sentry, that will do all of this basically for you, including the reporting. Actually, if you're curious about how we did Crashpad, there's something on our blog from a colleague of mine. There's a really good blog post about that goes into how we symbolicate Python stack traces. So what's weird in our case is that we have a lot of Python code and then code from other languages. It's common to have a crashing stack trace that has a lot of PyVal exec frame EX, I believe, which doesn't tell you a lot about what's happening. And what we actually did is we built a system that lets us symbolicate Python stack frames inline so you can actually see the entire stack trace, including the Python function calls. So this is really cool, and if you're curious about that, check out the Dropbox tech blog. So earlier we talked about how building a UI means entering the world of threading, so let's get back to that for a moment. So now that you're building an app, that means you're going to be front and center, so your users are going to be interacting with that thing, and that means responsiveness is very important. So your users might not all be running the sort of monster dev machine that you have, and so you should really strive to make your app run as fast as possible on any device. So let's go over a few tips, and these are things that we've learned over the years. So first, don't block the main thread, and you may have heard this one before, but as a reminder, the main thread is there to respond to the user's interactions with your app. So once it starts executing those Python callbacks, it needs to be able to respond and get back to doing that as soon as possible. So to maximize that, try not to do any kind of sleep or costly work in the main thread itself. Delegate basically anything that can take a while to a background thread or to somewhere else. What's nice is that, like I said, most UI libraries actually include ways for you to do this really easily. Second, try to avoid noisy callbacks. So I mentioned that apps are callback driven. Now it's really tempting to register a lot of callbacks to give the user a lot of feedback. Now try to limit this though, because, or better yet, use APIs that don't require noisy callbacks. And so you might want to ask yourself, like how often you truly need to respond to user interaction? A sort of extreme example is, do you really need to know every time the user moves the mouse by a single pixel? Probably not. And if you remove those, your app will tend to be less sluggish. Tip number three, so try to move CPU bound work outside of Python altogether. So this one's really interesting, because CPU intensive work is not kind of known to be very fast in Python. That's why things like Hashlib, for example, actually implement the costly work in C. And the nice thing about this is, if you use another language, it'll be a bit faster. But more importantly, you can actually drop the GIL or the global interpreter lock. This is nice, because this will actually prevent or this will actually allow you to paralyze and use more of the cores on the device. Now if you're not familiar with the global lock, it is a sort of feature of CPython that means that Python code from multiple threads can't actually execute in parallel, in or at least truly in parallel. But by actually taking CPU bound work and putting it in C or in another language and dropping the GIL, you can actually start to use multiple threads. And so if you actually look at the activity monitor for Drawbox, you'll notice that if you're syncing heavily, we don't use 100% CPUs, more like 400 or 500% or something, which is another problem, but we are using all of your cores, which is good. And tip four, use Python 3. I think Guida will like me for this one. So the main thread is callback-driven, right? And it's written in Python, so your callbacks are in Python. And Python has a global interpreter lock, so it's subject to a lock. You might be thinking, well, Max, didn't you say we shouldn't block on the main thread? And grabbing a lock is one such thing that could block the main thread? Well, yes. Theoretically, Python by its nature means that the main thread might be blocked on waiting to acquire the GIL itself. And this is actually particularly bad in Python 2, because Python 3's GIL was rewritten and is much better. But it usually starts showing up when you have a lot of threads. Now, this is unlikely to be a problem for you, but we have a lot of threads now, and so we kind of hid that problem relatively quickly. It's still actually technically possible in Python 3, but very, very unlikely. But the way we fix that is we have a Python fork of CPython, so we actually patch Python's GIL to always prefer the main thread if it's ever waiting. And so that kind of solves that problem. Okay, so let's recap monitoring. Shipping is just the beginning. Logging and crash reporting are great because they help you build confidence. And finally, responsiveness is really important, and so you should invest in it. Okay, now let's get to the controversial part. So over time, your needs may start to change. So in many areas, we actually kind of, we feel like we reach the limits of Python. We either chose to use another programming language for some tasks, or we were made to. In some cases, the platforms are very stringent and require you to do certain things using native code. And that can lead to some pretty interesting changes that we made that I'll quickly touch upon. So for example, I just mentioned using C for performance reasons. The nice part here is that you can actually use bindings here too. So the only difference is that rather than target third-party code, you're targeting your own code. So for example, a Dropbox, we're now increasingly using Rust for some of our components. And there's a whole bunch of reasons why I really like it, but the sort of type safety that comes from that language is relatively popular. But don't underestimate how this type of change and how this can add up. So because Dropbox is a Python package, so at first, we relied on setup tools as our build system. So we basically have a setup.py. And this is also how freezer scripts actually work. But the more non-Python code you introduce, the harder this is going to get. You'll eventually ask yourself the question, like, should everything be a Python extension? Can anything be a Python extension? And the answer is no. Actually, there are some tools that very cleverly hack around this, like Milksnake, which does this for Rust, actually. But in general, this didn't really work out for us. So in our case, this wasn't quite enough. We developed so many non-Python platform integrations, like app extensions or kernel extensions on Mac, that freezer tools, as a reminder, can only package Python code and extensions. So it became really, really hard to package the rest of this stuff with the app. And this added up so much, and the platforms became so constraining. If you do Mac development, you'll see that Apple is quite a bit restrictive these days about what you can do. We decided to flip that on its head. So we actually stopped using freezers, and we moved to embedding instead. Now, what does this mean? So in freezing, remember, you take a script and you turn it into an app. In embedding, you already have an app, but you use the Python CAPI to run Python code. Really, the only main difference this made was that we could stop using setup tools as our build system. We now have another custom build system. Really, this reflects the plurality of languages now. Now, in our code base, Python is no longer alone, and it's no longer the dominant programming language, even though it still drives the core of our desktop app. This is definitely unique to Dropbox. One last thing to talk about, web technologies. So you may have heard of Electron. It's relatively popular out there. One of the issues that we've noticed is that, well, every platform is different, and that can lead to code duplication. We made the choice back in the day to have a sort of Mac UI and a Windows UI, and so that meant we had to write it twice. Eventually, because we wanted to make more complex UI, in fact, we recently announced a new UI that's pretty, it's like a full window, it's a bit more complicated, you might not be willing to pay the price of writing it twice. It might be too much, and meanwhile, HTML and JavaScript have become very popular and very good at this, actually. Electron's popularity is generally well-deserved. The developer experience is really good. So we're building new complex UI. The requirements changed, so we decided to actually use HTML and JavaScript for that new Dropbox UI. And the reason for that was that this thing was going to be complex enough, and HTML was really well-suited for rich dynamic UIs, especially with React and things like that, that this really was going to simplify our lives. Now, the good news is, we don't actually use Electron. Now, the good news is that because we use Qt, that actually comes with a very good HTML renderer, Q Web Engine. On Mac, we actually considered using the system's web views, WK WebView, but in the end, this had some problems, so we're actually using the Chromium Embedded Framework there. But basically, the way this works is that the view and view models, or the view and controller layers, if you're familiar with MVC, are written in TypeScript, but then the model and most of the business logic remains in Python. And what we did is we kind of made a custom bridge between the model and the view model. So we have a bridge between TypeScript and Python, that is also MyPy enabled, by the way, that lets us kind of straddle the two worlds like that. Okay, so let's recap the beyond Python section. So Python may not always be the best tool for the job. You might be forced to use another tool for the job. Freezers are going to be limited to shipping Python code, and you can enhance them, but that has limits. Embedding is great because it gives you complete control over your app. And finally, your requirements will change, so you have to be ready to adapt when that happens. Now, I have a minute, and so there's a bonus round of a few interesting things that I thought I'd mention really quickly. So first, we now have a very large team working on the desktop app. When I started, it was much smaller. We are now, I think, more. I don't actually know the exact number, but it's a lot. Maintaining a code base like that with a large team can get pretty challenging, and so here's some of the things we did to make that just easier. So first, we have something we call a commit queue. So rather than just accepting commits directly, all commits are fully CI tested before landings, pretty much like pull requests. And that means that you can't land anything that actually breaks the build. This has been a game changer for a long time. A game changer for us. Second, we actually support debugging directly using PyDEVD, so in fact, a lot of people use PyCharm and VS Code internally, and you can debug the app directly from that. It's actually possible to debug an app even though Python is packaged in a .app, for example. We use MyPy for static typing everywhere. That is huge when you have a large code base. We're now at 60% plus coverage, which is really nice. And finally, we actually built a custom linter using Asteroid to detect patterns of things we don't like, and so we add rules, lint rules over time, on top of things like Flake8, Pepe8, et cetera. So these are some of the things we did to grow the team. Let's talk about Python 3, because I mentioned Python 2 earlier. We initially were a Python 5, then a 2.7 app for the longest time. The move to embedding rather than freezing actually is what helped us to migrate to Python 3. And the way we did that is we eventually moved to a hybrid syntax where we had both Python 2 and Python 3 supported, and we would selectively choose whether to run in Python 3 or in Python 2 at runtime. And the only reason we were able to do that was because if you control using the Python C API, you can actually link against both Python 2 and 3. And so we actually, for a time, we actually shipped both Python 2 and 3 in our app, and then we would gradually migrate people over kind of one by one. And we're now running Python 3.7, and we're very happy. So with that, I think I have a few minutes left, hopefully. And so I think we'll take a few questions, but I hope I didn't ramble on too much. Thank you very much for coming. This was really fun. And yeah, thank you. Yeah, thank you very much. There was time about two minutes time left for questions. So are there any questions? Yes, please. Question about Qt. I know a very, very large code base. So how did you make it first that it's low footprint, as you said in the slide? And second, did you audit all of Qt's code? Yes, the first question was, how do we kind of limit Qt's footprints? We actually altered Qt's builds to only ship very specific components that we needed. And so we found out that most of the libraries in Qt, we didn't actually need. And so we actually stripped out, I'd say like almost half of the Qt libraries themselves. So that we do ship web engine, which is quite large. I think the second question was, did we audit all of Qt's code? I don't know if I can answer that question. I think we did do a complete review and we do effectively trust Qt as a library. But yes, it is a lot of code, but yeah, we did look at it. Okay, now the question. Since there's a Win32 package on Mac, maybe it would be similar to on Mac. So it's Mac kind of, similar way like. Yeah, I think I couldn't, I said like, I think the mic was dropping out, so I didn't really hear most of your question. Oh, interesting. So something, just to make sure I understand the question, like did we see a third-party library similar to Pi Win32 for Mac? Yeah, so I don't think we have any plans to release our work there now, although we did actually upstream many of our fixes to CPython over the years. And so we did contribute those back to the community. Honestly, the best tool for Mac is PyObshi. The bindings to be able to call the objective C APIs on Mac OS are really the most of what you need, because Apple's APIs are generally very, very rich. And so the binding work there is generally quite good. And Mac as an OS is a very uniform API, so you don't have like the multiple surface areas the Windows has. So on Mac, I would definitely recommend PyObshi. Yeah. Okay, thank you very much for your time. Thank you. Let's have another round of applause for Macs.