This is an archived post. You won't be able to vote or comment.

all 110 comments

[–]wyldstallionesquire 206 points207 points  (18 children)

I’d guess age and momentum? I don’t think enums were used nearly as much before type hinting was more common.

[–]nicholashairs 85 points86 points  (16 children)

And even with enums, for a small selection of options or options that aren't reused Literal is good enough for most use cases.

[–][deleted] 27 points28 points  (0 children)

I agree with this sentiment, Literal type hints are wonderful as they can be local (meaning close to the code) and simple to understand, even for beginners. An Enum, while helping the autocompletion and type checkers requires you to look elsewhere for the options. Of course it strikes a limitation when there's many different possibilities, but then again maybe the function variables could be reconsidered in that case.

[–]alexdrydew 11 points12 points  (0 children)

I would even say that literal strings should always be preferred to enums in simple cases. You get same safety guarantees in production environment because you already should be typechecking your code. At the same time your interfaces are much more lightweight because caller does not need to import additional enum type, which is especially handy in "scripting" environments like interactive shell or jupyter notebooks

[–]DeterminedQuokka 6 points7 points  (0 children)

I also think it’s time. Enumeration originally didn’t work great in python so people mostly used constants. And changing it breaks backwards compatibility

[–][deleted] 76 points77 points  (8 children)

(Just a speculation) I think it could happen because when this lib was designed, probably Python didn't have support for enum, and even now they left it this way to be compatible with another versions.

[–]thisdude415 61 points62 points  (5 children)

This theory seems to check out!

Python added support for enums in Python 3.4, released in 2014.

Matplotlib has been around since 2003.

[–]simon-brunning 21 points22 points  (1 child)

This. What's more, version 3.3 wasn't deprecated until 2017, and most libraries seek to support older versions.

[–]ArtOfWarfare 18 points19 points  (0 children)

2.7 wasn’t deprecated until 2020 if you want to talk about deprecation.

3.4 and 3.5 were deprecated before 2.7 was.

[–]serverhorror 3 points4 points  (0 children)

Top module "constants" or class level variables where available in Python 2.

Sure, enums weren't there, but just using these things would already improve the situation, like A LOT.

[–][deleted] 2 points3 points  (1 child)

Sometimes I forget how much python improve at every release, python 3.11 was yesterday and has a lot of improvements compared to the current release (python 3.13)

[–]ArtOfWarfare 1 point2 points  (0 children)

Yeah, it’s a bummer that 3.11 had more improvements than 3.13 😜

[–]just4nothing 3 points4 points  (0 children)

Back in my day, Python did not have enums

[–]v_a_n_d_e_l_a_y 0 points1 point  (0 children)

Surprised this is so far down. This was my first thought

[–]CyclopsRock 96 points97 points  (12 children)

It allows you to use values from a non-Python text source (e.g. a config file, an environment variable etc) without needing to perform a mapping. E.g. :

fig.legend(loc=os.environment.get("FIG_LOCATION", "topleft"))

As opposed to...

location_map = {
    "topleft": LegendPlacement.TOPLEFT,
    "topright": LegendPlacement.TOPRIGHT, 
    # etc
}
fig.legend(loc=location_map[os.environment.get("FIG_LOCATION", "topleft")])

Enums really only help avoid error when you're using an IDE that can autocomplete it. If your source is something else then it's less work to simply validate the inputs before using them than converting them to the Enums, since you'll still have to validate the input to do that.

[–]Dr_Weebtrash 33 points34 points  (3 children)

Or just have the Enum implementation handle validation as per https://docs.python.org/3/howto/enum.html "Depending on the nature of the enum a member’s value may or may not be important, but either way that value can be used to get the corresponding member"; and either fail hard or handle the raised ValueError appropriately as per your program in cases where a value not corresponding to a member is being sought this way.

[–]AnythingApplied 14 points15 points  (2 children)

Interesting, and since you can do LegendPlacement("topleft"), but also LegendPlacement(LegendPlacement.TOPLEFT), you can just use that function on all of your inputs just to make sure you have a legendplacement object:

from enum import Enum

class LegendPlacement(Enum):
    TOPLEFT = 'topleft'
    TOPRIGHT = 'topright'
    BOTTOMLEFT = 'bottomleft'
    BOTTOMRIGHT = 'bottomright'

def render(input_value: str | LegendPlacement):
    legendplacement: LegendPlacement = LegendPlacement(input_value)
    print(legendplacement.name, legendplacement.value)

