Python Entry Points Explained

In this post I’m going to explain about entry points in Python. Most people know entry points as the little snippet that you put in your setup.py file to make your package available as a script on the command line, but they can be used for so much more. I’m going to show you how to use entry points as a modular plug-in architecture. This is super useful if you want to let other people write Python packages that interact or add abilities to your existing Python package at runtime.

!! WARNING: TERRIBLE HUMOR AHEAD !!

Snek, Inc.

Congratulations! You have just been appointed CEO of “Snek Semiconductors and Software, Incorporated”. Your first job as CEO is, obviously, to instruct your R&D department to develop a snek prototype ASAP. And so, they get to work, writing snek.py:

ascii_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

def main():
    print(ascii_snek)
    
if __name__ == '__main__':
    main()

Their prototype was displayed at the company demo day, and it worked!

$ python snek.py
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

SaaS - Snek as a Service

Unfortunately, customers don’t know about Python (except for the snek). They want easy access to snek from any path in the shell without needing to worry about this Python thing, or finding snek.py. So the gals over at R&D worked all night and came up with a way to package snek in a way that automagically creates a console script when it’s installed. When you create a distribution for a Python package, you need to have a setup.py file that contains the package’s name, dependencies, etc. It can also be used to register entry points and it looks like this:

from setuptools import setup

setup(
    name='snek',
    entry_points={
        'console_scripts': [
            'snek = snek:main',
        ],
    }
)

console_scripts, they say, is a special type of entry point. setuptools reads its content as "<console script name> = <python object path>" and creates an appropriate script when your package is installed. Right now, they are installing it from source:

$ python setup.py develop

running develop
running egg_info
writing snek.egg-info\PKG-INFO
writing dependency_links to snek.egg-info\dependency_links.txt
writing entry points to snek.egg-info\entry_points.txt
writing top-level names to snek.egg-info\top_level.txt
reading manifest file 'snek.egg-info\SOURCES.txt'
writing manifest file 'snek.egg-info\SOURCES.txt'
running build_ext
Creating c:\program files (x86)\py36-32\lib\site-packages\snek.egg-link (link to .)
snek 0.0.0 is already the active version in easy-install.pth
Installing snek-script.py script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe.manifest script to C:\Program Files (x86)\Py36-32\Scripts

Installed c:\users\rachum\notebooks
Processing dependencies for snek==0.0.0
Finished processing dependencies for snek==0.0.0

At the keynote talk in the highly-acclaimed SnekCon conference (tickets sold out six months in advance) you take the stage and present it for the world to marvel at:

$ snek
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

Snek for Everyone

Everyone loves snek. “Snek Semiconductors and Software, Incorporated” has an IPO and is valued at over 60 billion dollars. Hipsters are demanding a fancier version of snek. And your R&D team delivers:

"""Print an ASCII Snek.

Usage:
    snek [--type=TYPE]
    
"""
import docopt

normal_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

fancy_snek = """\
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'
"""

def get_sneks():
    return {
        'normal': normal_snek,
        'fancy': fancy_snek,
    }


def main():
    args = docopt.docopt(__doc__)
    snek_type = args['--type'] or 'normal'
    print(get_sneks()[snek_type])
    
if __name__ == '__main__':
    main()

They’ve added a fancy snek. Now the hipsters are happy.

$ snek
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

$ snek --type fancy
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'

Snek International Community

Millions all over the world are using snek. Even after acquihiring Google, “Snek Semiconductors and Software, Incorporated” just can’t keep up with the increasing demand for more and more versions of snek! Professional customers are demanding a customizable version of snek! They want to create their own snek on top of the snek infrastructure. R&D better get to work.

"""Print an ASCII Snek.

Usage:
    snek [--type=TYPE]
    
"""
import docopt
import pkg_resources

normal_snek = """\
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`
"""

fancy_snek = """\
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'
"""

def get_sneks():
    sneks = {
        'normal': normal_snek,
        'fancy': fancy_snek,
    }
    for entry_point in pkg_resources.iter_entry_points('snek_types'):
        sneks[entry_point.name] = entry_point.load()
    return sneks


def main():
    args = docopt.docopt(__doc__)
    snek_type = args['--type'] or 'normal'
    print(get_sneks()[snek_type])
    
if __name__ == '__main__':
    main()

They have added the infrastructure for customizable sneks. Whenever snek is run, it looks for other sneks that registered using an entry point called snek_types. Each snek is a string registered under a type name. That name can be used with the console script to dynamically print a custom snek.

In particular, the magic happens in get_sneks. The call to pkg_resources.iter_entry_points('snek_types') iterates over all the entry points that were registered under the name "snek_types". So, external packages can define an entry point called "snek_types" in their setup.py, and snek will dynamically load it at runtime.

The guys over at “Snek Pro Solutions and Consultation Services” have been notified about the "snek_types" entry point and have started working on a far better version of snek than you could dream of. A cute snek. They have created a simple cute_snek.py with very little code:

cute_snek = r"""
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~ 
                        ~--______-~                ~-___-~
"""

They are packaging cute_snek, but they also need to let snek know how to find the cute snek.

