45

I'm working on a relatively large Python application, and there are several resources that I would like to keep as global variables accessible throughout several different modules. These values are things like the version number, version date, the global configuration, and some static paths to resources. I've also included a DEBUG flag that gets set by a command line option so that I can run my application in a debug mode without needing the full environment.

The values I'm importing I've been careful to ensure are ones that do not change over the course of running the program, and I've documented them as global constant variables that should not be touched. My code looks essentially like


# Main.py
import wx
from gui import Gui

DEBUG = False
GLOBAL_CONFIG = None
VERSION = '1.0'
ICON_PATH = 'some/path/to/the/app.ico'

def main():
    global DEBUG, GLOBAL_CONFIG

    # Simplified
    import sys
    DEBUG = '--debug' in sys.argv

    GLOBAL_CONFIG = load_global_config()
    # Other set-up for the application, e.g. setting up logging, configs, etc

    app = wx.App()
    gui = Gui()
    app.MainLoop()

if __name__ == '__main__':
    main()

# gui.py
import wx
from __main__ import DEBUG, GLOBAL_CONFIG, ICON_PATH

import controller


class Gui(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None)

        icon = wx.Icon(ICON_PATH, wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

        # Always make a copy so we don't accidentally modify it
        conf = GLOBAL_CONFIG.copy()
        self.controller = controller.Controller(conf)

        # More setup, building the layout, etc

# controller.py
from __main__ import DEBUG

import logging
log = logging.getLogger('controller')

class Controller(object):
    def __init__(self, conf):
        if DEBUG:
            log.info("Initializing controller in DEBUG mode")
        self.conf = conf
        # Other setup ...

This is obviously far stripped down from what my application actually is, and neglects error handling, documentation, and basically all implementation details.

Now, I've seen it said that this is a bad idea, but without explanation for why. Since most results when googling for variants of "python import __main__" are questions about what if __name__ == '__main__' is, it's hard to find some solid information on this topic. So far I've had no problems with it, and it's actually been really convenient.

So is this considered good Python practice, or is there a reason I should avoid this design?

Community
  • 1
  • 1
bheklilr
  • 53,530
  • 6
  • 107
  • 163
  • 1
    Would you consider a scenario where you could change your main entry point? If not, just reference the normal module, without using __main__. It also helps to track the origin of the code. – Luis Masuelli Jun 03 '14 at 19:44
  • @LuisMasuelli My main entry point will not change, but the problem is that some of the values don't get set until after `main()` is run in `if __name__ == '__main__'`, so those useful values aren't set if I were to just use `from Main import ...` and in my actual code some of these other files are in folders beneath the entry point, and I don't want `from ..Main import ...`. – bheklilr Jun 03 '14 at 19:47
  • @bheklilr, note that by asserting that your entry won't change, you're making your code extremely brittle against any attempts to refactor components into the library and the like. That alone is sufficient to call this bad practice. – Charles Duffy Jun 03 '14 at 20:18
  • @CharlesDuffy My application is being used for a very specific purpose, I know my requirements and _they will not change_. This isn't development for a website or some user oriented tool, it's an engineering application meant to run in on an assembly line. The only people who would be refactoring this code would be those on my team for a very specific purpose. Also, I'm not using the global variables in my libraries that are designed to be reused, only in the application specific code, i.e. the entry point, GUIs, and controllers. – bheklilr Jun 03 '14 at 20:39
  • the suggestion in these cases is: have those variables in a separate cfg file, imported both in the main and in the gui scripts. – Luis Masuelli Jun 03 '14 at 22:07

2 Answers2

41

I think there are two main (ha ha) reasons one might prescribe an avoidance of this pattern.

  • It obfuscates the origin of the variables you're importing.
  • It breaks (or at least it's tough to maintain) if your program has multiple entry points. Imagine if someone, very possibly you, wanted to extract some subset of your functionality into a standalone library--they'd have to delete or redefine every one of those orphaned references to make the thing usable outside of your application.

If you have total control over the application and there will never be another entry point or another use for your features, and you're sure you don't mind the ambiguity, I don't think there's any objective reason why the from __main__ import foo pattern is bad. I don't like it personally, but again, it's basically for the two reasons above.


I think a more robust/developer-friendly solution may be something like this, creating a special module specifically for holding these super-global variables. You can then import the module and refer to module.VAR anytime you need the setting. Essentially, just creating a special module namespace in which to store super-global runtime configuration.

# conf.py (for example)
# This module holds all the "super-global" stuff.
def init(args):
    global DEBUG
    DEBUG = '--debug' in args
    # set up other global vars here.

You would then use it more like this:

# main.py
import conf
import app

if __name__ == '__main__':
    import sys
    conf.init(sys.argv[1:])

    app.run()

# app.py
import conf

def run():
    if conf.DEBUG:
        print('debug is on')

Note the use of conf.DEBUG rather than from conf import DEBUG. This construction means that you can alter the variable during the life of the program, and have that change reflected elsewhere (assuming a single thread/process, obviously).


Another upside is that this is a fairly common pattern, so other developers will readily recognize it. It's easily comparable to the settings.py file used by various popular apps (e.g. django), though I avoided that particular name because settings.py is conventionally a bunch of static objects, not a namespace for runtime parameters. Other good names for the configuration namespace module described above might be runtime or params, for example.

mathause
  • 1,607
  • 1
  • 16
  • 24
Henry Keiter
  • 16,863
  • 7
  • 51
  • 80
  • 1
    Notably, the special-purpose module approach you're suggesting here has some popularity, as in the Django world, so chances are good of folks seeing that being, perhaps, somewhat less surprised. – Charles Duffy Jun 03 '14 at 20:16
  • @CharlesDuffy Was just in the middle of adding a mention of that fact :-) – Henry Keiter Jun 03 '14 at 20:16
  • 1
    @HenryKeiter I like this suggestion, and it would get around the import order issue brought up by chepner. I've used a similar approach in another module of this project, but it never crossed my mind to do the same with my global configuration. – bheklilr Jun 03 '14 at 20:41
  • @bheklilr Indeed it does. It also allows for modification during the life of the program (assuming a single thread/process, of course), and eliminates the dangerous-looking cyclical import in the original code. – Henry Keiter Jun 03 '14 at 21:42
  • 1
    @HenryKeiter I wanted to come back to say that this solution has worked out quite well for me. I have made a small modification to this design by using a singleton class defined and instantiated in my equivalent of `settings.py` that way someone can't do `from settings import VAR`, and instead have `from settings import CONF` and then reference `CONF.VAR`. Basically I've forced myself to have to refer to the variable itself so updates propagate. – bheklilr Jan 12 '16 at 14:41
  • @HenryKeiter. What if the process I need HAS TO run in main? What I need is a multiprocessing fork object which has to run in main (because of Windows) and I need to import it in another module. My program is also rather large as the one of this question. My original (unanswered question is here [link](http://stackoverflow.com/questions/42602584/how-to-use-multiprocessing-pool-in-an-imported-module?noredirect=1#comment72337652_42602584)) – B Furtado Mar 05 '17 at 13:24
8

Doing so requires violating PEP8, which specifies

Imports are always put at the top of the file, just after any module comments and docstrings, and before module globals and constants.

In order for gui.py to successfully import __main__.DEBUG, you would have to set the value of DEBUG before import gui.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • 1
    The import order is definitely a problem, and I do have to work around it by breaking PEP8. I think @HenryKeiter's solution provides a good workaround that allows me to still keep all the behavior I want without having as brittle of a structure. Thanks for your input, this is certainly a valid reason why this shouldn't be done. – bheklilr Jun 03 '14 at 20:48