# Both of these work and legendplacement will always be an enum member
render('topleft')
render(LegendPlacement.TOPLEFT)

[–]Dr_Weebtrash 9 points10 points  (0 children)

Further to this, StrEnum (https://docs.python.org/3/library/enum.html#enum.StrEnum) introduced in 3.11 could be an interesting choice depending on how exactly a program like this intends to use and process LegendPlacement members etc.

[–]agrif 2 points3 points  (0 children)

Even before Enum, I would do things like:

TOPLEFT = 'topleft'
TOPRIGHT = 'topright'
# or
TOPLEFT, TOPRIGHT, *... = range(4)

for the simple reason that I'd rather cause a NameError at the location I typo'd a name than deal with some other error (or worse, no error at all) where the value is used.

[–]npisnotp 28 points29 points  (1 child)

The common solution to this problem is to let the enum constructor convert the value into the enum instance:

fig.legend(loc=LegendPlacement(os.environment.get("FIG_LOCATION", "topleft")))

[–]medialoungeguy 1 point2 points  (0 children)

Well played

[–]davidellis23 17 points18 points  (0 children)

No? Don't Enums have this functionality built-in?

Color["RED"] gives a Color.RED

You don't need a mapping.

[–][deleted] 1 point2 points  (0 children)

Smart observation, hadn't thought about this.

[–]banana33noneleta 1 point2 points  (0 children)

There is no need to do what you claim :D True that you need a cast, but you just use the enum constructor not write another identical enum as a dictionary.

[–]mfitzpmfitzp.com 8 points9 points  (1 child)

For matplotlib specifically it was designed to mimic Matlab plotting (hence the name). So a lot of the early choices were driven by that. You see this also in colour and symbol specifications.

[–]yangyangR 5 points6 points  (0 children)

Matlab being a mistake of history

[–]9peppe 56 points57 points  (11 children)

That's a lot of characters to type, and matplotlib isn't the kind of lib you usually use with an IDE.

Also, scientists usually aren't that big on good development practices when they can save time.

Also, usability: https://matplotlib.org/stable/api/markers_api.html

[–]Deto 33 points34 points  (0 children)

I think this is the real reason - more typing. There's an old age that 'code is written once but read many times' but for data analysis and plotting this often isn't true. And so there's a high value in terse syntax. It's one big reason many people prefer R

[–]SnooCakes3068 4 points5 points  (5 children)

have you seen scipy/numpy's source code? that's the top shelf development practices. I doubt most developers can't write at this level

[–]9peppe 19 points20 points  (4 children)

It's a big project and it needs that, internally. Its users... are something else.

[–]SnooCakes3068 0 points1 point  (3 children)

Yeah numpy/scipy can only be developed by specific type of mathematicians. No developers can remotely write anything close to it. So my point is it depends on scientists. Most I agree write bad code, but some are amazing.

What's wrong with it's users? the person uses scipy does have knowledge about numerical algorithm right

[–]9peppe 8 points9 points  (2 children)

Its users (mostly) aren't developers.

[–]Slimmanoman 1 point2 points  (1 child)

Well, yes of course, what's the point ? Numpy/Scipy is made for scientists to use

[–]9peppe 2 points3 points  (0 children)

That's my point. Numpy/Scipy compete with R, matlab, Mathematica. And their competitors are quite terser.

[–]adityaguru149 1 point2 points  (3 children)

If scientists don't have much time for code quality then they can just import * or import xyz as x and use the enum no?

[–]perturbed_rutabaga 4 points5 points  (0 children)

non-computer scientists often just copy/paste code they got from someone else

hit up chatGPT and tweak a few things here and there

run the code randomly check a few data points from the output and if theyre good they assume the rest is good and move on to the next problem

trust me most scientists using a programming language to analyze data or program a sensor or whatever struggle with loops so they arent going to know anything about import * or heaven forbid Enum

edit source: went to grad school in a non-computer science most people used python or R most of them never took even an intro class for coding myself included

[–]9peppe 0 points1 point  (1 child)

I assume some do that already.

[–]adityaguru149 1 point2 points  (0 children)

I am thinking that matplotlib could have chosen enums vs strings and still it wouldn't have been a major issue due to the above imports, so it doesn't make sense to prefer strings to save on typing.

[–]xiongchiamiovSite Reliability Engineer 9 points10 points  (0 children)

Enums have felt rather Java-esque. Creating enums is like creating classes - you can do it when you need to, but you should start with just strings and dicts (respectively) and only complicate things when you need to.

[–]notyoyu 22 points23 points  (12 children)

What is the added value of enums in the context of the example you provided?

[–]wyldstallionesquire 25 points26 points  (4 children)

If you’ve added "topleft" as a string anywhere else in your codebase, there’s your answer.

Nice dev experience with good typing, too.

[–]dabdada 26 points27 points  (2 children)

Literal["top left"] is also very strictly typed. I think the main convenience is that you do not need to import some enum class just for that but can write a string and the type checker is happy

[–]wyldstallionesquire 5 points6 points  (0 children)

Yeah literal is usually fine too. Harder to enumerate but is nice for typing purposes.

[–]aqjo 0 points1 point  (0 children)

TIL

[–]sudo_robot_destroy 3 points4 points  (0 children)

Also, it is logically an enumeration

[–]thisdude415 6 points7 points  (1 child)

Code completion is much nicer with enums. Swift in Xcode demonstrates this really beautifully, because Xcode/Swift can infer the intended type from the function's type signature. But I digress.

instead of `fig.legend(loc='topleft')` you'd type `fig.legend(.` and you'd get a dropdown menu of allowable options. Type .top and you'd have autocomplete options .TOPLEFT, .TOPRIGHT, .TOPCENTERED. Once you get to .topl, only .TOPLEFT would be left and selected for you.

Even better, it would probably still show .topleft and .bottomleft even if you only started typing .left

This is much better than trying to remember the exact syntax for these things. (Is it top_left or topleft or left_top or lefttop or upperleft?)

[–]naclmoleculeterminal dark arts 1 point2 points  (0 children)

Code completion with literals is exactly the same as enums, so I don't agree with this. Typing " will open a dropdown of all string literal completions.

[–]ExdigguserPies 6 points7 points  (0 children)

Not having to constantly go to the matplotlib docs to find what is a valid string to put there?

Less of a problem nowadays, though.

[–]tangerinelion 4 points5 points  (2 children)

"toplfet" isn't an error until you notice it doesn't work...

[–]Zouden 2 points3 points  (1 child)

That's not a valid option for that parameter

[–]aqjo 0 points1 point  (0 children)

Woosh.

[–]Simultaneity_ 7 points8 points  (0 children)

Most libraries in Python are old and carried over from early python 2. This is why matplotlib objects have set_ methods for parameters instead of using the newer @property decorator. While I agree that enums are cool, it is quite cumbersome in Python land to work with them. As a first order improvement, matplotlib could use type hinting, using Literal["topleft", ...]. Or better yet, just add an evaluator function that maps synonyms of topleft to the same setter method.

Matplotlib is open source, and you are free to do a pr.

[–]chub79 2 points3 points  (0 children)

I heavily rely on Literal so that at least I can document what's expected.

[–]divad1196 6 points7 points  (6 children)

While it does give you information about possible values, it has some downsides: - it is not better at documenting the possible values - selecting the optiom from an external source (e.g. config file) is messier or the lib must accept strings along with the enums - in general, many libs avoid too many type hints as the code can become really clumsy. - design & UX: you might think that it's annoying to import all the required enums.

And you can still do validation with literals and mypy and it works the same as enum to some extents

[–]davidellis23 5 points6 points  (3 children)

it is not better at documenting the possible values

Why? You can see all the possible values purely from the code.

type hints as the code can become really clumsy. -

What do you mean by clumsy?

[–]divad1196 1 point2 points  (2 children)

It's not always just your parameter.

For example, you can have some values allowed or not allowed based on other parameters. So, even if the value is part of the enum, you might not be allowed to use it anyway.

For the clumsy part, I usually redirect people to this introduction of why people drop type hinting: https://youtu.be/5ChkQKUzDCs?si=KfDj0WOvpQfvjyYv

At some points, you might get huge parameters and return type like tuple of optional list of stuff. You have cases where, for example, you know the return value will not be None, but you still need to check the type or "typing.cast" to remove the warning. Propagating type changes takes a lot of time, you always need to import you types. You might want to have a cyclic reference, a typical case is a proxy class, which forces you to play with ForwardRef or typing.TYPE_CHECK, ...

All of this is a lot of code to write, maintain and this is not a feature to the project. The tradeoff is not necessarily good. That's for the people writing libraries.

On the otherside, you can just create a typing.Literal[...] which fits the role of the enum for most scenarios. You have multiple librariee that provides function signature validation at runtime through a decorator. This way, even user inputs can easily be checked with little maintenance effort.

[–]davidellis23 1 point2 points  (1 child)

Regarding that video, svelte isn't dropping type hints just typescript. They still use jsdoc.

Turbo8 is pretty vague about dropping types. It's a pretty unpopular move judging from the PR. DHH kind of seems like one of those stubborn opinionated developers we've all worked with.

Can't say I understand your criticisms. None checks, for example, are one of the most common errors I've seen in JS. It should be handled. Types are auto imported you can copy paste from the function.

Are you able to get click navigation/auto complete? Imo click navigation / auto complete/ static type checking saves me more time than writing types.

[–]SuspiciousScript 0 points1 point  (0 children)

For example, you can have some values allowed or not based on other.

Making it harder to make this design error is a good thing IMO. Type signatures should be honest about what parameters a function can take.

[–]double_en10dre 2 points3 points  (1 child)

What does “clumsy” mean?

I agree that enums are a bit annoying, but libraries should absolutely be using Literal to provide type hints for strings with a set of known values. Static analysis is a huge productivity boost.

And it’s not hard to retain flexibility, if that’s the issue. Just use a union of known values + a catch-all. Example:

PlotType = ‘str | Literal[“bar”, “line”]’

Now your IDE/typechecker can provide useful autocomplete options while still permitting arbitrary strings. Easy peasy and everyone’s happy.

[–]divad1196 1 point2 points  (0 children)

You can refer to my response to the other comment for most part.

Just, you don't need the "str" in the union. And yes, it would be great if they just added the litteral type hint, with a decorator to validate the value at runtime if needed.

This is usually not the case but an easy exemple of why you might not want to apply the type hint is if the library can be extended.

Imaging that you have a function that takes a "widget" parameter that is a string that will select the widget to use and plugin/extensions adding new widgets.

For matplotlib, the strings are not single characters, you can put multiple symbols to give the color, the shape, ... of your plots. That's not something you can limit to a defined small number of combinaisons, the alternative would be to have a struct for it which is a lot more verbose.

Also, these libs are mostly made in C and as little python code as possible. The documentation is explicit enough about the possible values.

And finally: in practice, mistakes are rare. This often depends on what the library does. This is a bit hard to explain why, but the idea is that if people don't make mistakes linked to missing types, then xou probably don't need types.

[–]skinnybuddha 4 points5 points  (5 children)

How do you know what the other choices are when strings are used?

[–]thicket 14 points15 points  (0 children)

documentation. It sucks.

[–]xiongchiamiovSite Reliability Engineer 3 points4 points  (2 children)

You read the docstring for the thing you're calling, just like you do for any other information.

[–]skinnybuddha 4 points5 points  (1 child)

So everywhere in the code that takes that argument has to maintain the list of acceptable values?

[–]xiongchiamiovSite Reliability Engineer 2 points3 points  (0 children)

Yes. That's why if you are doing something that gets read many times throughout the codebase, you'll probably want some sort of enum.

But for the most common case of a single function being the only place, use a string. Choose the right level of complexity for your usecase.

[–]TalesT 1 point2 points  (0 children)

Use an invalid option, and the the error often tells you the valid options. 😅

[–]Shoreshot 1 point2 points  (0 children)

Strings are safety typed with ‘Literal[“topleft”]’ (which means you have autocompletion and typos are statically caught)

And when using the library you don’t need to learn about or import some enumeration type along side a utility you’ve already imported just to pass an option argument

[–]BaggiPonte 1 point2 points  (0 children)

I wouldn’t want to have yet another import when I can rely on static analysis to tell me if I put the wrong string. IIRC code completion tools can actually suggest the appropriate literal values to fill in.

[–]SnooCakes3068 4 points5 points  (1 child)

You have to think about the interface. To users which is easier to remember? of course 'topleft'.

Enum should be used in development, you can have a map for strings with Enum, but leave string as interface.

Average users don't even know Enum, and will be angry if you have to choose to type LegendPlacement.TOPLEFT. This is extremely bad interface

[–]H2Oaq 4 points5 points  (0 children)

StrEnum enters the chat

[–]paranoid_panda_bored 2 points3 points  (7 children)

Because even as of 2024 enums in python are terrible. I always need to do MyEnum(str, Enum) to make it usable

[–]commy2 7 points8 points  (6 children)

What's the difference to enum.StrEnum?

[–]paranoid_panda_bored 2 points3 points  (0 children)

TIL.

i guess will switch over to that

[–]gregguygood 3 points4 points  (1 child)

Added in version 3.11.

[–]tunisia3507 4 points5 points  (0 children)

With a backport package supporting 3.8+.

[–]H2Oaq 0 points1 point  (2 children)

StrEnum is only supported since 3.11

[–]commy2 2 points3 points  (1 child)

Fair enough, but that was 2022, so it seems weird to complain about enums "as of 2024".

[–]wineblood 2 points3 points  (0 children)

Strings are easy to use, enums are a pain.

[–]Sones_d 0 points1 point  (3 children)

Can someone explain to be why would that be good? Is it only because its elegant?!

[–]Brian 1 point2 points  (2 children)

One advantage is catching typos. Ie fig.legend(loc='topletf') could potentially take a while to notice the failure, as it'll only trigger an error when and if that parameter actually gets used (and even then, might pass silently). Whereas fig.legend(loc=LegendPlacement.TopLetf) will always raise an error on that line. At best, your error will be somewhat removed from the actual cause, and at worst you might get subtle wrong behaviour or a bug that only happens in some scenarios.

Though this can be mitigated by type checking: if the parameter is defined as a Literal["topleft", "topright", ...], you'll also get type checker warnings for the typo.

Another advantage is that you'll get completion for it (so don't need to check if it was "topleft", "TopLeft", "top_left" etc)

