python module import – relative paths issue

My Python development workflow

This is a basic process to develop Python packages that incorporates what I believe to be the best practices in the community. It’s basic – if you’re really serious about developing Python packages, there still a bit more to it, and everyone has their own preferences, but it should serve as a template to get started and then learn more about the pieces involved. The basic steps are:

  • Use virtualenv for isolation
  • setuptools for creating a installable package and manage dependencies
  • python setup.py develop to install that package in development mode

virtualenv

First, I would recommend using virtualenv to get an isolated environment to develop your package(s) in. During development, you will need to install, upgrade, downgrade and uninstall dependencies of your package, and you don’t want

  • your development dependencies to pollute your system-wide site-packages
  • your system-wide site-packages to influence your development environment
  • version conflicts

Polluting your system-wide site-packages is bad, because any package you install there will be available to all Python applications you installed that use the system Python, even though you just needed that dependency for your small project. And it was just installed in a new version that overrode the one in the system wide site-packages, and is incompatible with ${important_app} that depends on it. You get the idea.

Having your system wide site-packages influence your development environment is bad, because maybe your project depends on a module you already got in the system Python’s site-packages. So you forget to properly declare that your project depends on that module, but everything works because it’s always there on your local development box. Until you release your package and people try to install it, or push it to production, etc… Developing in a clean environment forces you to properly declare your dependencies.

So, a virtualenv is an isolated environment with its own Python interpreter and module search path. It’s based on a Python installation you previously installed, but isolated from it.

To create a virtualenv, install the virtualenv package by installing it to your system wide Python using easy_install or pip:

sudo pip install virtualenv

Notice this will be the only time you install something as root (using sudo), into your global site-packages. Everything after this will happen inside the virtualenv you’re about to create.

Now create a virtualenv for developing your package:

cd ~/pyprojects
virtualenv --no-site-packages foobar-env

This will create a directory tree ~/pyprojects/foobar-env, which is your virtualenv.

To activate the virtualenv, cd into it and source the bin/activate script:

~/pyprojects $ cd foobar-env/
~/pyprojects/foobar-env $ . bin/activate
(foobar-env) ~/pyprojects/foobar-env $

Note the leading dot ., that’s shorthand for the source shell command. Also note how the prompt changes: (foobar-env) means your inside the activated virtualenv (and always will need to be for the isolation to work). So activate your env every time you open a new terminal tab or SSH session etc..

If you now run python in that activated env, it will actually use ~/pyprojects/foobar-env/bin/python as the interpreter, with its own site-packages and isolated module search path.

A setuptools package

Now for creating your package. Basically you’ll want a setuptools package with a setup.py to properly declare your package’s metadata and dependencies. You can do this on your own by by following the setuptools documentation, or create a package skeletion using Paster templates. To use Paster templates, install PasteScript into your virtualenv:

pip install PasteScript

Let’s create a source directory for our new package to keep things organized (maybe you’ll want to split up your project into several packages, or later use dependencies from source):

mkdir src
cd src/

Now for creating your package, do

paster create -t basic_package foobar

and answer all the questions in the interactive interface. Most are optional and can simply be left at the default by pressing ENTER.

This will create a package (or more precisely, a setuptools distribution) called foobar. This is the name that

  • people will use to install your package using easy_install or pip install foobar
  • the name other packages will use to depend on yours in setup.py
  • what it will be called on PyPi

Inside, you almost always create a Python package (as in “a directory with an __init__.py) that’s called the same. That’s not required, the name of the top level Python package can be any valid package name, but it’s a common convention to name it the same as the distribution. And that’s why it’s important, but not always easy, to keep the two apart. Because the top level python package name is what

  • people (or you) will use to import your package using import foobar or from foobar import baz

So if you used the paster template, it will already have created that directory for you:

cd foobar/foobar/

Now create your code:

vim models.py

models.py

class Page(object):
    """A dumb object wrapping a webpage.
    """

    def __init__(self, content, url):
        self.content = content
        self.original_url = url

    def __repr__(self):
        return "<Page retrieved from '%s' (%s bytes)>" % (self.original_url, len(self.content))

And a client.py in the same directory that uses models.py:

client.py

import requests
from foobar.models import Page

url="http://www.stackoverflow.com"

response = requests.get(url)
page = Page(response.content, url)

print page

Declare the dependency on the requests module in setup.py:

  install_requires=[
      # -*- Extra requirements: -*-
      'setuptools',
      'requests',
  ],

Version control

src/foobar/ is the directory you’ll now want to put under version control:

cd src/foobar/
git init
vim .gitignore

.gitignore

*.egg-info
*.py[co]
git add .
git commit -m 'Create initial package structure.

Installing your package as a development egg

Now it’s time to install your package in development mode:

python setup.py develop

This will install the requests dependency and your package as a development egg. So it’s linked into your virtualenv’s site-packages, but still lives at src/foobar where you can make changes and have them be immediately active in the virtualenv without re-installing your package.

Now for your original question, importing using relative paths: My advice is, don’t do it. Now that you’ve got a proper setuptools package, that’s installed and importable, your current working directory shouldn’t matter any more. Just do from foobar.models import Page or similar, declaring the fully qualified name where that object lives. That makes your source code much more readable and discoverable, for yourself and other people that read your code.

You can now run your code by doing python client.py from anywhere inside your activated virtualenv. python src/foobar/foobar/client.py works just as fine, your package is properly installed and your working directory doesn’t matter any more.

If you want to go one step further, you can even create a setuptools entry point for your CLI scripts. This will create a bin/something script in your virtualenv that you can run from the shell.

setuptools console_scripts entry point

setup.py

  entry_points=""'
  # -*- Entry points: -*-    
  [console_scripts]
  run-fooobar = foobar.main:run_foobar
  ''',

client.py

def run_client():
    # ...

main.py

from foobar.client import run_client

def run_foobar():
    run_client()

Re-install your package to activate the entry point:

python setup.py develop

And there you go, bin/run-foo.

Once you (or someone else) installs your package for real, outside the virtualenv, the entry point will be in /usr/local/bin/run-foo or somewhere simiar, where it will automatically be in $PATH.

Further steps

Suggested reading:

Leave a Comment