Better Programming

Advice for programmers.

Follow publication

Algebraic Data Types for Python. A Lesson From Rust.

Photo by Ben Wicks on Unsplash

The most useful concept when writing code. Algebraic data types make it easy to write complex scenarios in a simple way.

When learning a new programming language, the most important thing you get is to expose yourself to a new way of thinking. Different programming languages, not only have different syntaxes (in fact this is the least important distinction) but also have different rules and implementations of similar concepts.

In this post, I’ll compare Python enums (meh) vs Rust enums (good… Really good!). I’ll give you a short, understandable, and, most importantly, powerful explanation of why Rust enums are so amazing and how to implement them in Python.

In Python, enums are implemented as “a set of symbolic names bound to unique values”. In Rust, enums give you a way of saying a value is one of a possible set of values… wait, I just wrote the same thing. Well, in both cases enums basically represent a collection of variants of a single type. The key difference is that while in Python enums are only a symbolic collection of variants, in Rust each of these variants can have its own signature and behavior.

Let’s start simple. If you were to represent weekdays, enums, in both languages, are a perfectly good way to represent each day of the week. In Python, you could write something like…

from enum import Enum


class Weekday(Enum):
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7

While in Rust you could write…

enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}

Besides the ugly = 1..7 in the Python code, both examples are basically the same. In this scenario, both Python and Rust enums are perfectly capable of modeling the problem. But what happens if these enum variants not only represent plain data, such as the name of a weekday, but meaningful actions that required data, such as actions a human might perform?

Let’s say we want to model 4 human actions: Eat, Talk, Walk, and Sleep. A perfectly reasonable way to group these is using an enum called HumanAction. In Python that might look something like…

from enum import Enum

class HumanAction(Enum):
TALK = 1
WALK = 2
SLEEP = 3
EAT = 4

While in Rust, something like…

enum HumanAction {
Talk,
Walk,
Sleep,
Eat,
}

But then, you might start thinking that each of these actions requires a unique set of parameters to be executed by a human. The Talk action requires the text that will be said, the Walk action requires the number of stepsto be walked, the Sleep action requires the number of seconds the person will sleep for, and so on. The idea is that each enum variant might have a distinct signature while still representing one variant of a similar type. In Rust, this use case can be implemented the right way…

enum HumanAction {
Talk { text: String },
Walk { steps: u8 },
Sleep { seconds: u16 },
Eat { food: String },
}

fn human_do_action(action: HumanAction) {
match action {
HumanAction::Talk { text } => println!("{:?}", text),
HumanAction::Eat { food } => println!("I'm eating {:?}", food),
HumanAction::Sleep { seconds } => println!("I'll sleep for {:?} seconds", seconds),
HumanAction::Walk { steps } => println!("I'll walk {:?} steps", steps),
}

While, in Python, this use case can not be implemented using enums. Enums in Python are not thought to store complex data structures, they are only symbolic names. To implement this, in Python, you are required to use typing functionalities and forget about enums. The same pattern we just wrote in Rust can be achieved in Python as follows…

from __future__ import annotations

from dataclasses import dataclass
from typing import TypeAlias

from typing_extensions import assert_never


@dataclass
class Talk:
word: str


@dataclass
class Walk:
steps: int


@dataclass
class Sleep:
seconds: int


@dataclass
class Eat:
food: str


HumanAction: TypeAlias = Talk | Walk | Sleep | Eat


def human_do_action(action: HumanAction) -> None:
if isinstance(action, Talk):
print(f"{action.word}")
elif isinstance(action, Eat):
print(f"I'm eating {action.food}")
elif isinstance(action, Sleep):
print(f"I'll sleep for {action.seconds} seconds")
elif isinstance(action, Walk):
print(f"I'll walk {action.steps} steps")
else:
assert_never(action)

The power of this pattern is that enums now both represent a variant of a single type while containing the values that are meaningful for each of those variants. This helps to write less code that is easier to understand, maintain, and safer because you can leverage assert_never to ensure you are handling all variants of the implemented enum.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Responses (1)

Write a response

Your final Python code can be even more concise and readable if you use Python 3.10 structural pattern matching (https://peps.python.org/pep-0636/#matching-positional-attributes).