Skip to content

Commit ea8e390

Browse files
committed
blog post on enums
1 parent 8e5b5d4 commit ea8e390

8 files changed

+400
-4
lines changed

Diff for: Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build:
44
./generate-html.py
55

66
serve:
7-
python -m http.server
7+
python -m http.server 8899
88

99
watch-build:
1010
ls **/*.md **/*.html **/*.xml *.py | entr ./generate-html.py

Diff for: blog/2020-10-27-i-hate-enums.html

+223
Large diffs are not rendered by default.

Diff for: book/chapter_13_dependency_injection.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1462,7 +1462,7 @@ <h3 id="_wrap_up"><a class="anchor" href="#_wrap_up"></a>Wrap-Up</h3>
14621462
</div>
14631463
<div id="footer">
14641464
<div id="footer-text">
1465-
Last updated 2020-07-30 14:51:25 +0100
1465+
Last updated 2020-08-22 06:02:38 +0100
14661466
</div>
14671467
</div>
14681468
<style>

Diff for: book/epilogue_1_how_to_get_there_from_here.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1246,7 +1246,7 @@ <h3 id="_wrap_up"><a class="anchor" href="#_wrap_up"></a>Wrap-Up</h3>
12461246
</div>
12471247
<div id="footer">
12481248
<div id="footer-text">
1249-
Last updated 2020-04-10 15:14:40 +0100
1249+
Last updated 2020-09-03 10:52:51 +0100
12501250
</div>
12511251
</div>
12521252
<style>

Diff for: images/crab_nebula_multiple.png

1.44 MB
Loading

Diff for: index.html

+10
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ <h3>Recent posts</h3>
9393
<ul>
9494

9595

96+
<li>
97+
<a href="/blog/2020-10-27-i-hate-enums.html">2020-10-27 Making Enums (as always, arguably) more Pythonic</a>
98+
</li>
99+
100+
101+
96102
<li>
97103
<a href="/blog/2020-08-13-so-many-layers.html">2020-08-13 So Many Layers! A Note of Caution.</a>
98104
</li>
@@ -135,6 +141,8 @@ <h3>Guest Posts By David</h3>
135141

136142

137143

144+
145+
138146
<li>
139147
<a href="/blog/2019-08-03-ioc-techniques.html">2019-08-03 Three Techniques for Inverting Control, in Python</a>
140148
</li>
@@ -172,6 +180,8 @@ <h3>Classic 2017 Episodes on Ports & Adapters, by Bob</h3>
172180

173181

174182

183+
184+
175185
<li>
176186
<a href="/blog/2017-09-19-why-use-domain-events.html">2017-09-19 Why use domain events?</a>
177187
</li>

Diff for: posts/2020-10-27-i-hate-enums.md

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
title: Making Enums (as always, arguably) more Pythonic
2+
author: Harry
3+
image: crab_nebula_multiple.png
4+
image_credit: https://commons.wikimedia.org/wiki/File:Crab_Nebula_in_Multiple_Wavelengths.png
5+
6+
OK this isn't really anything to do with software architecture, but:
7+
8+
> I hate [enums](https://docs.python.org/3/library/enum.html)!
9+
10+
I thought to myself, again and again, when having to deal with them recently.
11+
12+
Why?
13+
14+
```python
15+
class BRAIN(Enum):
16+
SMALL = 'small'
17+
MEDIUM = 'medium'
18+
GALAXY = 'galaxy'
19+
```
20+
21+
What could be wrong with that, I hear you ask?
22+
Well, accuse me of wanting to _stringly type_ everything if you will,
23+
but: those enums may look like strings but they aren't!
24+
25+
```python
26+
assert BRAIN.SMALL == 'small'
27+
# nope, <BRAIN.SMALL: 'small'> != 'small'
28+
29+
assert str(BRAIN.SMALL) == 'small'
30+
# nope, 'BRAIN.SMALL' != 'small'
31+
32+
assert BRAIN.SMALL.value == 'small'
33+
# finally, yes.
34+
```
35+
36+
I imagine some people think this is a feature rather than a bug? But for me
37+
it's an endless source of annoyance. They look like strings! I defined them
38+
as strings! Why don't they behave like strings arg!
39+
40+
Just one common motivating example: often what you want to do with those
41+
enums is dump them into a database column somewhere. This not-quite-a-string
42+
behaviour will cause your ORM or `db-api` library to complain like mad, and
43+
no end of footguns and headscratching when writing tests, custom SQL, and so on.
44+
At this point I'm wanting to throw them out and just use normal constants!
45+
46+
But, one of the nice promises from Python's `enum` module is that **it's iterable**.
47+
So it's easy not just to refer to one constant,
48+
but also to refer to the list of all allowed constants. Maybe that's enough
49+
to want to rescue it?
50+
51+
But, again, it doesn't quite work the way you might want it to:
52+
53+
```python
54+
assert list(BRAIN) == ['small', 'medium', 'galaxy'] # nope
55+
assert [thing for thing in BRAIN] == ['small', 'medium', 'galaxy'] # nope
56+
assert [thing.value for thing in BRAIN] == ['small', 'medium', 'galaxy'] # yes
57+
```
58+
59+
Here's a _truly_ wtf one:
60+
61+
```python
62+
assert random.choice(BRAIN) in ['small', 'medium', 'galaxy']
63+
# Raises an Exception!!!
64+
65+
File "/usr/local/lib/python3.9/random.py", line 346, in choice
66+
return seq[self._randbelow(len(seq))]
67+
File "/usr/local/lib/python3.9/enum.py", line 355, in __getitem__
68+
return cls._member_map_[name]
69+
KeyError: 2
70+
```
71+
72+
I have no idea what's going on there. What we actually wanted was
73+
74+
```python
75+
assert random.choice(list(BRAIN)) in ['small', 'medium', 'galaxy']
76+
# which is still not true, but at least it doesn't raise an exception
77+
```
78+
79+
Now the standard library does provide a solution
80+
if you want to duck-type your enums to integers,
81+
[IntEnum](https://docs.python.org/3/library/enum.html#derived-enumerations)
82+
83+
84+
```python
85+
class IBRAIN(IntEnum):
86+
SMALL = 1
87+
MEDIUM = 2
88+
GALAXY = 3
89+
90+
assert IBRAIN.SMALL == 1
91+
assert int(IBRAIN.SMALL) == 1
92+
assert IBRAIN.SMALL.value == 1
93+
assert [thing for thing in IBRAIN] == [1, 2, 3]
94+
assert list(IBRAIN) == [1, 2, 3]
95+
assert [thing.value for thing in IBRAIN] == [1, 2, 3]
96+
assert random.choice(IBRAIN) in [1, 2, 3] # this still errors but:
97+
assert random.choice(list(IBRAIN)) in [1, 2, 3] # this is ok
98+
```
99+
100+
That's all fine and good, but I don't _want_ to use integers.
101+
I want to use strings, because then when I look in my database,
102+
or in printouts, or wherever,
103+
the values will make sense.
104+
105+
Well, the [docs say](https://docs.python.org/3/library/enum.html#others)
106+
you can just subclass `str` and make your own `StringEnum` that will work just like `IntEnum`.
107+
But it's LIES:
108+
109+
```python
110+
class BRAIN(str, Enum):
111+
SMALL = 'small'
112+
MEDIUM = 'medium'
113+
GALAXY = 'galaxy'
114+
115+
assert BRAIN.SMALL.value == 'small' # ok, as before
116+
assert BRAIN.SMALL == 'small' # yep
117+
assert list(BRAIN) == ['small', 'medium', 'galaxy'] # hooray!
118+
assert [thing for thing in BRAIN] == ['small', 'medium', 'galaxy'] # hooray!
119+
random.choice(BRAIN) # this still errors but ok i'm getting over it.
120+
121+
# but:
122+
assert str(BRAIN.SMALL) == 'small' #NOO!O!O! 'BRAIN.SMALL' != 'small'
123+
# so, while BRAIN.SMALL == 'small', str(BRAIN.SMALL) != 'small' aaaargh
124+
```
125+
126+
So here's what I ended up with:
127+
128+
```python
129+
class BRAIN(str, Enum):
130+
SMALL = 'small'
131+
MEDIUM = 'medium'
132+
GALAXY = 'galaxy'
133+
134+
def __str__(self) -> str:
135+
return str.__str__(self)
136+
```
137+
138+
* this basically avoids the need to use `.value` anywhere at all in your code
139+
* enum values duck type to strings in the ways you'd expect
140+
* you can iterate over brain and get string-likes out
141+
- altho `random.choice()` is still broken, i leave that as an exercise for the reader
142+
* and type hints still work!
143+
144+
```python
145+
# both of these type check ok
146+
foo = BRAIN.SMALL # type: str
147+
bar = BRAIN.SMALL # type: BRAIN
148+
```
149+
150+
Example code is [in a Gist](https://gist.github.com/hjwp/405f04802ea558f042728ec5edbb4e62)
151+
if you want to play around.
152+
Let me know if you find anything better!

Diff for: rss.xml

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,21 @@
77
Simple patterns for building complex apps
88
</description>
99
<link>https://www.cosmicpython.com</link>
10-
<lastBuildDate>Mon, 17 Aug 2020 06:02:42 -0000</lastBuildDate>
10+
<lastBuildDate>Tue, 27 Oct 2020 14:08:23 -0000</lastBuildDate>
1111
<pubDate>Sat, 4 Jan 2020 19:15:54 -0500</pubDate>
1212
<atom:link href="https://cosmicpython.com/rss.xml" rel="self" type="application/rss+xml" />
1313

14+
<item>
15+
<title>Making Enums (as always, arguably) more Pythonic</title>
16+
<description>
17+
18+
</description>
19+
<link>https://www.cosmicpython.com/blog/2020-10-27-i-hate-enums.html</link>
20+
<pubDate>Tue, 27 Oct 2020 12:00:00 -0000</pubDate>
21+
<dc:creator>Harry</dc:creator>
22+
<guid></guid>
23+
</item>
24+
1425
<item>
1526
<title>So Many Layers! A Note of Caution.</title>
1627
<description>

0 commit comments

Comments
 (0)