Then MY_PATHS is the constant that I wanted, and has some nice properties:
I can use dataclasses.fields to iterate over the fields of the class.
Unlike a dict, I can access values cleanly with MY_PATHS.input_path with tab-completion in my editor instead of needing to guess at the dict key like MY_PATHS["input_path"].
Type checkers can understand and enforce all the type information involved.
But it would be even cleaner if I could just do something like
and have the result that MyPaths is a constant immutable object, allowing me to access MyPaths.inputs directly, and raising an error in case of any attempt to instantiate it or modify its attributes.
I’m not sure if a decorator is the right approach – a signature like class MyPaths(Constant) would be fine as well.
In this scenario I would just use a module of constants? What’s the class-like behavior that you’re using here?
A module has all the benefits and is simpler to use. You get type-checking, tab completion…If you want to iterate over values, you could just define that as another constant (fields = [inputs, outputs, .... etc])
A module of constants is a reasonable alternative. My main concerns with that approach:
It’s easy to add a new constant and then forget to add it to the corresponding fields = [... meta constant – ideally the relationship is programmatically enforced.
In cases where I have several related small groups of constants, the module-of-constants approach would involve creating several tiny modules, which can be a little harder to read/organize/document vs allowing them to all live in the same model, grouped instead under class definitions.
You could programmatically make this in a few ways. Probably the easiest is:
fields = [x for x in globals() if not x.startswith("_")]
This requires the discipline that anything NOT meant to be part of fields has a leading underscore (eg if you import any other modules), but otherwise is completely automatic.
Here is a constant decorator implementation that extends @nedbat’s solution. As shown in the demo,
type hints are now optional within the class declaration – sometimes the type is quite obvious both to the reader and the type checker, i.e. : Path = Path( is redundant.
__iter__ iterates over the name: value pairs, such that you can call dict directly on the class to get what you’d expect