 Thank you very much, Martin. Hi, I'm super excited to be here because this topic and this conference has a special place in my heart. Writing faster Python was my first talk that I ever did at the conference and I presented it in Europe Python in Bilbao in 2016. But many things has changed since that time, so I decided it's time to update that talk. Python 2 is no longer a thing. We have like new interpreters popping up. We also have some very interesting initiatives aiming at speeding up the C Python. But one thing has not changed. Whenever there is an internet discussion and two words appear together, Python and performance, there is always someone saying don't use Python. Python is slow. Python is not slow. Python is slower than some other languages. And quite often that's fine. I mean, Python was not optimized for the speed. Python was optimized for the development speed. And comparing the number of people writing code in assembly to the number of Python programmers, I think it was a good decision. But why is Python slower? Well, one of the reasons that makes Python slower than some other languages is also the reason why it's so easy to use Python. Python is a dynamic language. And that combined with the weekly typing means that we can use the famous duck typing and we don't have to worry too much about the types of our variables. So nothing stops you from randomly reassigning variables throughout your code. Okay, technically your sanity should stop you from doing that. But I mean, you can assign a variable to a string, then reassign it to an integer, to a list, to a pandas data frame, and so on. And the interpreter has no way to know in advance what's the type of a given variable. So it can't do any of those optimizations that the statically typed languages can do. For him, your variables are always a mystery. Sure, you declared a string, but was it going to be 500 lines of code later? Still a string? You don't know. So the thrill of being a C Python interpreter must be crazy. I don't have time to go into more details why Python is slower. But if you're interested, there is this really nice talk by Anthony Shaw given at PyCon a few years ago, so we can find it on YouTube. So what can we do with the fact that Python is slower than other languages? Well, I probably don't have to tell that in a room full of developers, but I work in various companies and I'm still surprised how many of them will pay a ridiculous amount of money to the developer, only to make them work on some old laptop that was just lying around. I mean, look. If this is your computer, then I'm sorry to break it to you, but Python is not your bottleneck. And even if you're hopefully not running your Python programs on a potato, if you need to run some large computations, just rent a server in a cloud. I did a quick check and for 54 bucks per hour, you get a VM with 6 terabytes of memory. And even if you don't need such a ludicrous amount of memory and, let's say, just 128 gigabytes is fine, that's like $1 per hour. And we are still looking at a quite expensive cloud computing provider and on-demand pricing, so we can find much better deals. So instead of wasting a whole day just to implement some crazy algorithm to process your data in chunks because they don't fit into your memory, just spend a few minutes. And run your computations on a computer that can fit all the data into memory. You can also use a different interpreter. When I say Python, I actually mean CPython implementation, but there are other implementations out there. And I know this is an advice that is, everyone gives this advice, but very few people takes it. But the ecosystem of Python interpreters is constantly changing. PyPy is now much more compatible with CPython code than it was six years ago when I was talking about writing faster Python for the first time. If you have some code that does a lot of math in loops, then PyPy will probably run your code a few times faster. And even though it might not improve the speed of your C extensions, at least it can run them, which was a major concern back in the past. And there are, of course, other interpreters. There is PyStone developed by Anaconda, there is Cinder used by Instagram, there is PyGon, Gralpython and so on. One thing that makes Python so great and so popular in the data science community is that Python is a perfect glue language. It means that you can use Python to orchestrate some libraries that were written in a much more performant languages. So you can use NumPy, which has very efficient data structures and scientific functions. You can, for example, use also Numba that has a just-in-time compiler that you can apply with a simple decorator. And again, you will get some really nice speed-ups on math operations and loops. Yet another way to squeeze those few percent of speed-up with almost no cost is to update your Python version. With each new release, various parts of code gets improved. So if you suddenly jump from Python 3.6 to 3.11, it might turn out that your code is much faster right now. And even though switching between, let's say, Python 3.9 and 3.10 is not as problematic as moving from Python 2 to Python 3, you still need to have a good test suit to make sure that everything works fine. But if you do have a good test suit, then upgrading your Python version is basically a free speed-up. And finally, you can write faster Python by using better algorithms and data structures. And that's an advice that applies pretty much to any programming language. Let me show you an example. So that's my magic stickworks. Malcolm. Yes, I get it here. So let's say I want to compute the sum of powers of the first one million numbers. So I can define a total variable and then run a function that will just go through all the numbers and add their powers to the total variable. And then, at the end, I just print this total variable. Now, I want to check how long it takes to execute this function. I can use Ipython and two of its magic commands. I can use run to run a script and then time to measure the execution time of the previous command. And this method of measurement is far from perfect. And in the later examples, I will use something much more reliable. But this is a simple example. I just want to have the most simple way to give me some number. So please bear with me. And if you have no idea what Ipython is, it's a much better Python interactive shell. I gave a talk about it three years ago. So if you're interested, you can find it on YouTube. But back to our example. So I'm running my code and I can see that it takes 72 milliseconds. I'm using the wall time, which means that I start the timer. I run the code. I stop the timer. And I return the difference between start and stop. Nothing too fancy, but it gives us a number. Obviously, there is a lot to improve here. So let's start small by moving the total variable inside of our function. So we got rid of the global variable and we are down from 72 milliseconds to 63. That's like a decent 15% of speed up by simply eliminating the need to modify the global variable. Global variables not only make your code harder to read and reason about because you have to search where they are defined but also what other code modifies them. They also make your code slower because looking up global variables takes time. But we can do even better. Let's use the build in some function to actually add the numbers. To make this work, we will have to switch from a for loop to a list comprehension. Cool. Our code got much simpler and we are down to 59 milliseconds. So another few percent of speed up improvement. We can do one more improvement at this point. We can replace the list comprehension with a generator expression. And to do that, we just have to remove the square brackets. And now our code is slower. Is it bad? Well, that depends. Optimization is often a trade-off between the speed and memory usage. Generator expression has disadvantage over the list comprehension that it uses much less memory because it does the computation when it's needed. List comprehension will generate you the whole list at once and it will use much more memory. So for a very large list, you might actually want to use a code that is a bit slower but it won't eat up all your RAM. We can actually check this with IPython and the memory profiler. So here you can see that the list comprehension version increased the memory usage by 50 megabytes and the generator expression had almost no impact on the memory usage. So as I said, optimization is often a trade-off between the speed and the memory usage. Alternatively, if we can't turn our for loop into a nice list comprehension combined with a built-in function, there is another trick that we can use. There is a library called Numba that is basically a JIT compiler packed as a Python package. So we can install it with pip install Numba and we can just add this small decorator here and as you can see, we just cut the execution time by half with literally two lines of no-brainer's code. And of course, Numba can only improve some specific code, like loops and map operations. If we try to use it on some other code, it might not improve it, but I mean, it's very easy to use it, so it's a nice tool to test. But we can do even better. Let's go back to the version with the list comprehension. So Python lists are heterogeneous. It means that they can store different types of values together. But in our example, we are operating on integers and there is a library that is much faster for working with numbers. As probably most of you can guess, it's NumPy. So NumPy is packed with functions and data structures optimized for working with numbers, so we can take advantage of that. And we can create an array of integers and then we can call the power and sum functions from the NumPy library. And if we try to run it, it will improve the execution time by two more milliseconds compared to the list comprehension. Okay, that's not much. I mean, it's almost like using NumPy doesn't make any sense. Well, it turns out that the problem is not with NumPy. The problem is with my poor simple benchmarking methodology. So when I run the benchmark the second time, it now only takes 10 milliseconds to execute this code. That's because my benchmarks are also measuring the execution time of the import statement and that's when I run it the second time, the import statement is much faster. That's why I said that my benchmarking method I'm using here is pretty terrible. We will switch to something better in a moment. But just to sum up my example, as you can see, we got down from 72 milliseconds to around 10. That's a pretty decent improvement, but the more important thing is that we learn some things on the way. We replace a global variable with a local one, which not only makes your code faster, but also easier to reason about. We switched to using the built-in function instead of reinventing the wheel. You can find a lot of optimized functions in the Python standard library, especially in modules like ether tools or collections. We used a list comprehension instead of a for loop, which is a much more idiomatic way for creating a list. And if you're more concerned about the memory usage of your applications than the speed, you can use the generator expression instead. Finally, we reached out for NumPy, which gives you Python bindings for functions and data structures implemented in C or Fortran. So you can keep writing your code in Python and at the same time benefit from those very fast languages running your code. And we also saw how we can use a JIT compiler like Numba, where with just two lines of code, we could get a really good speed improvement on a specific function. So what we just saw was the source code optimization. That is, writing your programs in a way that will make them run faster, hopefully without sacrificing the readability. So in the rest of this talk, I want to show you some more examples of two or more ways to achieve the same results for some common operations. And then I will run some benchmarks and try to explain you why one way might be better than the other. You can find all the code examples at this URL. I will show this URL at the end, so don't worry. And here is the better setup that I used for measuring my code examples. I used Python 3.10.4, which was the latest stable version of Python at the time of making those slides. I set the Python don't write byte code environment variable, so I don't accidentally cache any byte code between my benchmarks. And for the actual execution, I use a fairly standard and simple way of running timeit in the terminal. So here is how it works. The green part tells Python to run the timeit module, which is a built-in module for measuring the execution time of some code. Then the red part tells, run me this code but don't include it in the benchmarks. This will solve the problem I had in the previous examples where I was importing the NumPy module and the import statement actually took most of the benchmarks. And finally, in the yellow, I will have the function that I want to benchmark. And not that it matters much, but the benchmarks has been run on this laptop, which is MacBook Pro from 2021, 16 gigs of RAM, 10 CPU cores, 16 GPU cores. But it doesn't really matter which laptop I'm using. Even with the same setup, you will probably get a different number, and that's fine. I mean, exact numbers don't matter. If I rerun the benchmarks a couple of times, sometimes they will be slightly slower, sometimes they will be slightly faster, depends on some CPU spikes from other processes. What won't change is that my slow examples will still remain slow, while my faster examples should hopefully be faster. And this is the important bit. Okay, when you want to do some action and you are not sure if it will work, there are two common patterns how you can approach this in your code. You can explicitly check if what you're trying to do is possible and then do this. So, let's say you're trying to read the content of a file, but you're not sure if the file is there, so you write on if the file is there, you read it, and you return it. But even if the file exists, maybe you don't have permissions to read it, so you add another check. And then the number of checks will keep growing, but you still have no guarantee that some unexpected error won't prevent you from reading this file. So, there is a different approach that you can try. So, in this alternative version, you just do the action, and in case something goes wrong, you wrap your code in a try-except block. So, let's see which one of those approaches is faster. I will use a slightly simpler example. So, let's say I have a base class that might or might not have a specific attribute and that I make a subclass, and in the subclass, I want to access my attribute. But if the attribute was not set in the base class, I will get an exception. So, I can either check if my object full has the attribute hello and before accessing it, or I can try to access this attribute and catch the attribute error in case it was not there. So, in other words, I can ask for permission to access a given attribute, or I can ask for forgiveness after accessing a non-existing attribute. And if we run our benchmarks, we can see that asking for permission is around 15% slower than asking for forgiveness. Slightly slower, but not some crazy amount. But what if our class has more than one attribute and we still need to check all of them since one of them might be missing? So, this time I'm using a class that is supposed to have three attributes and we want to check if all of them exist before we perform some action. So, with more attributes to check, now the difference between both code examples starts to be more prominent. Now asking for permissions is around 80% slower. So, does it mean that asking for forgiveness is always the best choice? Well, let's see what happens if the attribute is actually missing. So, here I commented out one of the attributes that we are trying to access, and when I run my benchmarks one more time, I can see a completely opposite picture of what we saw before. Asking for forgiveness is almost four times as slow as asking for permission. So, the tables have turned because handling exceptions is expensive. So, a good rule of thumb here is to ask yourself, is it more likely that my code will throw an exception or not? If the answer is yes, I will probably get an exception and you can predict what will go wrong then an if statement is a good idea. It will be faster than handling the exception and it will much better show the intention of your code. But if you find yourself putting a ton of if statements, only to cast some exceptions that happens one in a while, and on top of that your code already failed twice because you forgot about some corner case, then maybe it's better to use the ask for forgiveness approach and just wrap your code in a try accept block. Let's see an example of how you can iterate over a collection of items and find the one that matches some criteria. So, let's say I want to find the first number that can be divided by 42 and 43. I could use a naive approach and write a while loop that would start iterating from one, return the value if it's divided by 42 and 43, and otherwise just check the next number. And while there is nothing wrong with the while approach, we are manually incrementing the counter, which feels like a lot of unnecessary code, so we can do better. We can instead use a for loop and a counter that will keep giving us the next number until we find the one we are looking for. And if we measure the execution time of both functions, we can see that the while loop is around 25% slower than the for loop, but more importantly, the for loop is a bit more concise and I like it much more. But what if someone told you that Python is all about list comprehensions, and for loops are bad if you're dealing with lists, you should just use list comprehensions all the time. Okay, let's give it a try. So, here I'm checking the first 10,000 numbers if they are divisible by 42 and 43, and then I just return the first element that matches this criteria. As you can probably guess, using list comprehension is a terrible idea. We are unnecessarily creating a list of 10,000 elements just to grab the first one. It might make sense to use it if we really wanted to check those first 10,000 numbers, but we just need to get the first element that matches. So, what we need here is some kind of a lazy list comprehension. And a lazy list comprehension is basically a generator expression. So, we can write a generator expression that will filter the list of numbers based on our criteria and simply grab the first element using the next function. Generator expressions will do just enough work to give us the first number and then stop. And if we benchmark it, it turns out that it's as fast as the for loop. So, generator expressions are a great alternative to list comprehension if you need to evaluate them lazily. They are fast, concise, and memory efficient. But if you want to check multiple conditions and you could in principle wrap all of them in like a function and then call this function in your generator, but sometimes that's just too much, so I prefer to stick with a plain old for loops. While we are talking about lists, let's take a look at another example. Let's say I have a list of numbers and I want to get only the odd ones. I can write a for loop that will do the job. And for filtering one million of numbers, it takes 33 milliseconds. But since we are filtering something, there is literally a function called filter in the standard library. So, we can use that to simplify our code. And now our code is simpler, but it's also around 50% slower. So, instead of using the filter function, we try to use the list comprehension since we are constructing a list. And the list comprehension is a clear winner. A for loop is around 30% slower and filter is almost twice as slow as the list comprehension. So, in this example, we try to use three different structures. And while the list comprehension is the fastest one and, in my opinion, the most readable one, each of the constructs that we used actually has a valid use case. So, the comprehension are the obvious choice if you want to build a new list. But if you don't need to have the whole list at once, the filter function is actually quite useful because it returns an iterator. So, if we take a look back at the second example, we can see that we are actually calling list to give us a list from whatever filter returns. And it's this list creation that takes a lot of time. If you need an iterator that will generate you the next value only when needed, filter is a great choice. And finally, we have the for loop, which some people might call non-Pytonic, but there is a limited amount of if statements that you can put in a list comprehension before it becomes unreadable. So, for more complex filtering conditions, I just stick with the for loop. Okay, let's cover one more example of list and we will switch gears to something else. How do we check if an element exists in a list? Let's say I have a list of one million numbers and I want to check if a specific element is there. Again, we can do this with a for loop, just iterating over the list of numbers, return through if we find a number we are looking for, otherwise we return false. But we can achieve the same results simply using the in operator. It's much less code and as you can probably guess, it's faster. So, let's run some benchmarks. So, I'm running benchmarks for three scenarios. First one is when the element is located at the beginning of the list, second one when the element is located at the end of the list, and then when the element is not in the list at all. And the results are pretty consistent across the board. We can see that using the for loop is around twice as slow as using the membership testing operator. Okay, that was kind of a boring example. I mean, if you know the in operator, you will probably not write a for loop to check if an element exists in a list. But we are not done yet. We can do better. Instead of using a list, we can use a set. Lookup time in a list has O and time complexity. So, the bigger the list, the longer it takes to execute the in statement. But the average lookup time in a set is constant. And if we run the benchmarks, we can see that something very interesting. So, for an element located at the beginning of the list, the lookup time is only six times as slow as the lookup in a set. I'm saying only six times, because if the element is located at the end of a list, or it's not in the list at all, then the lookup time in a list is over 100,000 times slower. That's impressive. It would be even more impressive if I wasn't cheating. I did not include converting a list to a set in my benchmarks. I only created the set in the setup part of my code, and in the benchmarks, I was already checking the numbers in a set. But if you're starting with a list, you will have to convert it to a set, and that takes time. So, let's update our benchmarks with a proper function that does the conversion from a list to a set first and then checks the membership. And when I compare the time it takes to do a membership testing in a list versus converting a list to a set and then doing the membership testing, I see a completely different picture. Doing the conversion takes twice as long as doing the lookup in a list, and that's a scenario when the element is located at the end of a list or doesn't exist in a list. But if the element is at the beginning of a list, conversion makes our code almost 40,000 times slower. So, to conclude this example, using for loops for performing membership testing is a terrible idea. Using the in operator is usually twice as fast. The average lookup time in a list grows as the list gets bigger, but it's constant in a set. So, if you already have a list, then converting it to a set just to do one lookup doesn't make much sense. But if you have a choice between constructing the list and the set in the first place, if you choose to construct the set, then you will get some really nice speed-up improvements in the membership testing. And one last remark. Set is not a drop-in replacement for a list. For example, set is not ordered. So, don't randomly change your data structures because some guy at the conference told you that it's gonna make your code run 100,000 times faster. Just read about different data structures in Python and the time complexity for the most common operations, and there is this really, really nice wiki page where you can find tables for everything. Highly recommended. Let's talk about creating dictionaries. So, there are two ways how you can create a dictionary. One is to call the dict function, and the other is to use the literal syntax and just write those two squiggly brackets. And in many cases, they are equivalent, so you might not give it much thought and assume that they both take the same amount of time. Actually, if we measure the execution time, it turns out that the dict function is twice as slow as using the brackets. And that kind of surprised me, so I decided to dig a bit deeper to see what's going on. So, I fired up this assembler for Python bytecode, and I looked at what happens when I call each of those functions. So, what this module does, it prints you the bytecode instructions that will be executed. And even though I usually have no idea what most of the instructions there means, the important part is this call function. You see, nothing stops you from doing that in your code. I mean, this is stupid, but Python allows you to be stupid. So, when you call the dict function, Python interpreter has to check that maybe you actually overall these dict functions somewhere. But, if you use the curly brackets, you use a Python statement. So, there is no way for you to override this, and Python can directly call the corresponding bytecode instruction. That's why using the literal syntax for creating dictionaries, lists, or tuples is faster than calling the equivalent functions. And this module is a really nice tool that can show you the bytecode instructions that are executed when you run some code. So, if you're curious how something works under the hood, let's say you want to remove duplicates from a list. Here I have a list of 1 million numbers, each of them between 0 and 99. I can write a for loop that will go through the duplicates list and append the unique numbers to a new list. And then I just return that list. I could also write a list comprehension that does the exact same thing. And if I benchmark both of those approaches, I can see that they take the same amount of time. Except that this is a terrible way to use list comprehension. List comprehension should be used when you are creating a list. But in our case, we want it for the side effect of appending the current number to some external list. And the list comprehension will still create as a list of nones that we immediately discard. So, in this case, it's much better to stick with a for loop to show the intention of our code. But there is a data structure that we already talked about that by definition doesn't contain duplicates. Any guesses? Set. Yes. So, we can take our list of duplicates, convert it to a set, and then convert it back to a list. And doing that is way faster. Using the for loop is over 50 times slower than simply converting our list to a set and back to a list. There is one caveat with this method, though. Sets don't preserve the insertion order. So, what we get from this list to set to list conversion might have a different order than our for loop. So, if you want to keep the initial order, there is this trick with dictionaries. You can create dictionary with keys taken from a list and then convert that dictionary back to a list. This will remove the duplicates and preserve the insertion order. And if we benchmark this, we can see that it's still much faster than the for loop. Now, the for loop takes around 28 times longer than the dictionary trick. There is a small caveat, though. The dictionary trick only works with elements that are hashable because dictionary keys has to be hashable. And hashable means immutable. So, if you have a list of lists or a list of dictionaries, then this method won't work. Okay. As a bonus exercise, I decided to run all my examples through different Python versions. So, I simply wrote a one large shell script that runs all the commands in the terminal. And then I used pyenv to switch to different Python versions. So, I switch to Python 3.7, run my benchmarks, switch to Python 3.8, run my benchmarks, and so on. If you're curious how to use pyenv, I have a blog post on that. And here we have the results. So, on the left side, we can see the functions that I was checking. And on the top, we can see Python versions. And in the last column, we can see how much slower was Python 3.7 versus 3.11 latest beta release. That's a lot of numbers. So, I put the most significant ones in bold. As you can see, Python 3.11 is a clear winner in terms of performance improvements for most of those small code snippets. But even for older versions, performance usually improves as you upgrade your Python version with some occasional minor degradations here and there. So, what happened in Python 3.11? Well, in 2020, Mark Shannon, one of the core CPython developers, proposed that he can make CPython faster by a factor of five. It would take four stages, with first two being general tweaks and improvements, and the last two would implement some sort of a simple JIT compiler. And now Mark is working at Microsoft with Guido and some other smart people implementing those ideas. And the speed improvements in Python 3.11 are the first fruits of their labors. Actually, Mark is giving a talk in this room right after mine, so I'm really looking forward to hear about the progress of this project. It sounds very interesting. And here we have the rest of my benchmarks. And you can find all the results in the benchmarks results folder in the repository for the stock. And I wish I could show you more examples because I had a lot of fun gathering them, but I am running out of time. So, whatever didn't make it to this presentation, I put it in the repository for the stock. You can find some examples there with benchmarks. I also left some examples where the alternative version which I was expecting it to be faster, but it wasn't, and I also put some comments why it's not faster. And this whole idea for this presentation started as a list of articles on my blog. So, if you want to get more details about the examples I showed you, you can find them on my blog. I have around ten articles, and I didn't have time to update my blog in two years, but I have plenty of more ideas, so I promise you when I get back to blogging there will be more examples. Okay, we still have like five minutes. Let's jump to conclusions. Source code optimization is often the last thing you think about when you have performance issues. And that makes sense. I mean, you don't start optimizing your code by rewriting it from a scratch unless you have a terrible code to start with. But source code optimization is not something that you should be doing when you have performance issues. Rather, it should be something that you should keep in the back of your head when you write your code in the first place. Being curious about which code structures are faster will lead you to understanding Python better. Understanding which data structure is better in a specific scenario, and sometimes even understanding what is happening under the hood of your code. Replacing a loop with a list comprehension when you are writing the code is an easy thing to do. And even though it might increase the speed of your code by, let's say, 20%, if you replace a few loops here, you change the data structure here, you use the building function there, suddenly you will realize that your code is now twice as fast. And last but not least, please don't sacrifice the readability of your code for some small performance gains. Sure, fast code is good, fast code that is hard to read is not good. Thank you so much for listening, you can find all the links here. Thank you very much for that great talk, Sebastian. We do have a few more minutes time for questions from the audience, so if somebody would like to ask a question, please step up to the microphone. Hey, thanks for the talk. I have one comment about the example with permissions. So there was this, like, if condition, whether we have access to a file. If we can come back to this. Yep. Bit of a clicking, but... Come on. Yeah. That wasn't a very serious example that was just to show you. Yeah, so I would say the main problem here is that the top example, I mean, the one with if condition is actually broken, I mean, insecure, because you have a race condition between those, like, OS path exists, OS access and open. I mean, between opening a file and trying, and checking its access, the file can, like, reappear, be deleted or anything else. So I would say that's the main issue here, and that's why you should always use just trying to do something instead of, like, checking the access. That's a very good point. I mean, even around this code, this is just, like... But it makes... But it makes a lot of sense to, like, just... with the other example with, like, checking the attributes. But then, with checking attributes, there is another issue that usually your code is more complex, and you may have this, like, except being triggered in a situation where you didn't expect it, and then it's hard to debug. Great. Is there an actual question, or are we going to... Thank you very much. Any questions from the Internet? So could we please have the remote question? First of all, congratulations on your talk. It was awesome. Second, my question. The first time you mentioned the difference between loop and list comprehension. I'm not sure which slide was it. Could you tell a little bit if you know why it's slower about the interns working, because, as I mentioned in the end, usually more beginners at Python tend to go for for loop instead of list comprehension. So I was thinking why there is this difference in time. Do you have any idea? Frankly, no. I wish there was, like, a C Python developer who could answer this question. I guess the Python... Okay, there it is. Sorry. I think so. Yeah, could you repeat? I couldn't get it on the mic. So the main difference is function calls. Like, if you have a list object and you are calling list.append, this append needs to be looked up in the object. And it can actually be replaced, because, well, C Python doesn't prevent you from doing so. And that's the main reason why list comprehension are much faster than using, like, a for loop and appending to a list. Yeah, because you can always, like, inline your functions to make them faster. So I would assume that lookup takes quite some time. Yeah. Okay, great. Thanks. Thank you very much for the remote question, and we can have one more question in the auditorium, please. Hi, thank you for the talk. You mentioned Numba in the beginning and you throw this JIT decorator. There's also the Numba and JIT or no Python mode for Numba, which maybe in this case, and also when working with Numba specifically, it bypasses the jail, which also, like, leads to much higher speed-ups than you show now. So could you also run this example with no Python mode? Not this specific example. No, I just wanted to mention that there is this library which I find super cool because the decorator in the world... But you have a good point. There is JIT and NJIT, so depending on your use case, one might be faster than the other. Thank you. Okay, we can still take questions, so please. In Python 2, there was always the example of when you add up many strings, you could do it with a pen or you could create all the strings and do a join. And everybody used that as an example of an optimization. Does it still apply? Good point. I was looking at the string concatenation and then I saw that there are, like, many ways to do this because now we have the f-strings so we can use template strings, so I decided this is way too complicated so I didn't pursue this idea but I would be curious to check it as well. Okay, thanks. Okay, thank you very much for all these questions. And thanks again, Sebastian, for delivering this great talk. So let's have another round of applause for Sebastian, please.