Best practices for placing common enumeration/constants in a python package?

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

# first.py 

class UsageStatus(StrEnum):
    FAIL = auto()
    SUCCESS = auto()
    UNDEFINED = auto()

class FirstThingy(StrEnum):
   FOO = auto()
   BAR = auto()
   BAZ = auto()
   QUX = auto()

def somefunc(thingy: FirstThingy, status: UsageStatus):
    ...

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 have used a circular import where I had no other choice, but I do not recommend it.

I would go with the third solution:

2 Likes

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
2 Likes

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.

1 Like

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).

1 Like