Constants namespace with dataclass-like features (original) (raw)
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 likeMY_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
jamestwebber (James Webber) May 10, 2025, 10:03pm 2
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?
zkurtz (Zach Kurtz) May 10, 2025, 10:15pm 3
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.
Rosuav (Chris Angelico) May 10, 2025, 10:19pm 4
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.
nedbat (Ned Batchelder) May 10, 2025, 11:13pm 5
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")
zkurtz (Zach Kurtz) May 11, 2025, 12:42am 7
Amazing – that looks good, thanks!
effigies (Chris Markiewicz) May 11, 2025, 1:42am 8
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.
zkurtz (Zach Kurtz) May 11, 2025, 2:25pm 9
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 thename: value
pairs, such that you can calldict
directly on the class to get what you’d expect
makukha (Michael Makukha) May 11, 2025, 9:15pm 10
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")