Les environnements virtuels sont un mécanisme très souvent utilisé pour la gestion de dépendances et isolation entre projets python développés dans une même machine. Ils sont aussi très souvent utilisés pour faire face à la fragmentation amenée par l’arrivée de Python 3. Par exemple, parfois il est nécessaire d’installer plusieurs versions de Python sur une même machine, pour pouvoir disposer d’une version plus ou moins récente que celle installée par l’administrateur système.

Les environnements virtuels ont une longue histoire dans le monde Python. Cela a démarré avec un script créé par Ian Bicking, qui s’est transformé en workingenv.py puis virtualenv. Finalement, la solution a été standardisée dans le PEP 405 implémenté dans le module venv disponible dans la librairie standard en Python 3.6.

Dans un projet Python, on utilisera très souvent un environnement virtuel avec un fichier requirements.txt généré par pip freeze et interprété par pip install -r pour s’assurer que projet est développé et exécuté avec les bonnes versions de Python et des modules tiers utilisés par le projet. Un environnement virtuel contient un binaire Python.exe avec ses librairies et fichiers include, et un ensemble de packages tiers pré installés.

Dans cet article, on verra comment sont utilisés et comment sont implémentés les environnements virtuels.

Tuto : créer des environnements virtuels

Pour ceux qui n’ont jamais utilisé des environnements virtuels, on expliquera rapidement comment s’en servir.

Pour créer un environnement virtuel, il suffit d’exécuter le module venv en passant en paramètre le nom du dossier qui contiendra l’environnement virtuel (env dans cet exemple).

$ python3.5 -m venv env

La commande va créer un sous-dossier dans le dossier actuel, appelé env. Dans ce dossier, on trouvera des scripts pour activer/désactiver l’environnement dans un sous-dossier env/bin.

Pour activer un environnement, il suffit de charger un des scripts activate :

$ source env/bin/activate
(env) $ python
Python 3.5.2 (default, Nov 23 2017, 16:37:01)                         
[GCC 5.4.0 20160609] on linux                                         
Type "help", "copyright", "credits" or "license" for more information.
>>>                                                                   

Après l’activation de l’environnement, on pourra utiliser sa version de Python et tous ses packages en appelant l’exécutable Python. Pour installer un module, utilisez pip comme pour installer n’importe quel autre module.

(env) $ pip install django
...
(env) $ ls env/lib/python3.5/site-packages/
django                  easy_install.py  pip-8.1.1.dist-info  pkg_resources-0.0.0.dist-info  pytz                   setuptools
Django-2.0.3.dist-info  pip              pkg_resources        __pycache__                    pytz-2018.3.dist-info  setuptools-20.7.0.dist-info

Dans cet exemple, on voit que le module django est installé dans le sous-dossier site-packages de l’environnement. Son installation est donc isolée du reste du système.

Pour désactiver un environnement, il suffit d’appeler la commande desactivate créée par la script activate :

(env) $ desactivate

Où Python cherche les modules importés par un projet ?

La variable sys.path : est la base du chargement de modules Python. Elle contient une liste de chemins de dossiers où le moteur Python cherchera des modules. Le moteur python utilise la variable d’environnement PYTHONPATH pour initialiser cette variable. Pour éviter de laisser aux utilisateurs la tâche de gérer cette variable, des mécanismes standard d’installation et localisation de modules ont été créés.

Le moteur python cherche des modules dans un dossier site-packages, qui se trouve sous le chemin indiqué par la variable d’environnement PYTHONHOME. C’est dans ce répertoire qui setuptools installe des modules. Quand la variable PYTHONHOME n’est pas configurée, le moteur Python cherche le dossier site-packages dans le sous dossier lib/pythonX.Y/site-packages le plus proche du binaire.

Comment ça fonctionne ?

Pour comprendre comment cela fonctionne, je vous propose d’analyser le code de virtual-python. Un des premiers packages utilisés pour implémenter des environnements virtuels en Python.

Les plus curieux peuvent aller voir la version actuelle du même code. Vous verrez que le principe reste le même.

Dans les lignes 11-13, on voit qu’on se concentre sur trois dossiers : lib, où on trouvera des modules. Include, où on trouvera des headers, et *.h et bin, où on trouvera le binaire Python ainsi que d’autres scripts.

