Constants namespace with dataclass-like features

Sometimes I want class-like dataclass-like behavior on a collection of constants, so I do something like

@dataclass(frozen=True, init=False)
class _MyPaths:
    inputs: Path = Path("input/path")
    outputs: Path = Path("output/path")

MY_PATHS = _MyPaths()

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

@constant
class MyPaths:
    inputs: Path = Path("input/path")
    outputs: Path = Path("output/path")

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.

Related: Typing rules for SimpleNamespace - #3 by alippai

1 Like

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

What am I missing?

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.
1 Like

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.

You can define that decorator yourself:

def constant(cls):
    return dataclass(frozen=True, init=False)(cls)()

Now this does just what you want:

@constant
class MyPaths:
    inputs: Path = Path("input/path")
    outputs: Path = Path("output/path")
10 Likes

Amazing – that looks good, thanks!

Just want to link @invoke built-in Decorator for Immediately Invoked Function (IIF) Ability, which was basically the same thing.

@operator.call came up, so you can also do:

@operator.call
@dataclass(frozen=True, init=False)
class MY_PATHS:
    inputs: Path = Path("input/path")
    outputs: Path = Path("output/path")

Which does the same thing as Ned’s decorator, if you’re not into the whole brevity thing.

2 Likes

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

There’s also an option to use typing.NamedTuple, it is immutable:

from operator import call as const
from typing import NamedTuple

@const
class MyPaths(NamedTuple):
    inputs: Path = Path("input/path")
    outputs: Path = Path("output/path")
4 Likes

Excuse me, but why not an Enum?

2 Likes

My main issues with enums:

  1. I need to type MyEnum.ATTRIBUTE_NAME.value to access the value of an attribute, where I’d rather have direct access like MyEnum.ATTRIBUTE_NAME as in other objects.

  2. Type checkers are typically blind to the type of the value MyEnum.ATTRIBUTE_NAME.value, at least when the values are set to arbitrary mixed types:

    class MixedEnum(Enum):
    “““Type checker won’t know the type .value items.”””
    STRING_VALUE = “hello”
    INT_VALUE = 42

1 Like

Do you feel like this should become part of the language soon, or should it this issue be marked as resolved (I’d move it to Python Help and mark it as solved)?

The workarounds proposed in this thread are helpful and enabled me to draft a prototype that seems cleaner than any other existing option. Of course my implementation is not at battle-tested, and could be made even cleaner if added to the language natively. So I’d love that, but I can see that’s unlikely to be a community priority soon. So feel free to mark this as resolved – thanks for asking!

1 Like