Mario Corchero
Mario Corchero

Reputation: 5575

Grouping constants in Python

My module uses constants that I feel should be grouped. Dog and cat have a number of legs and favorite foods.

  1. I want to model nothing but those constants about dogs and cats.
  2. Likely I'll have more animals in the future.
  3. Those constants won't be used outside of the module.

I thought about:

  1. Constants at module level:

    DOG_NUMBER_OF_LEGS = 4
    DOG_FAVOURITE_FOOD = ["Socks", "Meat"]
    CAT_NUMBER_OF_LEGS = 4
    CAT_FAVOURITE_FOOD = ["Lasagna", "Fish"]
    

    They seem not grouped, but it is the solution I prefer.

  2. Classes as namespaces:

    class Dog(object):
      NUMBER_OF_LEGS = 4
      DOG_FAVOURITE_FOOD = ["Socks", "Meat"]
    class Cat(object):
      NUMBER_OF_LEGS = 4
      FAVOURITE_FOOD = ["Lasagna", "Fish"]
    

    I don't like this as they're classes I won't use but can be actually instantiated.

  3. Dictionary of constants:

    ANIMALS_CONFIG = {
       "DOG" : {
         "NUMBER_OF_LEGS" : 4,
         "FAVOURITE_FOOD" : ["Socks", "Meat"]
       },
       "CAT" : {
         "NUMBER_OF_LEGS" : 4,
         "FAVOURITE_FOOD" : ["Lasagna", "Fish"]
       }
    }
    

I also thought about adding submodules but I don't want to expose those internal constants. What is the most pythonic way or how would you do it?

Upvotes: 16

Views: 11238

Answers (6)

wu1meng2
wu1meng2

Reputation: 547

You can use enum.Enum to group a collection of constants. It has advantages over regular class, dataclass, or namedtuple:

  1. The value of enum items cannot be reassigned at runtime, which makes it truly constant;
  2. You cannot instantiate an object of an enum class, which means it behaves like a singleton.

Moreover, enum items are iterable (list) and hashable (in). You can access the actual constant values through the .value property, which is a bit verbose, though.

from enum import Enum

class DogConstant(Enum):
  NUMBER_OF_LEGS = 4
  FAVOURITE_FOOD = ["Socks", "Meat"]

class CatConstant(Enum):
  NUMBER_OF_LEGS = 4
  FAVOURITE_FOOD = ["Lasagna", "Fish"]

# List all constants
print(f"{list(DogConstant)}")

# Check if an enum item exists in the enum class
print(f"{'NUMBER_OF_LEGS' in DogConstant.__members__ = }")

# Get value of an enum item
print(f"{DogConstant.NUMBER_OF_LEGS.value = }")
print(f"{DogConstant.FAVOURITE_FOOD.value = }")
print(f"{CatConstant.FAVOURITE_FOOD.value = }")

prints

[<DogConstant.NUMBER_OF_LEGS: 4>, <DogConstant.FAVOURITE_FOOD: ['Socks', 'Meat']>]
'NUMBER_OF_LEGS' in DogConstant.__members__ = True
DogConstant.NUMBER_OF_LEGS.value = 4
DogConstant.FAVOURITE_FOOD.value = ['Socks', 'Meat']
CatConstant.FAVOURITE_FOOD.value = ['Lasagna', 'Fish']

Upvotes: 1

hiro protagonist
hiro protagonist

Reputation: 46901

in more recent versions (>= 3.7) i would suggest you use a dataclass for that and making it frozen. This not only prevents any of the members from being changed but it also allows the constants to be used as dictionary keys (as they become hashable).

and in python >= 3.10 you may even want to set slots=True which prevents the addition of new members.

this way you also get the typing right:

from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Animal:
    number_of_legs: int
    favourite_foods: tuple[str, ...]

DOG = Animal(number_of_legs=4, favourite_foods=('Socks', 'Meat'))
CAT = Animal(number_of_legs=4, favourite_foods=('Lasagna', 'Fish'))

print(CAT)                # Animal(number_of_legs=4, favourite_foods=('Lasagna', 'Fish'))
print(DOG.number_of_legs) # 4

Upvotes: 2

Mic
Mic

Reputation: 345

Another way of using NamedTuples:

If there are some set of constants and its always nice to group them. It will be even more nice, if we can access them on IDE as we type them. Dictionary is not a solution as IDE cannot show recommendations as you type.

from collections import namedtuple
    
_coverage_comments = namedtuple('COVERAGE_COMMENTS',['no_progress', 'coverage_improved'])
    
COVERAGE_COMMENTS = _coverage_comments(no_progress="No Progress", coverage_improved="Improved")
    
print(COVERAGE_COMMENTS.coverage_improved)

Upvotes: 0

jonrsharpe
jonrsharpe

Reputation: 122116

I would go for a fourth option, preferring a collections.namedtuple:

Animal = namedtuple('Animal', 'number_of_legs favourite_food')

You then create instances like:

DOG = Animal(4, ['Socks', 'Meat'])
CAT = Animal(4, ['Lasagna', 'Fish'])

and access the values externally as:

from animals import CAT

print CAT.number_of_legs

There's really no point having classes if you don't need to create any methods, and I think the form of access above is neater than e.g.:

from animals import animals

print animals['CAT']['number_of_legs']

namedtuples, like vanilla tuples, are immutable, too, so you can't accidentally reassign e.g. CAT.number_of_legs = 2somewhere.

Finally, the namedtuple is a lightweight data structure, which may be important if you're creating lots of animals:

>>> import sys
>>> sys.getsizeof({'number_of_legs': 4, 'favourite_food': ['Lasagna', 'Fish']})
140
>>> from collections import namedtuple
>>> Animal = namedtuple('Animal', 'number_of_legs favourite_food')
>>> sys.getsizeof(Animal(4, ['Lasagna', 'Fish']))
36

Upvotes: 12

bruno desthuilliers
bruno desthuilliers

Reputation: 77912

There's no one-size-fits-all answer, it really depends on how many constants you have to handle, how you use them, if having polymorphism dispatch makes sense or not and the phase of the moon.

Now in your example, since you have two or more sets of constants with a similar structure and meaning, I'd go for the "classes as namespaces" solution.

FWIW preventing a class from being instanciated is not that difficult:

>>> class Foo(object):
...    def __new__(cls):
...       return cls
... 
>>> Foo()
<class '__main__.Foo'>

But documentation should be enough - remember that your "constants" are only constants by convention anyway.

Upvotes: 4

Davide
Davide

Reputation: 321

I'd go for the dictionary approach, as it feels more readable and better organized, but I believe it's a matter of personal preference.

Using classes also doesn't seem like a bad idea if you use a class with only static variables and methods like this and you never instantiate them.

Upvotes: 1

Related Questions