keyboard-shortcut
d

imports in your code ⬇️

9min read

an image

Sometimes you will scan through someone's code and find imports that aren't at the top of the file. It feels a bit strange and uncomfortable. Why are they doing this? Is there something I'm missing...

Performance

Sometimes you will see code like this:

import helpful_package

def do_stuff():
    import sometimes
    ...

This can occur for one of many reasons.

Performance - Speed 🚀

Is it quicker or slower to use an import within a function?

Well, if you never call the function, the import is never executed. As a result you have potentially saved one import. Your performance has been boosted! 🚀 Yay. The saving depends completely on how long it takes to execute your import. I would start asking questions if your the package you are importing is really slow. Is your package doing too much before it is even called? What is causing the expensive start-up? If the import is really fast why bother trying to avoid this import?

Okay, so if you never import the function you save the time it takes to import that package once. As soon as you call the function once you will lose this "benefit". Damn... that didn't last long. Maybe it wasn't worth it...

If you call the function multiple times, some people will argue that you will repeatedly execute the import, which means that you will pay the import penalty multiple times! This would be far worse than just importing once at the top of the module! 😱 Luckily, this simply isn't true in modern python. Whether you import at the top of a file or mid-function, the import will be cached. Consequently, you can execute the same import once, twice or multiple times and you will only pay the import penalty once. Nice! 🙌

So, the benefit of importing things "mid-function" ranges between zero (no benefit if you call the function at least once) and the time it takes to import the package once.

This benefit will be relevant to you if you meet ALL of the following criteria:

  • For some reason, the package you want to use is expensive to import.
  • Start-up time for your application (cold starts) is of huge importance to you.
  • You call this function only a small percentage of the time.

If you meet all the criteria, then it may just be worth having some janky, slightly awkward deferred imports in your code (unless of course you care about any of the cons listed further in this article).

Performance - Memory 🧠

If you are deferring imports because your application is running out of memory, then you have bigger problems than functions executing imports...

Deferring imports in the hope that you won't hit an OutOfMemory error is relying quite heavily on "pot luck". If you understand the flow of your code well enough that deferring imports will help, then you're probably better off just having separate entry points for each flow of functionality! If you do not understand the flow of your code, then you are essentially rolling the dice and hoping you don't import too many packages at once!

If one package in particular is eating up all your memory, then again, should you be using this package? Can this package be improved? Does this package need its own dedicated worker to eat up resources?

Avoiding circular imports 🔄

Imagine you create a circular import. module_a contains function_a and module_b contains function_b. You have foolishly decided that function_a requires function_b but also function_b requires function_a.

# module_a.py
from module_b import function_b

def function_a():
    # do stuff
    function_b()
# module_b.py
from module_a import function_a

def function_b():
    # do other (but seemingly related) stuff
    function_a()

When you try to import one of these functions...

# main.py
from module_a import function_a

DOH! 🤦‍♂️ You've got circular import!

ImportError: cannot import name 'func_a' from partially initialized module
'module_a' (most likely due to a circular import)

Yes, burying just one of the imports within a function can "solve" this problem (maybe not solve, more defer...):

# module_a.py

def function_a():
    from module_b import function_b
    # do stuff
    function_b()

Your problem has now gone away and now you can deal with infinite recursion instead. Yaay! 🎉 There are usually much nicer ways to solve circular imports (such as re-organising your code or importing the whole module), so in my view, you must be really desperate to shove your import inside your function to solve your circular problems...

Conditional imports 🔀

Sometimes you will see conditional imports used to save executing unnecessary imports. The most common example of this is when importing type hints that are only used by a type checker like mypy:

from __future__ import annotations
# there are conflicting reports on when this will become the
# default behaviour in python, so we'll keep using it for a while to
# defer the importing of type hints

if typing.TYPE_CHECKING:
    from well_typed_module import Example

class MyThing:
    def get_example(self) -> Example:
        ...
        return example

Only importing these type hints when checking types will save you a very small amount of time on every execution of the module. Personally, I think this just adds unnecessary complexity for a very small benefit. It also means that you have to defer the importing of your type hints (but this is going to be the default behaviour soon anyway).

Why you should avoid mid-code imports...

Having imports anywhere other than at the top of your file, neatly organised into sections is very likely to cause confusion:

  • If you have conditional imports and import something within a function (which will be cached) it is very hard for someone reading the code to assess the exact state of the environment when reading your code.
  • You may accidentally leave a redundant import in your code, because your code wasn't sure about your conditional import.
  • Inconsistent code structure makes it harder for people to collaborate.
  • Importing packages within functions can defer ImportErrors to an inconvenient time. Preferably, you will want to identify these errors as early as possible.
  • If someone wants to use the same package somewhere else in the module they will most likely just move your import to the top of the file, eliminating any performance gains you may have tried to gain. In the worst case scenario, they may not even see your import and will re-import the package (because they weren't expecting an import anywhere other than the top of the file).

Some special imports...

If you import anything using __future__, apparently this should be at the top! The use of __future__ imports elsewhere is supposedly invalid and results in a SyntaxError, but I haven't been able to replicate this behaviour. If true, this is an example where the location of your import isn't a preference, but actually a hard requirement. Until there is some proof though, this issue doesn't seem to have any real relevance.