from setuptools import setup

setup(
    name='cute_snek',
    entry_points={
        'snek_types': [
            'cute = cute_snek:cute_snek',
        ],
    }
)

They registered the cute_snek variable in the cute_snek module under the name cute. Now, they install both snek and cute_snek

$ cd cute_snek && python setup.py develop
running develop
running egg_info
writing cute_snek.egg-info\PKG-INFO
writing dependency_links to cute_snek.egg-info\dependency_links.txt
writing entry points to cute_snek.egg-info\entry_points.txt
writing top-level names to cute_snek.egg-info\top_level.txt
reading manifest file 'cute_snek.egg-info\SOURCES.txt'
writing manifest file 'cute_snek.egg-info\SOURCES.txt'
running build_ext
Creating c:\program files (x86)\py36-32\lib\site-packages\cute-snek.egg-link (link to .)
cute-snek 0.0.0 is already the active version in easy-install.pth

Installed c:\users\rachum\cute_snek
Processing dependencies for cute-snek==0.0.0
Finished processing dependencies for cute-snek==0.0.0

Now, running snek, they can produce a cute snek, which is dynamically loaded from the cute_snek package:

$ snek --type cute
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~ 
                        ~--______-~                ~-___-~

Snek Infrastructure Overhaul

Your VP R&D has a request. He want to stop development for a while, while his engineers clean up the snek infrastructure for a better looking code base. You are too distracted by the public allegations that you embezzled sneks while doing insider-trading in the snek market, so you allow it.

The engineers in R&D have come to the conclusion that if some sneks can be dynamically loaded, all sneks can be dynamically loaded! Even built-in sneks! They removed special treatment of existing sneks:

--- a/snek.py
+++ b/snek.py
@@ -31,10 +31,7 @@ fancy_snek = """\
 """

 def get_sneks():
-    sneks = {
-        'normal': normal_snek,
-        'fancy': fancy_snek,
-    }
+    sneks = {}
     for entry_point in pkg_resources.iter_entry_points('snek_types'):
         sneks[entry_point.name] = entry_point.load()
     return sneks

And, instead, registered them like any other snek:

--- a/setup.py
+++ b/setup.py
@@ -6,5 +6,9 @@ setup(
         'console_scripts': [
             'snek = snek:main',
        ],
+       'snek_types': [
+           'normal = snek:normal_snek',
+           'fancy = snek:fancy_snek',
+       ],
     },
 )

Now, you need to re-install snek for the new entry points to take:

$ python setup.py develop
running develop
running egg_info
writing snek.egg-info\PKG-INFO
writing dependency_links to snek.egg-info\dependency_links.txt
writing entry points to snek.egg-info\entry_points.txt
writing top-level names to snek.egg-info\top_level.txt
reading manifest file 'snek.egg-info\SOURCES.txt'
writing manifest file 'snek.egg-info\SOURCES.txt'
running build_ext
Creating c:\program files (x86)\py36-32\lib\site-packages\snek.egg-link (link to .)
snek 0.0.0 is already the active version in easy-install.pth
Installing snek-script.py script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe script to C:\Program Files (x86)\Py36-32\Scripts
Installing snek.exe.manifest script to C:\Program Files (x86)\Py36-32\Scripts

Installed c:\users\rachum\notebooks
Processing dependencies for snek==0.0.0
Finished processing dependencies for snek==0.0.0
$ snek
    --..,_                     _,.--.
       `'.'.                .'`__ o  `;__.
          '.'.            .'.'`  '---'`  `
            '.`'--....--'`.'
              `'--....--'`

$ snek --type fancy
                          _,..,,,_
                     '``````^~"-,_`"-,_
       .-~c~-.                    `~:. ^-.
   `~~~-.c    ;                      `:.  `-,     _.-~~^^~:.
         `.   ;      _,--~~~~-._       `:.   ~. .~          `.
          .` ;'   .:`           `:       `:.   `    _.:-,.    `.
        .' .:   :'    _.-~^~-.    `.       `..'   .:      `.    '
       :  .' _:'   .-'        `.    :.     .:   .'`.        :    ;
       :  `-'   .:'             `.    `^~~^`   .:.  `.      ;    ;
        `-.__,-~                  ~-.        ,' ':    '.__.`    :'
                                     ~--..--'     ':.         .:'
                                                     ':..___.:'

$ snek --type cute
                    /^\/^\
                  _|__|  O|
         \/     /~     \_/ \
          \____|__________/  \
                 \_______      \
                         `\     \                 \
                           |     |                  \
                          /      /                    \
                         /     /                       \
                       /      /                         \ \
                      /     /                            \  \
                    /     /             _----_            \   \
                   /     /           _-~      ~-_         |   |
                  (      (        _-~    _--_    ~-_     _/   |
                   \      ~-____-~    _-~    ~-_    ~-_-~    /
                     ~-_           _-~          ~-_       _-~ 
                        ~--______-~                ~-___-~

This is indeed one small snek for snek, and one giant snek for snek kind (also, you know how to use Python entry points).

Discuss this post at the comment section below.
Follow me on Twitter and Facebook

Similar Posts