Best practice for Python & Django constants

Question:

I have a Django model that relies on a tuple. I’m wondering what the best practice is for refering to constants within that tuple for my Django program. Here, for example, I’d like to specify “default=0” as something that is more readable and does not require commenting. Any suggestions?

Status = (
    (-1, 'Cancelled'),
    (0, 'Requires attention'),
    (1, 'Work in progress'),
    (2, 'Complete'),
)

class Task(models.Model):
    status = models.IntegerField(choices=Status, default=0) # Status is 'Requires attention' (0) by default.

EDIT:

If possible I’d like to avoid using a number altogether. Somehow using the string ‘Requires attention’ instead would be more readable.

Asked By: Dylan Klomparens

||

Answers:

You could use a dictionary for a small improvement in clarity:

Status = {
    -1: 'Cancelled',
    0: 'Requires attention',
    1: 'Work in progress',
    2: 'Complete',
}

class Task(models.Model):
    status = models.IntegerField(choices=Status.items(), default=Status[0])
Answered By: Platinum Azure

My approach:

class Task(models.Model):
    STATUSES = { 'cancelled': 'Cancelled',
                 'requires attention': 'Requires attention',
                 'work in progress': 'Work in progress',
                 'complete': 'Complete' }

    status = models.CharField(choices=STATUSES.items(), default='cancelled')

This allows you to write convenient expressions:

tasks = Task.objects.filter(status='complete')

Also, it allows you to not create unnecessary global variables.

If you really want to use integer field:

class Task(models.Model):

   class STATUS:
      CANCELED, ATTENTION, WIP, COMPLETE = range(-1, 3)
      choices = {
        CANCELED: 'Cancelled',
        ATTENTION: 'Requires attention',
        WIP: 'Work in progress',
        COMPLETE: 'Complete'
      }


   status = models.CharField(choices=STATUSES.choices.items(), default=STATUSES.CANCELED)

And:

tasks = Task.objects.filter(status=Task.STATUSES.COMPLETE)
Answered By: defuz
CANCELED, ATTENTION, WIP, COMPLETE = range(-1, 3)
Status = (
    (CANCELED, 'Cancelled'),
    (ATTENTION, 'Requires attention'),
    (WIP, 'Work in progress'),
    (COMPLETE, 'Complete'),
)

class Task(models.Model):
    status = models.IntegerField(choices=Status, default=CANCELED)

Keep in mind that as others noted, the proper way is to put these variables
inside your Model class. That’s also how the official django example does it.

There is only one reason where you’d want to put it outside the class namespace
and that is only if these semantics are equally shared by other models of your app. i.e.
you can’t decide in which specific model they belong.

Though it doesn’t seem like this is the case in your particular example.

Answered By: rantanplan

It is quite common to define constants for the integer values as follows:

class Task(models.Model):
    CANCELLED = -1
    REQUIRES_ATTENTION = 0
    WORK_IN_PROGRESS = 1
    COMPLETE = 2

    Status = (
        (CANCELLED, 'Cancelled'),
        (REQUIRES_ATTENTION, 'Requires attention'),
        (WORK_IN_PROGRESS, 'Work in progress'),
        (COMPLETE, 'Complete'),
    )

    status = models.IntegerField(choices=Status, default=REQUIRES_ATTENTION)

By moving the constants and Status inside the model class, you keep the module’s namespace cleaner, and as a bonus you can refer to Task.COMPLETE wherever you import the Task model.

Answered By: Alasdair

You could use a namedtuple, using an Immutable for a constant seems fitting. 😉

>>> from collections import namedtuple
>>> Status = namedtuple('Status', ['CANCELLED', 'REQUIRES_ATTENTION', 'WORK_IN_PROGRESS', 'COMPLETE'])(*range(-1, 3))
>>> Status
Status(CANCELLED=-1, REQUIRES_ATTENTION=0, WORK_IN_PROGRESS=1, COMPLETE=2)
>>> Status.CANCELLED
-1
>>> Status[0]
-1

Using attributes on Task as constants like in Alasdair’s answer makes more sense in this case, but namedtuples are very cheap substitutes for dicts and objects that don’t change. Especially very handy if you want to have lots of them in memory. They are like regular tuples with a bonus of a descriptive __repr__ and attribute access.

Answered By: Chris Wesseling

I don’t use Django, but I do something like the following quite a bit under Pyramid and Twisted …

def setup_mapping( pairs ):
    mapping = {'id':{},'name':{}}
    for (k,v) in pairs:
        mapping['id'][k]= v
        mapping['name'][v]= k
    return mapping

