Generally, modules should not have side effects. However, in a lot of cases, the side-effects are hard to avoid or may be desirable. There are also popular packages with on-import side-effects.
Which side effects, and when, are allowable when developing a package? For instance, these seem on the border of acceptable and not-acceptable:
# Creates a logfile
logging.basicConfig(filename="module.log")
Write output to a logfile
logging.info("Module initialized successfully.")
Add to PYTHONPATH to find a required module
sys.path.append(PLUGIN_DIRECTORY)
import my_plugin
Replace a function with a patched version
(from future import ... does similar)
@functools.wraps(math.cos)
def _polyfilled_cosine(theta):
return math.sin(theta + math.pi / 2)
math.cos = _polyfilled_cosine
Register a class or method with some dispatcher
(Often this is done with a decorator or metaclass,
i.e. Flask's @app.route(): https://flask.palletsprojects.com/en/2.3.x/patterns/packages/)
class PngHandler(filetype_registry.GenericHandler):
pass
filetype_registry.register(PngHandler, '.png')
- For logfiles, it's more effort to defer their creation until the first write than it is to simply create one, but if you create a logfile it may be created when code is imported, and then never run.
- PYTHONPATH manipulation is often something to be avoided, but I've encountered many modules that add a subdirectory or a plugin path to PATH.
- Monkey patching has its issues, but when the situation calls for it I'm not sure
import monkeypatch; monkeypatch.apply_patch()is any better. - Registering classes in some way (like Flask does) seems very common, but it means that importing a package can have dramatic side effects.
In practice, it seems like many libraries have an object (potentially a singleton) whose constructor handles the initialization, so instead of my_library.do_thing() the semantics are ml = my_library.MyLib(); ml.do_thing(). Is this a better design pattern or does it just kick the issue down the road?