Skip to content

A Python/Django Advent calendar

Published on: December 3, 2023    Categories: Django, Python

Advent is the liturgical season preceding Christmas in many Christian traditions, and generally begins on a Sunday — often the fourth Sunday before Christmas Day, but it varies depending on the church and the rite, which can put the first Sunday in Advent in either late November or early December.

The concept of an Advent “calendar” which counts down to Christmas, and in which each day has a door or panel which opens to reveal a religious image or message comes originally from the Lutheran tradition but has spread from there. Now it’s common to have “Advent calendars” which open to reveal pieces of candy or other small gifts or items on each day, and some people have even gone so far as to do holiday sales where each day of an Advent calendar reveals a different deal. Which makes the concept rather flexible.

Which means many people like to do “Advent calendars” or “Advent of…” themed things, which have become completely disconnected from the season they’re named for (“Advent of Code”, for example, always begins on December 1, and presents a programming challenge each day).

As my own twist on this, I’m going to try to publish one short blog post each day of Advent 2023, each covering a small but hopefully useful tip or bit of information for Python and/or Django developers. As today — December 3 — is the first Sunday in Advent for 2023, I’m starting today, and will attempt to keep going daily until the final post on Christmas Eve (December 24).

Whether I’ll continue into the season of Christmas — which runs from Christmas Day through Epiphany on January 6 — I haven’t decided yet, but if these are popular enough and I can keep coming up with enough material, I may try it.

With that preface out of the way, here’s today’s post. Check back tomorrow for the next one.

Enum.HELLO

Many programming languages have enumeration or “enum” types, whose purpose is to act as a sort of container for a set of named constants. For example, compass/cardinal directions, as represented by this Java enum:

public enum Direction {
    NORTH,
    EAST,
    SOUTH,
    WEST
}

This would allow assigning a variable of type Direction, or comparing one to values like Direction.EAST or Direction.NORTH.

Python gained built-in support for enums in Python 3.4, which added the enum module to the standard library. The example above could be written in Python as:

import enum

class Direction(enum.Enum):
    NORTH = "north"
    EAST = "east"
    SOUTH = "south"
    WEST = "west"

Note that because this is still a Python class, it’s necessary to assign values to the attributes, where in some other languages the enum members can stand alone syntactically. For Python, you can assign whatever values you like — strings as above, or integers, or anything else you think is fitting. Or if you don’t care about the values you can assign enum.auto() and let Python handle it for you.

This is a pretty useful data-modeling feature, because it lets you describe sets of values in particular domains. For example, you could model the publication status of a blog post with an enum:

import enum

class Status(enum.Enum):
    DRAFT = 0
    PUBLISHED = 1
    HIDDEN = 2

And then your blog engine could query for entries whose status field is Status.PUBLISHED, or only show ones with Status.DRAFT to certain approved users, and so on.

Incomparably enumerated

But there’s a small gotcha here. If you have the “raw” value and try to compare it to the enum, it will fail:

>>> 0 == Status.DRAFT
False

This is because enums, by default, can only successfully compare to themselves; in fact, Python enum members are singleton objects, so you can use (and the documentation even recommends using) the is operator to compare them (some_value is Status.DRAFT, for example — personally I don’t like this and recommend avoiding the is operator in almost all cases since it causes more confusion than it’s worth). Enums also don’t, by default, support ordered comparisons, so something like Status.DRAFT < Status.HIDDEN would raise a TypeError.

You can check whether a “raw” value is an enum member by trying to “instantiate” it:

>>> Status(1)
<Status.PUBLISHED: 1>

You’ll get a ValueError exception if the value isn’t in the enum.

You can also work around this completely, if you want equality comparisons to raw values of the appropriate types and/or ordering, by also subclassing the raw type you want to use. For example, class Status(int, enum.Enum) or class Direction(str, enum.Enum).

Because this is frequently useful, the Python standard library contains the base classes enum.StrEnum (as of Python 3.11) and enum.IntEnum for string and integer enums, as well as enum.Flag and enum.IntFlag for representing “flag” values that will use bitwise operators (like Enum versus IntEnum, the difference is IntFlag works with any int value, while Flag only works with the enumerated values of its own type).

And more!

It’s also possible to do fancier tricks like attaching extra attributes to enum values. Django uses this with its enums for expressing choices in ORM model fields, to let you attach display/UI labels (including strings marked for translation) to enum members.

Doing this sort of thing yourself requires overriding the __new__() method of your Enum subclass, as shown in the Python documentation, but there are times when it’s useful to know how to do it. If you’ve never encountered the __new__() method in Python before, I wrote something a few years ago explaining Python class construction and metaclasses which walks through what it does and how it’s different from __init__().

The standard-library documentation for the enum module contains a lot more useful information — check out the tutorial and “cookbook” sections, in particular.

Also, as a final note, it’s worth a reminder that as of Python 3.10, there’s structural pattern matching baked into the language, which works really nicely with enums. Here’s a simple example:

match some_blog_post.status:
    case Status.DRAFT:
        print("It's a draft!")
    case Status.PUBLISHED:
        print("It's published!")
    case Status.HIDDEN:
        print("It's hidden!")

See the pattern-matching tutorial for much more useful examples.

Go forth and enumerate

In my experience, enums are one of the most underused features of modern Python; they make excellent domain-modeling helpers, they’re great as documentation of allowed sets of values, and if you know your way around them you can produce some pretty neat and useful enums. In fact, we might encounter one of those very soon, perhaps even tomorrow.

Of course, they can also be overused, so don’t go rushing out to replace everything in your codebases with enums, but being aware of them and using them judiciously can probably improve the overall quality and readability of your Python. And for some situations — like modeling sets of choices for a user interface or a data store — they should be the very first thing you always reach for (and you should be sure to familiarize yourself with any enum or enum-like helpers provided by the libraries/ frameworks you’re using, as in the case of Django’s model-choice enum types mentioned above).