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?
# (from __future__ import ... does similar)
". This is inaccurate.__future__
import effects are scoped to the module that performed the import from__future__
, your monkey-patching ofmath
affects everyone who importsmath
(aside from the occasional module that was loaded first and imported withfrom math import cos
). Module-global affects are often okay (though even then, don't confuse your maintainers), but modifying (as opposed to adding to) program-global state is basically never acceptable.basicConfig
, for example, should never be called from a module. It's up to the script/application to configure logging; modules simply expose loggers to be configured.