class ConstantsObject(object):
    _pairs= None
    mapping= None

    @classmethod
    def lookup_id( cls , id ):
       pass

    @classmethod
    def lookup_name( cls , name ):
       pass

class StatusConstants(ConstantsObject):
    CANCELLED = -1
    REQUIRES_ATTENTION = 0
    WORK_IN_PROGRESS = 1
    COMPLETE = 2

    _pairs= (
        (-1, 'Cancelled'),
        (0, 'Requires attention'),
        (1, 'Work in progress'),
        (2, 'Complete'),
    )
    mapping= setup_mapping(_pairs)

So the essence is this:

  • There is a base “constants” class , and another class for each type. the class defines the keywords to a value in ALLCAPS
  • I toss in the plaintext _pairs into the class too. why? because i might need to build out some DB tables with them, or I might want them for error/status messages. I use the numbers and not the ALLCAPS variable name as a personal preference.
  • i initialize a mapping class variable which basically monkeypatches the class by precompiling a bunch of variables within a dict because…
  • the class is derived from that base class, which offers classmethod functionality to search for a value or do other standard things you often need to do with constants.

It’s not a one-size-fits-all approach, but I’ve generally come to really like this. You can easily use a dict to define the pairs , have the ‘mapping’ function setup some other attributes, such as giving you tuples of the pair values as k,v or v,k or any weird format you might need.

my code can then looks like this:

status_id = sa.Column(sa.Integer, sa.ForeignKey("_status.id") , nullable=False , default=constants.StatusConstants.CANCELLED )

status_name = constants.StatusConstants.lookup_id(status_id)    
status_name = constants.StatusConstants.mapping['id'][status_id]

whenever you need to use the constants in another way, you just add or alter the classmethods of the base.

Answered By: Jonathan Vanasco

Sometimes I have to create some huge choice list. I do not like to type like a monkey, so I rather to create a funcion like this:

def choices(labels):
    labels = labels.strip().split('n')
    ids = range(1, len(labels)+1)
    return zip(ids, labels)

And use like this:

my_choices = """
choice1
choice2
choice3
"""
MY_CHOICES = choices(my_choices)
print(MY_CHOICES) # ((1, choice1), (2, choice2), (3, choice3))
Answered By: Paulo Cheque

Python 3.4+: Enum

You write "If possible I’d like to avoid using a number altogether."
and indeed a named representation is clearly more pythonic.
A bare string, however, is susceptible to typos.

Python 3.4 introduces a module called
enum providing Enum and IntEnum pseudoclasses
that help with this situation.
With it, your example could work as follows:

# in Python 3.4 or later:
import enum  

class Status(enum.IntEnum):
    Cancelled = -1,
    Requires_attention = 0,
    Work_in_progress = 1,
    Complete = 2

def choiceadapter(enumtype):
    return ((item.value, item.name.replace('_', ' ')) for item in enumtype)

class Task(models.Model):
    status = models.IntegerField(choices=choiceadapter(Status), 
                                 default=Status.Requires_attention.value)

and once the Django team picks up Enum, the
choiceadapter will even be built into Django.

EDIT 2021-06: After some work with Enum, I must say I am not enthused.
In my style of work (in Django; your mileage may vary), the abstraction tends to get in the way and I find myself preferring a loose list of constants (often embedded in a different class that exists anyway).

Answered By: Lutz Prechelt

One possible approach could be to use python range function with the combination of tuple.

class Task(models.Model):
    CANCELED, ATTENTION, WIP, COMPLETE = range(-1, 3)
    Status = (
        (CANCELLED, 'Cancelled'),
        (REQUIRES_ATTENTION, 'Requires attention'),
        (WORK_IN_PROGRESS, 'Work in progress'),
        (COMPLETE, 'Complete'),
    )

    status = models.IntegerField(choices=Status, default=REQUIRES_ATTENTION)
Answered By: Rashid Mahmood

Finally constants have been added to python via https://github.com/python/mypy/pull/5522

to instal:

pip install mypy typing_extensions

usage example:

from typing_extensions import Final

DAYS_IN_A_WEEK: Final = 7
DAYS_IN_A_WEEK = 8  # I really want more days in a week!

You’d need to run mypy type checker:

mypy --python-version=3.6 --strict week.py
week.py:4: error: Cannot assign to final name "DAYS_IN_A_WEEK"

for more info see: https://dev.to/wemake-services/1-minute-guide-to-real-constants-in-python-2bpk

Answered By: Neil
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.