Python relative imports testing my sanity

98 Views Asked by At

EDIT: A circular dependency issue was the reason why from .. import auth didn't work, see my own answer below. Cheers to all.


I'm dabbling with relative imports in Python while reorganizing some code and what should have taken 10 minutes is now taking untold amounts of time. Here's my file structure:

project/
       /__init__.py
       /application.py
       /lib/
           /__init__.py
           /auth.py
           /utils.py
           /views/
                 /__init__.py
                 /login_view.py
                 /test.py

In login_view.py I have:

from . import auth

which yields: ImportError: cannot import name auth

However this works:

from ..auth import untoken, req_auth_or_error

Which I don't understand: auth is only one level above login_view, why the .. and not .?

This also fails with the same error:

from .. import auth

In test.py I tried the following:

import types
from .. import *

def imports():
    for name, val in globals().items():
        if isinstance(val, types.ModuleType):
            print 'imported: ', val.__name__

imports()

which results in:

imported:  lib.utils
imported:  lib.views
imported:  types

... I'm at a loss as to why lib.auth is not imported. What am I doing wrong?

I'm running the code as such (the abs. path is /scratch/project/):

$ cd project/
$ ./application.py
4

There are 4 best solutions below

0
On BEST ANSWER

The problem was a circular dependency one and not one of relative imports.

Indeed, .. is needed to load a module from the parent directory.

However somewhere in auth.py I had an import views.login_view. I believe that the from .. import auth in login_view.py tried to import the auth module as it was being loaded itself. This is probably why from ..auth import untoken works because those functions had already been loaded.

0
On

In Python a single dot in import represents the current package, NOT the current module.

Then every additional dot represents a new level of "parentness" (ok this word does not exist but I think it expresses well what I mean). So the behaviour you notice in your first case is just normal.

For your second case, I to reproduced your hierarchy with those contents:

application.py

lib.views import login_view

print("oh yeah")

login_view.py

lib.views import login_view

print("oh my god")

... and running application.py from the project directory does not cause any error. I get the following output:

$ python application.py 
oh my god
oh yeah

However, adding a from lib.views import test (using the exact same test.py content you provided) in the application.py gives me the following output showing that some lib modules are not loaded:

$ python application.py 
oh my god
imported:  types
imported:  lib.views
imported:  lib.auth
oh yeah

The lib.utils module is missing :/

What I think is that since from .. import * names nothing: no package, no module, no function or variable or anything, the import statement simply imports nothing. TH displayed modules/packages are only the one which have been loaded earlier.

That said, there is a way to force a "correct" behaviour by using the __all__ magic variable, which defines which modules are loaded when using the splat import, in the lib/__init__.py file:

__all__ = ['auth', 'views', 'utils']

After doing this, running again the application.py file shows the following:

$ python application.py 
oh my god
imported:  types
imported:  lib.auth
imported:  lib.views
imported:  lib.utils
oh yeah
1
On

In the first case, from . import auth the . means the package views. That is why Python can't find it.

But, to avoid those problems is recommended that you use absolute import instead of relative imports.

1
On

What if you went up 1 more directory when importing?

from ... import auth