What are Python __future__ imports?

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Hello, everyone! I'm James Murphy. And welcome to mCoding, where we try to get a little better at programming each episode so that we can ultimately  determine whether or not we're in the Matrix. In today's episode, what are Python's `future` imports. While they look like normal import statements, they're anything but. This video is sponsored by me. Thank you to me for sponsoring myself. Did you know that I'm available for Python and C++ consulting, contracting, training, and interview prep services? Again, let's give a round of applause to me for sponsoring myself. This is the future module. It's a real module. But it doesn't really do anything. It defines some constants and has a class that represents some metadata. And then it just has a bunch of metadata about the future features. But, beyond just keeping track of this metadata, the code in this file doesn't implement any of those features. What actually happens is Python looks for these future imports whenever it compiles a module. and then sets a specific feature flag depending  on which future imports you used. For instance, if we from future import `annotations`, then the `CO_FUTURE_ANNOTATIONS` flag will be set in the compiler. Let's take a look at the C code  that this actually affects during compilation. I know compared to Python, this might look very verbose. But I promise you don't need to know any C in order to understand this. This is the function that gets called to convert an annotation in an argument into bytecode. The important part is right here. We have a set of features represented by a bunch of bit flags, And we do a bitwise AND with the feature flag that we're interested in. In this case, `CO_FUTURE_ANNOTATIONS`. If the feature flag is set, we use this visit macro, which just expands to calling this function. The bytecode operation this corresponds to is then loading a new constant. The constant that we load is the annotation expression as Unicode, i.e., treat the annotation as a string. Let's see that in action. Here, we have a `Node` class with an annotation for its `data` member. We can access the annotations at runtime like this. Notice that what gets printed out is that the annotation has the string 'int' in it. But if we comment out the future import, then we see the annotation contains the actual `int` class object. So, a future import is doing something really fundamental. It's completely changing the way that Python interprets your source code. It can literally cause Python to generate different bytecode. And therefore completely change the meaning of your program. So, what are all of these future imports? And why are they used instead of just telling a user to upgrade to the next version? After all, if I have a new feature like the match statement from Python 3.10, it's easy to tell people, "If you want to use the match statement, you need to upgrade to 3.10." But if you were already using annotations in a previous version, upgrading and changing the behavior can break old code. We don't want to go around just breaking people's old code for no reason. We need a really good reason if  we're going to do something like that. This is where Python might differ from some languages like C and C++. In C++, backwards compatibility is a much stronger priority than in Python. A broken, incomplete, or not well-thought-out feature might very well stay in the language forever. In Python, you get about three to five years to upgrade. And the way that you opt into  an upgrade early is by using a future import. So, why do we need a future import for annotations? What's wrong with just sticking the class object in there? Well, if an annotation is just treated like a normal expression, that implies that the annotation should be defined before the thing that it's annotating. This makes it impossible to define a recursive data structure like a linked list. When I try to annotate the `next` pointer in the linked list, I'm in the process of defining the `Node` class. So the `Node` class is not defined yet, and hence I get a name error. The annoying solution to this is to put quotes around your annotations. And then hope that your editor and your type checker understand. As we already saw, the future solution to this is to just automatically always treat annotations as strings. Very few programs actually care to access type hints at runtime. But those that do can use `typing.get_type_hints`  to evaluate the strings back into class objects or whatever. Eventually, the behavior of the future  import will just become the default behavior. But do keep in mind that with future imports, the behavior is not always completely settled.   When you opt in early to a feature, it's possible that that will change before the feature actually comes out. In fact, with annotations specifically, there's a decent chance this behavior will just be replaced by some kind of lazy computing of the annotations. That's definitely something to watch out for. But I don't really worry about it. And in fact, I use this particular future import in almost every file that I define. So, what are all the rest of the future imports? When in doubt, of course, go to the source. The good news, since these are breaking changes after all, is that there are only two that even apply to Python 3. This one is just an easter egg. As you can see, it's set to appear in Python 4. All the rest are in Python 2. `unicode_literals`: allowed you to create bytes literals like this using the `b` and then quotes. `print_function`: introduced the `print` function. `with_statement`: introduced the `with` statement. `absolute_import`: changed and clarified the way that absolute and relative imports work. `division`: made the single slash always mean float division and introduced the double slash to mean int division. `generators`: introduced generators. And `nested_scopes`: allowed the ability to define nested scopes And guaranteed that they would be computed at compile time. In particular, this allowed for lambdas or inner functions to access variables that were defined inside  another enclosing function scope. And that's all the future imports except for one, which is `generator_stop`. `generator_stop` was introduced because of the following behavior. We want the generator to yield one, two, three, four. Pretend one, two, three, and four are  just stand-ins for some complex operations. Maybe two and three are particularly complicated to compute. So we factor it out into a subgenerator. But we made a mistake in our subgenerator. We're only yielding one thing instead of two things before we're done. However, we have two unguarded calls to `next`. The first call works as normal and returns two. Then, for the second call, we hit  the `return`, which raises a `StopIteration`. So what's effectively happening is a `StopIteration`  is getting raised at this point in the generator. We're not inside a loop. We made a call to `next`, and we got an exception, which is propagating up. But nothing's shown to the user. That's because we're in a generator, and that `StopIteration` gets raised all the way up to this `for` loop. Normally, when a `for` loop receives a `StopIteration`, that means we're just done iterating. So, the program prints out one, two, and then stops. Instead of raising the exception and showing the user an error message. We silently loop over less data than we intended to. So, I really don't want `StopIteration` in  generators to cause `for` loops to just terminate. If I wanted a `for` loop to just terminate, I can always just return from the function, which will end up raising a`StopIteration` in the `for` loop. But having an unhandled `StopIteration` propagate outside a generator is almost always a bug. So, `from future import generator_stop`, and now any `StopIteration` that's raised out of a generator gets turned into a runtime error. No more silent errors - we get a big, beautiful  error message telling us exactly where things went wrong. The future import allowed us, starting  in Python 3.5, to opt into this new behavior. But hopefully, you or your company are now using a version of Python that's at least 3.7.   It's fine to leave the future import in.  It doesn't hurt anything. But as of 3.7, you can delete it, and that's now the default behavior. So realistically, the only one that you really need to worry about is `from future import annotations`.   And a quick tip that you might not have realized: Since annotations are now just strings, I can actually start using typing features  from way later versions. For instance, this bar notation for the union of types was introduced in Python 3.10. But it's working just fine in 3.7. This works completely fine as long as you're not trying to actually evaluate those annotations at runtime. If you're doing that, then you just need to upgrade. Actually, if you dig into the C source code, there's one more future import you might be interested in. Anyway, that's all I've got. And finally, I want to give a huge shoutout to Neil Rashania. Thank you to Neil for subscribing at the factorial level on Patreon. I really appreciate the support. Of course, thank you to the rest of my patrons as well. Don't forget to subscribe, leave a comment. And as always, slap that like button an odd number of times. See you next time!
Info
Channel: mCoding
Views: 44,327
Rating: undefined out of 5
Keywords: python
Id: 7CRybttp0Uc
Channel Id: undefined
Length: 8min 18sec (498 seconds)
Published: Mon Jul 18 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.