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.
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])
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)
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.
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.
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.
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.
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))
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).
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)
Final
ly 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
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.
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])
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)
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.
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.
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.
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.
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))
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).
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)
Final
ly 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