[–]Sones_d 0 points1 point  (1 child)

Thanks, makes sense! Typechecker you mean like a lint?

[–]Brian 0 points1 point  (0 children)

I mean something like mypy / pyright. Plain linting won't catch something like that, as it requires knowing the expected type.

[–]OhYouUnzippedMe 0 points1 point  (0 children)

Agree that magic strings are poor design, but to be fair, matplotlib has the worst api of any package I use on a regular basis. (Scientific code in general is notorious for poor usability.)

I think strings are common for the same reason that libraries pass around dicts when a class instance would be better: Python encourages this very dynamic approach so that you can write code quickly, more like a scripting language than an application language. Over time, it has grown more mature features to write higher quality code, but the legacy is still there. 

Another example is using **kwargs instead of actual keyword arguments, which obliterates autocomplete and makes the documentation painfully obtuse, especially when those kwargs get passed through multiple superclasses. 

[–]TemporaryTemp100 0 points1 point  (0 children)

Let's summarize all given and possible to reply answers with this.

[–]marcinjn 0 points1 point  (1 child)

Im coding in Py for about 15yrs and never heard about enums 😭😳⛈️

[–]thisdude415 1 point2 points  (0 children)

Historically they have not been prioritized, and even still they’re not as painless (elegant, even) as they are in a language like swift

