Best practice for Python & Django constants

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.


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.


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


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.