1 00:00:12,083 --> 00:00:18,880 >> Hello. Welcome back to Curlyboi Theatre,  everyone. Our next speaker, Joachim Jablon,   2 00:00:19,680 --> 00:00:25,120 is joining us all the way from France, so almost  the other side of the world from where I am.   3 00:00:28,800 --> 00:00:34,000 Where it is very early in the morning for  him. So, thank very much for joining us.  4 00:00:36,400 --> 00:00:42,080 Joachim is a French Pythonista soft engineer  and started engaging with our dear Curlyboi.   5 00:00:46,080 --> 00:00:52,640 He's contributed to bits of the Python Index.  He has four cats he was telling us before.   6 00:00:55,600 --> 00:00:57,800 What's your cat's name? >> [Indiscernible].  7 00:00:57,800 --> 00:00:59,000 [Laughter]. >>   8 00:01:01,920 --> 00:01:07,760 He has also spoken at and/or organized  multiple Python and Django events.   9 00:01:09,920 --> 00:01:15,360 They live in Poitiers, with the top four  cutest cats in the world according to us,   10 00:01:15,360 --> 00:01:23,760 and yes, we do want to see pictures. Okay. So, Joachim's talk is prerecorded, everyone,   11 00:01:23,760 --> 00:01:30,400 so he will be in the chat, answering questions  live in Venueless. So, don't worry about the   12 00:01:30,400 --> 00:01:36,160 questions tab this time. Just talk away and  Joachim will be there with you. Is there   13 00:01:36,160 --> 00:01:43,600 anything you'd like to say before we start? JOACHIM JABLON: I'm good. I'm good. We can go.  14 00:01:52,640 --> 00:01:59,040 From Hat Import Rabbit: Hacking Your Way  Through Python's Import Machinery for   15 00:01:59,040 --> 00:02:02,560 Fun and Profit. Hacking your way through  import machinery for fun and for profit.  16 00:02:02,560 --> 00:02:09,760 Hi, I'm Joachim and I'd like to perform a magic  trick. I have to admit that I'm far too clumsy   17 00:02:09,760 --> 00:02:18,560 to be a real illusionist and my favorite is to  explain how it works so I'm not a great magician.   18 00:02:19,440 --> 00:02:25,520 I've been a member of the Python Packaging  Authority and the Python Software Foundation   19 00:02:25,520 --> 00:02:38,240 and Django Software Foundation since 2021. Let's dive into it. Let me take you on a   20 00:02:38,240 --> 00:02:44,720 short hike, where I tell you a trick that I had  to perform last year. By the way, we'll be taking   21 00:02:44,720 --> 00:02:53,760 the scenic route, so kick back, relax and enjoy  the ride. This magic trick begins with a dove   22 00:02:53,760 --> 00:03:00,480 and as doves migrate to a warmer climate twice  a year, so does SQL, which migrates to a schema,   23 00:03:00,480 --> 00:03:06,480 release, after release. In case you're  not familiar with what a SQL migration is,   24 00:03:07,280 --> 00:03:14,800 let's make a quick reminder. Let's imagine that  we're responsible for an application that has user   25 00:03:14,800 --> 00:03:21,840 accounts so it stores usernames and passwords.  Now we want to attach some new information   26 00:03:23,280 --> 00:03:29,280 to profiles, such as whether the user gave  their consent to store cookies for them or not.   27 00:03:30,960 --> 00:03:36,320 This means we'll have to change the  database to add a colon in a table,   28 00:03:37,040 --> 00:03:42,480 which in turn means we're going to write some  SQL code and run this code on the database.  29 00:03:44,960 --> 00:03:50,960 If you're developing a website using Python,  chances are, you've heard of the Django framework.   30 00:03:52,640 --> 00:03:59,920 Because Django helps you with your database, it  has a whole subsystem for writing SQL for you   31 00:03:59,920 --> 00:04:06,480 and especially writing SQL migrations for you and  the subsystem is based on Python file that look   32 00:04:06,480 --> 00:04:13,600 quite like this. So, this is migration module, so  it's a module that contains a single class named   33 00:04:13,600 --> 00:04:22,880 "migration." And inherits Django's own inherent  class. There is dependencies, with Django   34 00:04:26,640 --> 00:04:31,600 lets it determine in what order to run the   35 00:04:32,320 --> 00:04:43,920 migrations and the operations lists, which  contains the AddField or RemoveModel or   36 00:04:43,920 --> 00:04:49,520 many different object that Django knows  how to turn into migration as SQL code.  37 00:04:51,760 --> 00:04:58,640 There's still a way we can use the Django  migration system if we already have the SQL code,   38 00:04:58,640 --> 00:05:09,840 by using run SQL object. We can use it directly  and wrap it inside the Django migration system.   39 00:05:10,960 --> 00:05:16,640 Migrations will be stored in a folder  that is named "migrations." Inside   40 00:05:17,280 --> 00:05:23,840 each and every Django application, these  migration will live in their own module.  41 00:05:26,800 --> 00:05:34,400 And now, for something completely different. In  the last years, I've been working on a project   42 00:05:34,400 --> 00:05:39,760 named procrastinate. Procrastinate is  a Task Queue. Maybe you know celery.   43 00:05:49,040 --> 00:05:55,200 You want something to do done later, for example,  a long task that needs computation time and you   44 00:05:55,200 --> 00:06:07,120 need to respond to the customer quickly. You take  notice of it and you respond, yep, I'll do that   45 00:06:07,120 --> 00:06:12,800 later. That's the name, "procrastinate."  I'm sure that you will be doing it later.  46 00:06:13,680 --> 00:06:23,040 What makes procrastinate unique is that many Task  Queue, such as Celery, use it as a broker and it   47 00:06:23,040 --> 00:06:32,240 is backed by a PostgreSQL database. The mascot  is an elephant so the magic of procrastinate   48 00:06:32,240 --> 00:06:39,440 is turning a rabbit into an elephant. As you  imagine, in order to procrastinate to work,   49 00:06:39,440 --> 00:06:55,520 it needs specific SQL schema and this schema  can sometimes evolve. They can become complex   50 00:06:56,320 --> 00:07:01,840 so I'm showing part of the file, I'm not going  to explain it. It's just to give you an idea.  51 00:07:03,040 --> 00:07:08,800 The procrastinate SQL migration files live in  a migration folder, which is located within   52 00:07:08,800 --> 00:07:14,960 the procrastinate source code. It looks  like the Django migration folder, except   53 00:07:15,680 --> 00:07:19,040 those files are SQL files and not Python modules.  54 00:07:21,680 --> 00:07:27,200 So, on the one hand, procrastinate helps  you when you have long tasks in a website   55 00:07:27,200 --> 00:07:33,600 and on the other hand, Django helps you when  you're building your website so quite logically,   56 00:07:33,600 --> 00:07:38,880 people may want to use procrastinate with  Django and for this reason, we've built a   57 00:07:38,880 --> 00:07:45,120 Django compatibility app within procrastinate.  But you might wonder, what about migrations?   58 00:07:46,800 --> 00:07:53,360 The procrastinate Django application doesn't have  a migration application model. If it did, it would   59 00:07:53,360 --> 00:08:02,320 mean that we would have to manually duplicate  the code from the SQL migrations into the Django   60 00:08:02,320 --> 00:08:09,760 migration and it's something we didn't want to do. What we wanted to do is to make Django believe   61 00:08:09,760 --> 00:08:14,480 that for each SQL migration file,  as we can see on the left side,   62 00:08:15,040 --> 00:08:23,440 there was a corresponding Django module on the  right, in red. But this file wouldn't actually   63 00:08:23,440 --> 00:08:28,960 exist under this so we would want to make  Django believe that there are modules here,   64 00:08:28,960 --> 00:08:35,600 although the migration modules don't exist and  the migration folder, itself, doesn't even exist.  65 00:08:38,080 --> 00:08:45,280 So, what we wanted were a virtual migration  files. At first, I tried, for quite a while,   66 00:08:45,280 --> 00:08:52,400 to see if there was such an existing hook for  doing this within Django and it turns out,   67 00:08:52,400 --> 00:09:01,440 there just isn't. By Python is a dynamic language  so I thought maybe I could take an unofficial way,   68 00:09:02,000 --> 00:09:09,520 monkey patch to get it. All I found  was this was tricky and error prone   69 00:09:09,520 --> 00:09:20,320 so I didn't like the approach. Markus Holtermann, who was, by the way,   70 00:09:20,320 --> 00:09:25,520 on stage yesterday, for a talk on authentication,  he gave me, from the top of his head, the idea   71 00:09:25,520 --> 00:09:33,120 that I was missing, which was to use the Python  import machinery to trick Django. I only know the   72 00:09:33,120 --> 00:09:42,000 Python import machinery existed but I absolutely  did not know what was inside it. So, I had to   73 00:09:43,440 --> 00:09:51,520 take a route and explore the Python importer  secret mechanism and I'm going to give you   74 00:09:51,520 --> 00:09:58,000 some insight from what I learned at the time. So, what's the structure of the import machinery?   75 00:09:58,720 --> 00:10:03,840 Let me start by a diagram showing you the  interactions between the different parts.   76 00:10:04,400 --> 00:10:14,480 Whoops. Wrong slide. Oh, yeah, that's the one.  The import mechanism contains many moving parts   77 00:10:14,480 --> 00:10:19,680 and we're going to introduce just a few elements  today. If you find this topic interesting   78 00:10:19,680 --> 00:10:25,920 and you want to dive deeper, I will give some  pointers to some resources at the end. So,   79 00:10:25,920 --> 00:10:31,840 let's dive in with the part that you probably  already know, which is the import statement.   80 00:10:33,840 --> 00:10:39,600 Actually, chances are that you even know  multiple forms of the import statement, such as   81 00:10:40,240 --> 00:10:52,880 import A to B, from A Import B, from Import  A, Import B. This import statement can be   82 00:10:52,880 --> 00:10:59,840 translated to assignments and function calls.  An import statement always does two things.   83 00:11:00,960 --> 00:11:10,880 First, it will import one module using the built  in function and assign this module to a variable.   84 00:11:11,520 --> 00:11:18,000 By the way, when I say "dunder," it's short  for "double underscore." You'll hear "dunder."   85 00:11:24,080 --> 00:11:32,640 The dunder import function does a few  important things. You may know of syst.modules.   86 00:11:36,080 --> 00:11:50,000 It will start any module that's been imported.  Dunder import will help the cache for modules   87 00:11:50,000 --> 00:11:56,960 that have been imported and add modules to this  cache and they have been successfully imported.  88 00:11:58,960 --> 00:12:11,040 Second thing, when you import A.B.C, it will  import A first. The parent modules are always   89 00:12:11,040 --> 00:12:23,040 imported before the children. When the import of  Module C is successful, it will assign it. Thanks   90 00:12:23,040 --> 00:12:32,320 to that, if you have a Module A.B in your code  and import A.B C, you'll be able to access and   91 00:12:32,320 --> 00:12:40,000 see that you have imported. And then the important  part is the importing the module, itself.  92 00:12:41,920 --> 00:12:52,080 So, this is a three steps operation. First will  be looking for a MetaPathFinders. It is an object   93 00:12:52,080 --> 00:12:58,560 which, given the import pass, says whether or  not it's able to find a corresponding module   94 00:12:58,560 --> 00:13:05,840 in order to import it. Python keeps a list  of MetaPathFinders objects in sys.meta_path.   95 00:13:06,720 --> 00:13:14,400 The idea is that Python will ask  ever finder, from sys.meta_path,   96 00:13:16,160 --> 00:13:20,160 whether they think they can import  a path until one says "yes."  97 00:13:22,160 --> 00:13:28,800 Asking MetaPathFinders object is done by  calling it find_spec method, passing the   98 00:13:28,800 --> 00:13:38,480 spec as parameter. It returns none if it cannot  load the path or it returns a ModuleSpec object.   99 00:13:39,040 --> 00:13:44,560 A ModuleSpec object has a few properties  explaining how to load the module.   100 00:13:45,120 --> 00:13:55,360 One is ModuleSpec.loader. It has  an exec module method and it will   101 00:13:56,160 --> 00:14:07,120 create using the info it gets from the  ModuleSpec and call the exec_module and   102 00:14:08,240 --> 00:14:17,200 it is expected to attach attribute to the  module resulting in the final module imported.  103 00:14:17,920 --> 00:14:23,520 A normal Python modules that are  loaded from a source file on the disk,   104 00:14:24,480 --> 00:14:30,640 this would be the part where Python will open  the file and then run eval on all the contents,   105 00:14:30,640 --> 00:14:38,560 store everything that was created and  attach everything on the module object.  106 00:14:45,600 --> 00:14:51,760 So, one of the places we'll want to  explore, in order to add our customization,   107 00:14:51,760 --> 00:15:01,840 will be the sys.meta_path list. Let's first have a  look at the MetaPathFinders that we could find in   108 00:15:01,840 --> 00:15:10,720 classical sys.meta_path. If you open sys.meta_path  in Python, you'll find the built in importer and   109 00:15:10,720 --> 00:15:20,000 the FrozenImporter, those are what Python needs  in order to work. By the way, when a class is both   110 00:15:20,560 --> 00:15:25,840 implementing the finder interface and the  loader interface, we call it an importer.   111 00:15:28,000 --> 00:15:36,400 Before going deeper in the next element, let me  add a quick note on sys.meta_path. So, it's just   112 00:15:36,400 --> 00:15:42,720 a simple list and you can actually add your own  finders in there. The only requirement is that   113 00:15:42,720 --> 00:15:59,760 anything you add must have find Spec Method. And then there's domain finder, that Python   114 00:15:59,760 --> 00:16:08,880 implements, which is the one that's used most of  the time. You need a PathFinder. The PathFinder   115 00:16:08,880 --> 00:16:16,320 will look in multiple directories to see if we  can find a module that we're trying to load. For   116 00:16:16,960 --> 00:16:24,000 [Indiscernible] module, the list of directories  to search is given by sys.path, which is a list of   117 00:16:24,000 --> 00:16:34,320 strings. This is the in famous sys.path that you  may or may not have had to tinker within the past.   118 00:16:35,520 --> 00:16:43,440 Sys.path contains a list of folders on the disk to  explore in order to find a module. When the module   119 00:16:43,440 --> 00:16:51,600 is not a top level package, then, logically, it  has a parent and the parent module has a dunder   120 00:16:51,600 --> 00:16:58,320 path attribute, which is also going to be a list  of strings and similarly, this list of string   121 00:16:58,320 --> 00:17:05,840 is the list of places on the disk where one would  expect to find submodules of the parent's package.   122 00:17:06,560 --> 00:17:14,400 So, whether you're importing from whether you're  importing a top level package or a submodule,   123 00:17:14,400 --> 00:17:18,400 you'll end up with a list of  folders to explore on the disk   124 00:17:18,400 --> 00:17:25,040 and this folders are called path entries. We're  going to iterate on each path entries we have.   125 00:17:26,720 --> 00:17:37,600 The PathFinder will use another attribute which  is sys.path_hooks. Sys.path_hooks is a list of   126 00:17:37,600 --> 00:17:45,040 callables. We will iterate each callable from  sys.path_hooks so we're doing double iteration,   127 00:17:45,040 --> 00:17:51,040 both on the path entries and on the path hooks  so it's just kind of a [Indiscernible] loop.  128 00:17:52,880 --> 00:18:01,040 Each path hook receives the current  one and iterative one. It receives a   129 00:18:01,040 --> 00:18:14,320 importer or it returns a Path Entry Finder.  It may return a module spec. At this point,   130 00:18:14,320 --> 00:18:24,480 you may experience a very strange d j vu. It is  close from the meta PathFinder we talked about.   131 00:18:32,800 --> 00:18:38,800 I'll do my best to make it explicit, what  the difference is between a path entry finder   132 00:18:38,800 --> 00:18:48,880 and a meta PathFinder. Meta PathFinder tells  you whether or not it can find a module A.B.   133 00:18:51,280 --> 00:18:59,440 path entry finder tells you whether or not you can  find a module A.B Inside one very specific folder,   134 00:19:00,560 --> 00:19:08,720 which is the path entry that the path hook was  called. Path entry finders are very specialized   135 00:19:08,720 --> 00:19:15,840 meta PathFinders that restrict themselves  to searching on the disk in one directory.  136 00:19:19,120 --> 00:19:29,120 In order to determine if the path entry  finder, we'll have the find_spec method. So,   137 00:19:29,120 --> 00:19:37,360 kind of same as for the meta PathFinder. Same as  for the PathFinder, it returns none, we could go   138 00:19:37,360 --> 00:19:44,640 on and try another path hook or another path entry  or return a spec and, hurray, we found our module.  139 00:19:46,720 --> 00:19:57,360 So, what would be the default path hooks provided  by Python? One, the first one is zipimporter path   140 00:19:57,360 --> 00:20:04,000 hook. So, it's the one that lets you run Python  modules that are inside zip files. By the way,   141 00:20:04,000 --> 00:20:10,480 yes, you don't have to unzip archives to run  Python files that are inside zip archives.  142 00:20:13,760 --> 00:20:20,080 One of the hooks the single one is FileFinder.  So, it will look for classical Python   143 00:20:20,080 --> 00:20:33,920 files and folders on the disk, such as .py files  and as well as folders containing dunder in it,   144 00:20:33,920 --> 00:20:43,920 .py. You can add your own callables to the path  hook. So one way to do that would be to implement   145 00:20:43,920 --> 00:20:50,640 your path entry finder class and you would give  it a constrictor dunder in it that receives   146 00:20:50,640 --> 00:20:58,160 the path entry. So, you'd register the class,  itself, as the path hook. Python will call the   147 00:20:58,160 --> 00:21:04,640 restricter and pass the path entry as parameter  so we may store this as an instance attribute   148 00:21:05,280 --> 00:21:14,880 and Python, with path entry finder instance, it  will call the find_spec method on it so this way,   149 00:21:14,880 --> 00:21:20,160 you'll be able to access both the path entry  and the import path that's being searched.  150 00:21:24,560 --> 00:21:32,960 So, we're approaching the end of the wonderful  land of Python imports. If we remind ourselves   151 00:21:32,960 --> 00:21:38,400 of the initial goal, we were looking for ways  to trick Django into believing that there is   152 00:21:38,400 --> 00:21:47,040 a migration folder and that this folder contains  plenty of migration files. So, Django is going to   153 00:21:48,320 --> 00:21:54,320 dynamically import our applications  migrations module but because Django   154 00:21:54,320 --> 00:22:01,120 doesn't know the name of our application, it would  be complicated to use an import statement directly   155 00:22:01,120 --> 00:22:10,160 because the name will be inside a variable. Django could use the dunder import function   156 00:22:10,160 --> 00:22:16,160 directly and it would work, but this  function is made for Python to call   157 00:22:16,160 --> 00:22:22,720 and there's actually a better interface for  programmers to use, which is the Importly module.   158 00:22:25,840 --> 00:22:37,040 This function's goal is to import modules when  their path is dynamic. So, Import Module calls. So   159 00:22:37,600 --> 00:22:43,040 Django will call Import Module and we'll  expect to receive the Migrations module.   160 00:22:45,920 --> 00:22:56,160 Now that Django has the migration, how does it  discover all the ones inside the module? For now,   161 00:22:56,160 --> 00:23:01,680 we've talked about how to import a module given  its name, but not how to browse existing modules.   162 00:23:02,400 --> 00:23:09,120 This is something we can achieve with  pkgutil.iter_modules() which receives   163 00:23:09,120 --> 00:23:14,640 the module path and returns a list of objects  that reference the submodules below that path   164 00:23:15,440 --> 00:23:19,680 so it taps directly into the  PathFinder and the path entry finder   165 00:23:20,800 --> 00:23:28,160 and it will call a method that's named modules  on all the path entry finders that we found.  166 00:23:29,600 --> 00:23:39,360 Then, Django will know the existing name of the  migration submodules and it will call import   167 00:23:39,360 --> 00:23:49,840 module again on every submodule that was found.  So, it will read and hopefully use the migrations.  168 00:23:52,480 --> 00:24:03,840 So, if you want to take a screenshot of  the graph, this might be the right time.  169 00:24:05,200 --> 00:24:08,560 So, what does it look like in procrastinate?   170 00:24:09,760 --> 00:24:18,160 Let's have a look at the code and see the magic  trick for real. As a reminder, we're trying to   171 00:24:18,160 --> 00:24:26,160 generate both the migration module, the dunder.py  and the individual submodules for each migration.   172 00:24:28,160 --> 00:24:35,760 Also, here's what a migration submodule look  like. It contains a single class name migration   173 00:24:35,760 --> 00:24:42,960 that has a few attributes. So, creating all  the migration classes dynamically by inspecting   174 00:24:42,960 --> 00:24:52,080 existing parts of SQL is not the hardest part  of the problem. It's a function that takes   175 00:24:52,080 --> 00:24:59,440 a migration written in SQL as a string and  generates a Django migration class. So, this   176 00:24:59,440 --> 00:25:05,360 will be the class we want to attach inside the  virtual dedicated migration that we're creating.  177 00:25:06,880 --> 00:25:11,920 In order to tap into the import machinery,  we'll need to create an importer, which, as you   178 00:25:11,920 --> 00:25:23,840 remember, is both a MetaPathFinders and a Loader.  In the constrictor, we'll generate and then store   179 00:25:23,840 --> 00:25:28,320 all the migration class that we're going to  want to dispatch into our virtual modules.  180 00:25:29,680 --> 00:25:35,280 Our importer is a finder, so it needs  find_spec method that will only work   181 00:25:35,920 --> 00:25:42,720 for the migration module and the migration  submodules. If we receive the right pass as   182 00:25:42,720 --> 00:25:52,800 parameter, then we will return a spec instance. On our module spec, the loader is set to "self"   183 00:25:52,800 --> 00:25:59,840 because it is the finder and the loader.  The loader part will be on the same object.  184 00:26:01,920 --> 00:26:08,560 Then, let's look at the loading part.  We receive an module as argument and our   185 00:26:08,560 --> 00:26:13,680 job is to attach things to this module.  The loader must be able to load both the   186 00:26:13,680 --> 00:26:27,520 migrations module and the individual submodules. If we are loading the migrations module, itself,   187 00:26:27,520 --> 00:26:34,720 then the only important thing is the dunder path  attributes, because the string we add in there is   188 00:26:34,720 --> 00:26:41,280 the one that will later be checked by path hook.  We want to make sure to put something that will   189 00:26:41,280 --> 00:26:48,560 not be recognized as a valid path by other hooks,  thus using angle brackets, as you can see, here,   190 00:26:48,560 --> 00:26:56,400 which is unlikely to be a path on the disk. If we're using a submodule, that's the moment   191 00:26:56,400 --> 00:27:01,520 where we finally get to do the magic. We figure  out the name of the module we're building,   192 00:27:01,520 --> 00:27:10,960 using module and the domain and we attach the  class, the proper class inside the module.   193 00:27:14,720 --> 00:27:25,120 Now, our importer will also have metas added to  sys.path_hooks. So let's see what it looks like.   194 00:27:26,240 --> 00:27:32,960 This is the meta you can see above. It only  reacts to paths that match the attribute   195 00:27:32,960 --> 00:27:39,680 we've set on the migration module. And  it returns "self" as the loader the   196 00:27:41,600 --> 00:27:50,880 finder for any path that matches our virtual  path and it raises an importer for anything else.  197 00:27:52,560 --> 00:28:04,080 There is the items module method. In here,  you can see that we're leveraging that   198 00:28:04,080 --> 00:28:10,720 we've already pre computed all the migrations  and stored them in the finder the importer class.  199 00:28:12,880 --> 00:28:22,320 And then, the last touch, we register importer in  sys.meta_path and the path hook in sys.path hook.   200 00:28:24,160 --> 00:28:29,280 We are being good citizens here. We're adding  our stuff at the end of both iterables,   201 00:28:29,280 --> 00:28:35,520 which means that our [Indiscernible]  importer will be called but only if the other   202 00:28:35,520 --> 00:28:44,080 conventional import mechanism didn't succeed. In  other words, we are not slowing every import down,   203 00:28:44,080 --> 00:28:47,760 but we're just slowing a little  bit, the unsuccessful imports.  204 00:28:51,600 --> 00:28:59,520 So, showtime. Now, it's time to ask Django to  apply our migrations and everything kicks in from   205 00:28:59,520 --> 00:29:08,320 all over the place. So, Django will try to find  the migrations module and then it will export all   206 00:29:08,320 --> 00:29:15,520 the submigrations. It will the submodules, it will  find them and then execute them and everything   207 00:29:15,520 --> 00:29:24,480 runs smoothly. Presto. Whew, what a journey.  Before leaving the stage, let me share a couple   208 00:29:24,480 --> 00:29:33,360 of [Indiscernible]. First, yes, this might another  engineered solution. We could have a static   209 00:29:33,360 --> 00:29:42,400 migration generator that would write normal Python  files. In all honesty, I'm happy I get to learn   210 00:29:42,400 --> 00:29:51,040 a ton of stuff doing this. To this day, I feel  like hooking the import machinery is the cleanest   211 00:29:51,040 --> 00:29:57,200 solution to the problem we had. It doesn't  really add a lot of complicated code in the end.  212 00:29:59,360 --> 00:30:04,320 If you plan to use this knowledge to do some  good, you should definitely play with it   213 00:30:04,320 --> 00:30:11,040 and make it into a nice library. If you do  so, please feel free to send it my way. But,   214 00:30:11,040 --> 00:30:18,400 remember, smart code goes into libraries and  boring codes goes into projects because no   215 00:30:18,400 --> 00:30:25,520 one wants to debug a very custom, meta, meta path  entry PathFinder that went wild into production.   216 00:30:26,960 --> 00:30:33,200 Even for procrastinate, I still think it  might be a good idea, someday, to custom   217 00:30:33,200 --> 00:30:43,200 the Django process into a dedicated library. And finally, the import machinery is a very   218 00:30:43,200 --> 00:30:47,520 interesting part of Python and it could  give birth to very innovative users.   219 00:30:48,560 --> 00:30:55,440 Knowledge of this might not change your day to  day practice but it can be more than one tool in   220 00:30:55,440 --> 00:31:03,920 your built. As a general advice, Python is very  accessible and both quite rewarding and useful   221 00:31:03,920 --> 00:31:12,800 to take some time to learn how how one specific  Python subsystem works and so, yeah. Be curious   222 00:31:13,360 --> 00:31:21,040 with language and experiment and do some magic. Thank you. I'll be glad to take some questions,   223 00:31:21,040 --> 00:31:26,320 but while this is a prerecorded video, the  live question session will be featuring   224 00:31:26,320 --> 00:31:32,800 the live version of me, just woken up at 6:00  a.m., so please be gentle. Thank you very much,   225 00:31:32,800 --> 00:31:45,840 Goulwen, the illustration. Have  a nice day and see you around.  226 00:31:45,840 --> 00:31:47,832 >> All right. Thank you so much, Joachim,  for that fascinating talk. Do you have any   227 00:31:47,832 --> 00:31:49,200 any final words, closing remarks? JOACHIM JABLON: Uhhh. Yeah. Well,   228 00:31:49,199 --> 00:31:50,996 no. I'm really happy that everyone was able to  watch and I'll be available for questions in   229 00:31:50,997 --> 00:31:52,834 the chat. Please feel free to reach out if you  have any questions, even after the conference.  230 00:31:52,834 --> 00:31:54,514 >> Thanks, again, for joining us so early  in the morning and, yeah, thank you for the   231 00:31:54,514 --> 00:31:56,640 talk. All right, everyone, we've got another 15  minute break and we'll see you back after that.