Dans les lignes 19-45, on copie les fichiers de l’installation de Python utilisés pour créer l’environnement. Remarquez que le dossier site-packages (où les modules tiers seront installés) n’est pas copié. A la place, le script crée un dossier vide.

...
def main():
    ...
    parser = optparse.OptionParser()
    ...

    options, args = parser.parse_args()
    global verbose

    home_dir = os.path.expanduser(options.prefix)
    lib_dir = join(home_dir, 'lib', py_version)
    inc_dir = join(home_dir, 'include', py_version)
    bin_dir = join(home_dir, 'bin')

    ...

    prefix = sys.prefix                 
    mkdir(lib_dir)
    stdlib_dir = join(prefix, 'lib', py_version)
    for fn in os.listdir(stdlib_dir):
        if fn != 'site-packages':
            symlink(join(stdlib_dir, fn), join(lib_dir, fn))

    mkdir(join(lib_dir, 'site-packages'))
    if not options.no_site_packages:
        for fn in os.listdir(join(stdlib_dir, 'site-packages')):
            symlink(join(stdlib_dir, 'site-packages', fn),
                    join(lib_dir, 'site-packages', fn))

    mkdir(inc_dir)
    stdinc_dir = join(prefix, 'include', py_version)
    for fn in os.listdir(stdinc_dir):
        symlink(join(stdinc_dir, fn), join(inc_dir, fn))

    if sys.exec_prefix != sys.prefix:
        exec_dir = join(sys.exec_prefix, 'lib', py_version)
        for fn in os.listdir(exec_dir):
            symlink(join(exec_dir, fn), join(lib_dir, fn))

    mkdir(bin_dir)                                              
    print 'Copying %s to %s' % (sys.executable, bin_dir)
    py_executable = join(bin_dir, 'python')
    if sys.executable != py_executable:
        shutil.copyfile(sys.executable, py_executable)
        make_exe(py_executable)

    ...

Pour utiliser un environnement, il suffit d’ajouter le dossier env/bin dans le $PATH du système. Par defaut, l’exécutable python cherchera les dossiers lib et include parmi les dossier frères du dossier qui contiennent l’exécutable python. Si on regarde les scripts activate et desactivate, on verra qu’ils ne font que modifier les variables d’environnement et appeler l’exécutable python dans env\bin.

Pour aller plus loin

Pour ceux qui veulent aller plus loin, je vous recommande de regarder les scripts d’activation d’un environnement virtuel générés par venv dans env/bin/activate. Vous verrez qu’une fois que le bon arbre de dossiers est créée par venv il ne reste pas grand chose à faire pour activer un environnement virtuel.

Je vous conseille aussi de regarder des tutoriels officiels sur la création d’environnements virtuels et la gestion de dépendances en Python. Je vous recommande : Creating Virtual Environments et Managing Application Dependencies.

Une autre bonne référence est le texte de la PEP 405, spec officielle des environnements virtuels, qui explique les motivations derrière l’introduction du concept, et cela donne plus de détails sur les choix d’implémentation.

On peut aussi regarder la documentation officielle de venv, devenue standard depuis Python 3.6. En tant que module réutilisable, il permet l’automatisation de la création d’environnements virtuels.

Vous pouvez aussi regarder la documentation du module site pour voir comment Python trouve le dossier site-packages.

Pour voir des use case d’utilisation avancée des environnements virtuels, je vous conseille de regarder des outils comme tox, nox et cox qui font en sorte qu’on puisse tester un module dans différentes versions de Python.

Il peut être intéressant aussi de regarder des outils / approches similaires / concurrentes. Je vous conseille de regarder pyenv qui gère plusieurs installations de Python sur une seule machine. Il ne prend en compte que la gestion du PATH global, pas celle du PYTHONPATH, ni du PYTHONHOME. Il peut être aussi intéressant de regarder conda qui est un gestionnaire de packages qui en plus permet la création d’environnements virtuels. Pour un mécanisme simple de gestion de packages par utilisateur, vous pouvez regarder les user package. Finalement, pour une approche globale de gestion de dépendances dans un projet Python, vous pouvez regarder pipenv qui gère les environnements virtuels, les modules installés, et fait en sorte que les builds soient déterministes en créant un fichier lock (comme package.lock dans le monde npm) qui référence les versions de chaque module utilisées dans le projet.