type safety, because my type checker (mypy) can tell when I call f with the wrong method.
autocompletion, because my IDE (VS code) can autocomplete in this scenario
no need to import anything in order to call f
sensible run-time errors too
Thereās another pattern, relevant when you have multiple dispatch sites, which need different behaviours, and allows you to add a new option whilst only editing one place:
The only place where I see an enum winning you anything is for the sake of type-hinting deserialisation. But there youāre then heavily leaning on the magic created by libraries such as mypy and fastapi, and that magic has just as much potential to hurt you as ājustā having all the names listed in two places. (Once in the definition of the Literal, and once in the lookupdict or whatever your equivalent of your_lib/operation.py is).
Iām of the opinion that since Ethan could do this with a simple subclass, not even making a enum type, and then you quickly followed up with a a solution that is mypy friendly, that this should go into the stdlib.
It looks very clean and fits with the paradigm of all the existing special enums in stdlib.
Iām aware thatās valid syntax - I actually started there. The reason I pulled it into a base class was so the pattern could be used more generally rather than redefining __init__ every time you need this kind of enum in a different part of the codebase.
The error message was a symptom, not the problem I was trying to solve - I should have made that clearer in my earlier reply. The broader context was that the rest of the codebase used enums with match/case for this kind of pattern - this one place was an aberration, and I think it happened because a plain enum offered no real advantage over string literals here so the author never bothered.
This is what the handler looks like with BehaviorEnum:
python
class Operation(BehaviorEnum):
CREATE = "Create", create_op
UPDATE = "Update", update_op
READ = "Read", read_op
LIST = "List", list_op
def operation_handler(event: dict):
try:
operation = Operation(event.get("operation", ""))
try:
return operation.do(event)
except ValidationException as ve:
log.error(f"{operation.value} operation failed due to validation: {ve}")
raise
except Exception as ex:
log.error(f"{operation.value} operation failed due to unknown error: {ex}")
raise
except ValueError:
log.error("Invalid operation.")
Adding a new operation means one line in the enum - the handler never changes. And since the operation is resolved before the inner try block, the wrong error message in the log becomes much harder to reproduce. You canāt define a member without its behavior, so the pattern stays consistent without relying on discipline. Thatās what I was trying to make the enum worth reaching for.
I donāt think your original code is the correct point of comparison for this feature. More appropriate would be code like what @peterc showed above using a string literal, or similar enum+dict code:
i.e., Compare it against idiomatic code which uses enums mapped to callables. Is it better?
I agree that the requirement to keep the dict and the enum aligned is a place to look.
Personally, Iād find the following unit test perfectly adequate:
If a new enum type is added, I would strongly prefer that the values just be functions. The idea of it being a tuple of a string and a function (as you have shown) is odd. Enum members already have names.
Iām still not really convinced, but if this idea does go ahead, keeping it clear that it is a name->callable mapping will make it more usable.
I actually covered this in post 18 - Ethan suggested the @member approach where the values are just functions and the names are the keys, which I agreed was a cleaner design and addresses the tuple oddness entirely.
On the unit test - itās a good idea, but that validation should ideally be baked into BehaviorEnum itself so that every implementation doesnāt have to write and maintain it separately. Thatās part of what a shared base class gives you - the guarantee comes with the pattern rather than requiring each team to remember to test for it.
On the comparison with Peterās enum + dict - thatās a fair point and a better baseline than the string literal code I showed earlier. The difference is still the same one though: with the enum + dict you have two structures to keep in sync even if the unit test catches drift. BehaviorEnum collapses them into one so the question of sync never comes up.
Iām not sure it makes sense to combine the enum aspect with a dispatch table. Itās convenient, but what happens if you later need a second operation with the same values? Youād have to either abandon the class, or make a second duplicate enum, keep them in sync or have a mapping from one to the other. Both seem problematic.
What might work better is to just have a DispatchTable class, which takes the enum and functions, validates that all values have an associated function, and could be called to dispatch to them. Implementing it either as a class with methods matching the names, or just an object that searches globals() seems like good solutions.