48

I would like to be able to write:

try:
    import foo
except ImportError:
    install_the_module("foo")

What is the recommended/idiomatic way to handle this scenario?

I've seen a lot of scripts simply print an error or warning notifying the user about the missing module and (sometimes) providing instructions on how to install. However, if I know the module is available on PyPI, then I could surely take this a step further an initiate the installation process. No?

j b
  • 4,737
  • 4
  • 36
  • 57
  • 4
    Its good in theory but its a pain in the ass in reality. Just get the users to install the packages themselves or provide them when creating your package as dependencies. – Jakob Bowyer May 25 '11 at 08:34
  • 4
    @JakobBowyer - Your first sentence sums up most things that people get paid to do - if something was a good idea and not difficult to do then it'd already be done without anyone needing to pay someone else to go through the PITA. I was paid at work to write a script which automated sever deployments and could be run without any user interaction, so I needed to automatically handle the case where the Python modules weren't already installed. Now the world can benefit from what I was paid to do - I documented it below: http://stackoverflow.com/a/25643988/901641 – ArtOfWarfare Sep 03 '14 at 12:17
  • 5 upvotes doesn't make your python style conform to PEP8 – Corey Goldberg Jan 20 '17 at 16:37
  • 1
    @CoreyGoldberg - Allow me to quote from PEP8, since you seem to hold it in such high regard: "A Foolish Consistency is the Hobgoblin of Little Minds". When I feel uncertain about how to format a line, I'll see what PEP8 has to say. Otherwise I aim for maximum readability. Feel free to edit my code if you think I've fallen short. – ArtOfWarfare Jan 20 '17 at 16:41
  • that's very unpythonic of you – Corey Goldberg Jan 20 '17 at 16:43
  • 1
    @CoreyGoldberg Python begs to differ. "Beautiful is better than ugly. / Explicit is better than implicit. / Simple is better than complex." — "The Zen of Python", line 1-3 – JuSTMOnIcAjUSTmONiCAJusTMoNICa May 19 '17 at 19:54

4 Answers4

48

Risking negative votes, I would like to suggest a quick hack. Please note that I'm completely on board with accepted answer that dependencies should be managed externally.

But for situations where you absolutely need to hack something that acts like self contained, you can try something like below:

import os

try:
  import requests
except ImportError:
  print "Trying to Install required module: requests\n"
  os.system('python -m pip install requests')
# -- above lines try to install requests module if not present
# -- if all went well, import required module again ( for global access)
import requests
ring bearer
  • 18,848
  • 7
  • 53
  • 68
  • 1
    os.system is deprecated... use the subprocess module – Corey Goldberg Jan 20 '17 at 16:26
  • 3
    using subprocess module is just preferable but os.system is not deprecated – Roman Nov 22 '18 at 16:02
  • it's a hack but a damn good one. Thanks. – Mujeeb Ishaque Mar 29 '20 at 13:38
  • A solution using `subprocess` can be found at https://stackoverflow.com/a/58040520 . – asmaier Aug 12 '20 at 14:46
  • This adds a package without the user knowing about it. Bad idea. – goelakash Sep 29 '20 at 18:02
  • @goelakash It's not bad if the program alerts the user that some dependencies are required and can be downloaded automatically (and possibly gives them a size apprx). – JVE999 Oct 11 '20 at 04:47
  • @JVE999 Yes, it is. The user should have the option to reject the installation. This should not proceed without getting a confirmation from the user. Python environments are easy to break, and a random script that could potentially do that should have safeguards. – goelakash Oct 12 '20 at 07:29
  • Thanks for the risk, this is very much useful when you don't have the time or project requirement to make a package (egg or tarball) Also good for CI/CD when you are reinstalling on Containers/VM's/Docket. – Jamie Nicholl-Shelley Nov 03 '20 at 13:22
36

Installation issues are not subject of the source code!

You define your dependencies properly inside the setup.py of your package using the install_requires configuration.

That's the way to go...installing something as a result of an ImportError is kind of weird and scary. Don't do it.

