Let’s say you are writing a python package. That package contains
__init__.py
first.py
second.py
third.py
fourth.py
You want to enumerate clearly all options for different arguments and want that to be statically type checked. One possibility for this is to use string enumerations
Since FirstThingy has name which resembles “first”, it feels natural to put the enum definition to first.py. Your project grows and you have lot of different constants defined in lot of modules. All goes well until you get ImportErrors since first.py imports Second from second.py, which imports Third from third.py which imports Fourth from fourth.py which imports FirstThingy from first.py. The FirstThingy constant/enum definition is needed in fourth.py also.
I’m tempted to create separate constants.py and just dump all the StrEnums there as this will eliminate all possibilities for import loops as constants.py will never need anything from anywhere. The second approach would be to put the violating import statements somewhere out from module-level, inside a def or class definition, but that feels a bit dirty. Third approach would be to put all “shared” (=needed in more than one file) constants in constants.py and keep the rest in their respective modules.
What is the best practice for handle this kind of situation and how do you tend to solve this kind of problems?
I would advise to get to the bottom of what causes the circular imports, and break (get rid of) the mutual dependencies that cause them. The mutual dependency is almost always caused by flawed design.
If module A depends on B and B on A, and if that dependency is pervasive (occurs in lots of places) then the modules need to be merged into one
If A only depends on B inside a particular function, and only depends on particular other parts of B, you could also consider make the import inline (not at global level); that is not ideal but could work
A separate constants.py is a fairly common strategy. It would be fast to import (all it has is some little enums, no major work) and can be a dependency of everything else, but has no dependencies of its own. Yes, “First” is associated with first.py, but “constants” applies to the entire package.
from ... import ...
is the usual way to avoid circular dependencies (at the module level)
The constants.py is a solid approach because similar to the env.py/environment.py tradition, it is setting-up project/library globals.
Per @hansgeunsmeyer recommend stepping-back to take a look at the structure of the data, where it comes-from (unless global) and where it is actually needed (rather than import-ing everything in-case it is needed).
Not to be a pain in the ass, but, why are we using Enums to hold constants that will most likely never use the Enum functionality? I’ve recently started using just normal classes for 95% of constants and it seems to save an awful lot .value, but maybe I’m missing something. I think were missing something like “class for constants” which its purpose would only be to add a namespace for constants and maybe make them pseudo-immutable and thats it, no more functionality. And yes, I know named tuple, just use a dict, but for everyone who uses VScode or PyCharm, it is way more comfortable to use classes.
If your constants are not actually related to each other (in particular, alternate legal options for a particular value elsewhere) then sure, go ahead and represent them in whatever way makes sense. However, I think the previous discussion was really more about the organization of the code into files.