5

Say I have a module with a bunch of objects, none of which need a third-party library. Well, almost none: one single function, call it asterix(), needs a third-party package. asterix() isn't the point of the module, but is convenient to have if and when needed.

It seems a shame to have to require and import a whole third-party package just for that one function. What are my options? Moving asterix() to another module/package or vendoring it seem silly.

I have found that the best balance of quick and clean is to simply do the import within asterix() itself, possibly with some extra try/except logic around it. It violates the principle of least surprise and the convention of having imports at the top. But I tell myself I'll always be violating something.

What does crowd wisdom say about this? Is there something glaring I'm missing?

def asterix(...):
    try:
        import something_special
    except ModuleNotFoundError as err:
        # do something about err (help the user out!)
    # The special code...
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
thorwhalen
  • 1,920
  • 14
  • 26
  • [import at module level or function level?](https://stackoverflow.com/questions/9614051/import-at-module-level-or-at-function-level) – wim Sep 22 '20 at 18:45
  • Thanks @wim. Yes, PEP8. But 'tis more a question for the grownups in the room -- when faced with this tradeoff, what are the tricks beyond the book? For example, the disadvantage of "import check at every function call" could be mitigated by conditioning the very definition of the function on the successful import. Nice. But comes with another set of tradeoffs... – thorwhalen Sep 22 '20 at 18:51
  • 1
    Your solution is a good one. The import check at every function call is just a quick `dict` lookup and justifiable here. – tdelaney Sep 22 '20 at 18:54
  • 1
    @wim definitely imports at the top of the file are recommended practice by PEP 8. But also in PEP 8 is this: "However, know when to be inconsistent -- sometimes style guide recommendations just aren't applicable. When in doubt, use your best judgment. Look at other examples and decide what looks best. And don't hesitate to ask!" – Mark Ransom Sep 22 '20 at 19:05

1 Answers1

3

When I was new to the language I was tempted to use function-inline imports as well. In hindsight, now with 10+ years experience in Python, I can tell you that it was usually not a good idea to do that. Put all imports at the top of the module, unless you have a really good reason to defer them. Inline imports hide dependencies. If there is a good reason to defer the import, make sure you have the necessary logging and monitoring to know where/when it fails.

I'm aware that "because the style guide says to" isn't really a satisfactory answer, so here's what I think is the main underlying issue: packaging and deployment in Python can be complicated and tricky to get right. Imagine that your code will eventually use asterix but for whatever reason the something_special was not installed (or it failed import for any other reason, e.g. a recursive dependency was missing). You want to fail as early as possible. Either crash in the test suite, or even in a failed deployment / rollback. What you do not want is an ImportError crash at runtime, whenever the app happens to eventually call asterix (this could mean some poor on-call getting paged at 3:00 AM).

I agree that vendoring code is usually silly (glorified copy-n-paste). The suggestion to move the asterix into a dedicated module is not bad, this is actually the approach I would recommend.

# mod_with_extra_dep.py
import something_special

def asterix():
    ...

To make the dependency optional, use package metadata:

# setup.py
from setuptools import setup

setup(
    ...
    extras_require={"asterix": ["something_special"]}
)

If you're using entry-points, they also support optional requirements in packaging metadata (spec, setuptools example). This provides the best of both worlds:

  • the dependencies are explicit
  • something_special will not actually be imported unless it is needed

Any code that wants to use asterix will trigger the import at the time the name asterix is accessed, not at the time the function is actually called.

wim
  • 338,267
  • 99
  • 616
  • 750