Mohamed Taher Alrefaie
  • 14,080
  • 5
  • 41
  • 62
Andreas Jung
  • 1
  • 18
  • 70
  • 118
  • 1
    This doesn't help in the case where the required module is needed by setup.py itself, though. Is there a hook of some sort that setup() provides that can allow it to install a setup-time dependency via setup_requires and then use a command class defined in that newly installed dependency within the same call to setup(), without adding excessive boilerplate to setup.py for every package that uses that dependency? – hosford42 May 31 '16 at 16:00
  • I added [a new question](http://stackoverflow.com/questions/37551598/providing-a-custom-command-class-to-setup-py-in-a-separately-installed-package) to address my specific use case. – hosford42 May 31 '16 at 16:59
32
try:
    import foo
except ImportError:
    sys.exit("""You need foo!
                install it from http://pypi.python.org/pypi/foo
                or run pip install foo.""")

Don't touch user's installation.

CharlesB
  • 75,315
  • 26
  • 174
  • 199
9

Here's the solution I put together which I call pyInstall.py. It actually checks whether the module is installed rather than relying on ImportError (it just looks cleaner, in my opinion, to handle this with an if rather than a try/except).

I've used it under version 2.6 and 2.7... it would probably work in older versions if I didn't want to handle print as a function... and I think it'll work in version 3.0+ but I've never tried it.

Also, as I note in the comments of my getPip function, I don't think that particular function will work under OS X.

from __future__ import print_function
from subprocess import call

def installPip(log=print):
    """
    Pip is the standard package manager for Python. Starting with Python 3.4
    it's included in the default installation, but older versions may need to
    download and install it. This code should pretty cleanly do just that.
    """
    log("Installing pip, the standard Python Package Manager, first")
    from os     import remove
    from urllib import urlretrieve
    urlretrieve("https://bootstrap.pypa.io/get-pip.py", "get-pip.py")
    call(["python", "get-pip.py"])

    # Clean up now...
    remove("get-pip.py")

def getPip(log=print):
    """
    Pip is the standard package manager for Python.
    This returns the path to the pip executable, installing it if necessary.
    """
    from os.path import isfile, join
    from sys     import prefix
    # Generate the path to where pip is or will be installed... this has been
    # tested and works on Windows, but will likely need tweaking for other OS's.
    # On OS X, I seem to have pip at /usr/local/bin/pip?
    pipPath = join(prefix, 'Scripts', 'pip.exe')

    # Check if pip is installed, and install it if it isn't.
    if not isfile(pipPath):
        installPip(log)
        if not isfile(pipPath):
            raise("Failed to find or install pip!")
    return pipPath

def installIfNeeded(moduleName, nameOnPip=None, notes="", log=print):
    """ Installs a Python library using pip, if it isn't already installed. """
    from pkgutil import iter_modules

    # Check if the module is installed
    if moduleName not in [tuple_[1] for tuple_ in iter_modules()]:
        log("Installing " + moduleName + notes + " Library for Python")
        call([getPip(log), "install", nameOnPip if nameOnPip else moduleName])

Here are some usage examples:

from datetime  import datetime
from pyInstall import installIfNeeded

# I like to have my messages timestamped so I can get an idea of how long they take.
def log(message):
    print(datetime.now().strftime("%a %b %d %H:%M:%S") + " - " + str(message))

# The name fabric doesn't really convey to the end user why the module is needed,
# so I include a very quick note that it's used for SSH.
installIfNeeded("fabric", notes = " (ssh)", log = log)

# SoftLayer is actually named softlayer on pip.
installIfNeeded("SoftLayer", "softlayer", log = log)

Edit: A more cross-platform way of getting pipPath is:

from subprocess import Popen, PIPE
finder = Popen(['where' if isWindows() else 'which', 'pip'], stdout = PIPE, stderr = PIPE)
pipPath = finder.communicate()[0].strip()

This makes the assumption that pip is/will be installed on the system path. It tends to be pretty reliable on non-Windows platforms, but on Windows it may be better to use the code in my original answer.

ArtOfWarfare
  • 17,763
  • 14
  • 122
  • 177