Python import - The law of Demeter

The Demeter Law

Python import can be a little tricky. The law of Demeter guideline and his applications to python import can help you mastering them and also writes more user-friendly, testable and reliable code.

According to wikipedia, the law of Demeter (or principle of least knowledge) is a software developing guideline that can be summarized this way:

  • Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
  • Each unit should only talk to its friends; don't talk to strangers.
  • Only talk to your immediate friends.

If we want to apply these to python import (the python module would be the unit), it would become something like:

  • Each module should only import public object of submodule, but only from the closest submodule. (aka "immediate friends")

The method

In order to accomplish that, the key is to declare explicitly in each module (though the _init_.py) which object are exposed. Then these objects are accessible to the parent module folder, which can also re-expose them if necessary.

The benefices of this method are:

  • Your _init_.py files become some kind of "documentation" of which objects should be accessed or not from other modules.

  • You fully control your import system, which is a key to loose-coupling.

  • The final user (other developper using your module) experience is enhanced, import are short and clear.

A downside is that it is more verbose.

First example

Assuming this setup:

project
│   __main__.py
└───demeter
│   │   __init__.py
│   └───module1
│       │   __init__.py
│       │   code1.py
# demeter/module1/code1.py

class MyClass1:
    pass

With that setup, we can access directly MyClass1 class from _main_.py:

# __main__.py

from demeter.module1.code1 import MyClass1

class1 = MyClass1()

This works, however this is a demeter's law violation: _main_.py import MyClass1 from demeter/module1/code1.py which is not an "immediate friend".

A good smell to detect it are long import. When applying the law of demeter, import are usually pretty short.

A cleaner way to do that would be...

  1. Explicitly expose MyClass1 from module1 to upper level module:
# demeter/module1/__init__.py

from demeter.module1 import code1 as code1

MyClass1 = code1.MyClass1
  1. Re-expose it to the upper level:
# demeter/__init__.py

from demeter import module1

MyClass1 = module1.MyClass1
  1. Use it
# __main__.py

from demeter import module1

MyClass1 = module1.MyClass1

That way:

  • _main_.py only have knowledge of the demeter module and the demeter module only have knowledge about the module1 module. The principe of least knowledge is respected
  • modules (demeter and module1) explicitly expose what can be used from them(explicit is better than implicit, https://www.python.org/dev/peps/pep-0020/).

Second example

Assuming this extension of the previous setup:

project
│   __main__.py
└───demeter
│   │   __init__.py
│   └───module1
│       │   __init__.py
│       │   code1.py
|       └───module2
│       │   __init__.py
│       │   code2.py
# demeter/module1/module2/code2.py

class MyClass2:
    pass

To use MyClass2 in _main_.py we would...

Expose it to module1:

# demeter/module1/module2/__init__.py

from demeter.module1.module2 import code2 as code2

MyClass2 = code2.MyClass2

Re-expose it but this time from module1:

# demeter/module1/__init__.py

from demeter.module1 import code1 as code1
from demeter.module1 import module2 as module2

MyClass1 = code1.MyClass1
MyClass2 = module2.MyClass2

and...

# demeter/__init__.py

from demeter import module1

MyClass1 = module1.MyClass1
MyClass2 = module1.MyClass2

Finally, use it from your _main_.py:

# __main__.py

from demeter import module1

MyClass1 = module1.MyClass1
MyClass2 = module1.MyClass2

Whole module example

The law of demeter can also be applied to whole module.

The previous example could be something like:

# demeter/module1/code1.py

class MyClass1:
    pass
# demeter/module1/module2/code2.py

class MyClass2:
    pass
# demeter/module1/module2/__init__.py

from  demeter.module1.module2  import  code2  as  code2
# demeter/module1/__init__.py

from  demeter.module1  import  code1  as  code1
from  demeter.module1  import  module2  as  module2

MyClass1 = code1.MyClass1
# demeter/__init__.py

from  demeter  import  module1

MyClass1 = module1.MyClass1
# demeter/__main__.py
import  demeter

module1 = demeter.module1

MyClass1 = module1.MyClass1
MyClass2 = module1.module2.code2.MyClass2

This second method looks very similar to the bad practice example, isn't it?
However, the key points are that every desired module and object are explicitly exposed.

So you still benefice the full control of your module system and you respect the principle of least knowledge.

VSCode, Pylance, Pyrights

If you use VSCode and Pylance (which use Pyrights under the hood), this method will avoid you to have some reportUnknownMemberType errors

Conclusion

I hope you learned something reading this article (my first one uWu ^^).

It's up to you to choose if you prefer expose full module or only object. I guess it depends on the context, the project, the people etc....

Even if it look a little "too much" and require more code and more rigorous , these methods really enhance control and quality on the long run as well as deeply understanding (and thinking about ;) ) responsibility of every part of your code.

21