Grouping constants in python

Question:

This is mainly a “good python style” question.

I have a module which is using a number of constants that feels should be grouped.

Lets say we have Dogs and cat and each of them have number of legs and favorite food.

Note that

  1. we want to model nothing but those constants about Dogs and Cats
  2. quite probably we’ll have more animals in the future.
  3. those constants wont be used outside of the current module.

I thought about the following solutions:


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 really grouped, but I think it is the solution I prefer.


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 solution as we’ll have class that we wont use and they can be actually instantiated.


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 dont really want to expose those internal constants


what is the most pythonic way to do it / how would you do it?

Asked By: Mario Corchero

||

Answers:

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.

Answered By: Davide

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.

Answered By: bruno desthuilliers

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
Answered By: jonrsharpe

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)
Answered By: Mic

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
Answered By: hiro protagonist