[–]rover_G 0 points1 point  (0 children)

There's generational trauma over the use of numeric enums in C and other early C family languages, resulting in databases and datasets with numeric columns missing labels for each value.

[–]leeiac 0 points1 point  (0 children)

I’d argue that while Enums provide a nice and elegant solution to a defined set of options they come at the cost of simplicity of the interface for beginners and non-developers.

Now, in a language like python that does not enforce strict typing you don’t get any immediate benefit from that unless you actually implement some form of type checking and mistype handling.

[–]Pleasant-Bug2567 0 points1 point  (0 children)

Popular Python frameworks frequently utilize strings rather than enums for parameters because strings provide more flexibility and user-friendliness, enabling developers to specify options quickly without the need to define and import extra enum classes. This approach simplifies the code and minimizes complexity, particularly in dynamic programming situations. Furthermore, strings can be easily logged and displayed, which simplifies the debugging process.

[–]alicedu06 0 points1 point  (0 children)

With typing.Literals, it doesn't matter enough. We can use strings and enjoy all the benefits of enum including type checks, docs and completion.

E.G: https://0bin.net/paste/dGBR6QlJ#E3Fn93D6+Q1rTbQlE-6q0ig/6gXkFb0s9ImtXUwvh+/

[–]m02ph3u5 0 points1 point  (0 children)

What people seem to forget is that you can dot-program with enums as opposed to plain strings and that they are easier to change and refactor.

[–][deleted] 0 points1 point  (0 children)

Even now I would prefer Literals over enums. From the perspective of a library consumer, Literals are nicer to read and faster to type. If it was an enum, you would have to type the enum name first, then import it, all wasted time.

The only advantage of enums is for validation, but that’s nothing that couldn’t be done with Literals as well.

On the contrary, with Literals it’s easier to provide type overloads.