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:

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:

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,

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