Skip to content

Instantly share code, notes, and snippets.

@RobertAKARobin
Last active January 31, 2026 22:15
Show Gist options
  • Select an option

  • Save RobertAKARobin/a1cba47d62c009a378121398cc5477ea to your computer and use it in GitHub Desktop.

Select an option

Save RobertAKARobin/a1cba47d62c009a378121398cc5477ea to your computer and use it in GitHub Desktop.
Python Is Not A Great Programming Language

Python is not a great programming language.

It's great for beginners. Then it turns into a mess.

What's good

  • A huge ecosystem of good third-party libraries.
  • Named arguments.
  • Multiple inheritance.

What should be good

  • It's easy to learn and read. However, it's only easy to learn and read at the start. Once you get past "Hello world" Python can get really ugly and counterintuitive.
  • The Pythonic philosophy that "There should be one -- and preferably only one -- obvious way to do it." As someone who loves working within rules and rigid frameworks, I love this philosophy! As someone who writes Python, I really wish Python actually stuck to this philosophy. See below.

What's "meh"

  • Forced indentation. Some love it because it enforces consistency and a degree of readability. Some hate it because they think it enforces the wrong consistency. To each their own.
  • Dynamic typing. There are lots of dynamically-typed languages and lots of statically-typed languages. Which kind of typing is better isn't a Python debate, it's a general programming debate.

What's bad

  • 400 ways (more or less) to interpolate strings. This prints "Hello Robin!" 3 times:

    user = {'name': "Robin"}
    print(f"Hello {user['name']}!")
    print("Hello {name}!".format(**user))
    print("Hello %(name)s!" % user)
    

    If there was a unique and obvious use-case for each of these then that would be one thing, but there's not.

  • 69 top-level functions that you have to just memorize. GvR's explanation sounds nice, but in reality it makes things confusing.

  • map doesn't return a list, even though the whole point of a mapping function is to create one list from another. Instead it returns a map object, which is pretty much useless since it's missing append, reverse, etc. So, you always have to wrap it in list(), or use a list comprehension, which, speaking of...

  • List comprehensions are held up as an excellent recent-ish addition to Python. People say they're readable. That's true for simple examples (e.g. [x**2 for x in range(10)]) but horribly untrue for slightly more complex examples (e.g. [[row[i] for row in matrix] for i in range(4)]). I chalk this up to...

  • Weird ordering in ternary/one-line expressions. Most languages follow a consistent order where first you declare conditions, then you do stuff based the on those conditions:

    if user.isSignedIn then user.greet else error
    
    for user in signedInUsers do user.greet
    

    Python does this in the opposite order:

    user.greet if user.isSignedIn else error
    
    [user.greet for user in signedInUsers]
    

    This is fine for simple examples. It's bad for more complex logic because you have to first find the middle of the expression before you can really understand what you're reading.

  • Syntax for tuples. If you write a single-item tuple (tuple,) but forget the trailing comma, it's no longer a tuple but an expression. This is a really easy mistake to make. Considering the only difference between tuples and lists is mutability, it would make much more sense to use the same syntax [syntax] as lists, which does not require a trailing comma, and add a freeze or immutable method. Speaking of...

  • There's no way to make dicts or complex objects immutable.

  • Regular expressions require a lot of boilerplate:

    re.compile(r"regex", re.I | re.M)
    

    Compared to JavaScript or Ruby:

    /regex/ig
    
  • The goofy string literal syntaxes: f'', u'', b'', r''.

  • The many "magic" __double-underscore__ attributes that you just have to memorize.

  • You can't reliably catch all errors and their messages in one statement. Instead you have to use something like sys.exc_info()[0]. You shouldn't have a catch-all in production of course, but in development it's very useful, so this unintuitive extra step is annoying.

  • Dev environments. Setting up an environment is a problem in any langauge, but other languages have solved the problem better than Python. For example, while npm has its warts, it is widely accepted that a fresh environment should be set up with npm i && npm run [script]. Meanwhile each Python project seems to require a unique mish-mash of pip and pipenv and venv and other shell commands.

What's bad about the culture

Most programmers will acknowledge criticisms of their favorite language. Instead, Pythonists will say, "You just don't understand Python."

Most programmers will say a piece of code is bad if it's inefficient or hard to read. Pythonists will say a piece of code is bad if "it isn't Pythonic enough." This is about as helpful as someone saying your taste in music is bad because "it isn't cultured enough."

Pythonists have a bit of a superiority complex.

@Odalrick
Copy link

try copying some code snippet

I do it all the time. Selecting the text to copy is harder than fixing the white-space.

try to have scoped execution.

What does that have to do with white-space?

On my 720p screen, I lose track pretty fast what scope they are supposed to be in.

I don't think I ever had one of those. 340 in the nineties of course, but even then that was more a limitation of the computer than the monitor.

Still, look up: everything is in the scope of the first def you see,

People also do single line stuff with simple statements.

Yeah... your point? Are you trying to say multiple statements are "simple" so often that it bothers you? To me preventing idiots from trying to be too clever is a feature.

I'm not saying Python is perfect; and I can see why people would dislike is intensely.

Except for the white-space issue, if anything the common usage of "{}" in other languages should be worse, as they are difficult to type on some keyboard layouts.

@Benzor94
Copy link

Forced indentation. Some love it because it enforces consistency and a degree of readability. Some hate it because they think it enforces the wrong consistency. To each their own.

The vitriolic hatred the significant whitespace gets (cf. the comments here) is something that I find completely baffling. In several years of writing Python, indentation errors are something I literally never had. And that involved rather large amount of copy-pasting code. On the other hand, missing braces when copying or braces getting all kinds of fucked up and having to hunt down which block closes off where is something that did actually happen to me several times, using Java.

400 ways (more or less) to interpolate strings. [ ... ] If there was a unique and obvious use-case for each of these then that would be one thing, but there's not.

Modern Python absolutely does shit over the "there is one -- and preferably only one -- way to do it" idiom but I fail to see how string interpolation would be particularly offensive. The third way (C-style) is pretty much a relic of olden times and should be considered mostly obsolete. The other two mentioned (f-strings and .format()) obviously have a lot of overlap, but there is fairly easy to decide when to use which because there is one obvious use case format() can handle but f-strings cant:

  • If you need a string template that can only be substituted later, use format, since you can create the string template, store it, and later apply a dictionary to it via .format(). For an f-string, the substitutions must be in the scope right when the string is defined.
  • For any other situation, use f-strings since they are simple and nice.

69 top-level functions that you have to just memorize. GvR's explanation sounds nice, but in reality it makes things confusing.

There are (at least) two issues in this one statement, so let's separate them. You have stuff like max(), min(), pow(), any(), all() that are just general tools. Would it be better if these were in e.g. math and some other modules? You really don't need to know all that if you just start out with the language, and for most other languages these tools are shuffled around into various namespaces. In Python, you have input as a builtin in the builtin scope, it's very convenient for reading console input for simple stuff. How do you read console input in Java again? (rhetorical question but I frankly don't remember right now, I rarely have to do it and there are like 2313 different I/O toolsets in Java with various footguns or gotchas attached). Or have fun remembering which std header you need to import in C++ for some basic stuff that has overcomplicated syntax anyways. I fail to see how Python is more confusing here than any other language basically.

But the other issue is stuff like len(), abs(), next(), iter() etc. which other people have been calling inconsistent for e.g. having to use len(my_list) instead of my_list.length(). So this is partly a response to other comments that have been very aggressive about the supposed inconsistency of this style.

This is not inconsistent at all and some of the responses in these comments make me think people who comment don't actually understand Python at all.

These weird functions that "perhaps should be methods" are behaviours implemented as dunder methods (e.g. __len__, __next__ etc.) and they are a form of operator overloading. Dunder stuff should pretty much never be accessed directly, they are ways you can implement special functionality in the language. You don't call __init__, you instantiate objects and __init__ will be called automatically. You don't call __add__, you use the + operator. You don't call __call__, you use function call syntax instance(). In this sense len, next, iter etc. are basically prefix operators and they are controlled by operator overloading.

The consistency here is that each of these are implemented by dunder methods and they are all very general properties that lot's of stuff can have. The dictionary's my_dict.keys() method is a method instead of a keys(my_dict) function call is because this method is a particular property of a dictionary, it does not implement any special behaviour in regards to the language.

Now there is a practical reason for this which has something to do with efficiency regarding these operations on the builtins, and one might regard the choice of which behaviours are considered to be special to be arbitrary, but that's not really inconsistent. Knowledge of the Python language involves knowing the dunder methods and operator overloading, and it is very easy to determine which behaviours have these global quasi-operators attached to them -- those which are implemented by dunders.

By contrast, in Java, you can get the length of an array by myArray.length (a field rather than method, which is pretty rare in Java), the length of a list is myList.size(), collections and maps in general with .size() (so at least this is consistent), and the length of a string by myString.length(). C++ and Rust are surprisingly consistent here, its .size() and .len(), respectively, for everything, although the String's .len() doesn't exactly do what one would expect. In Python, it's len(thing) for everything that has a size.

map doesn't return a list, even though the whole point of a mapping function is to create one list from another. Instead it returns a map object, which is pretty much useless since it's missing append, reverse, etc. So, you always have to wrap it in list(), or use a list comprehension, which, speaking of...

Map returns an iterator, which is lazily evaluated. This is for chaining. It's the same thing essence as Java's Stream or Rust's iterator. It's so that if you chain several map and filter calls it won't run through the initial collection several times and build a new list each time, but rather it will iterate and build a container only once at collection time. You can also choose what collection is built (e.g. you might want to collect to a set). The parenthetical notation for chaining is fugly and in my opinion whatever comprehensions pretty much obsolete map and filter (but probably not reduce, ironically), but the return type is hardly the issue.

List comprehensions are held up as an excellent recent-ish addition to Python. People say they're readable. That's true for simple examples (e.g. [x**2 for x in range(10)]) but horribly untrue for slightly more complex examples (e.g. [[row[i] for row in matrix] for i in range(4)]). I chalk this up to...

I find the "slightly more complex example" here to be pretty well readable. With that said, whatever comprehensions should not be used for complex examples. It's very frequently stated in the docs that nested comprehensions, or comprehensions executed for side effects and stuff like that are antipatterns. Python is not really a functional programming language. Even modern Java is much more capable of being functional, and that's in spite of null, nominal typing and a lack of proper immutability. Python is an imperative language first and foremost. Whatever comprehensions and first-class functions are pretty much the only functional tools that are really idiomatic in Python. Python has no monads, no tail call optimization, no clean way to compose functions, no good way to create truly immutable data structures.

Whatever comprehensions are generally idiomatic when you need to process data that

  1. involves a single map and a single filter operation (both optional);
  2. both operations (when relevant) can be specified through expressions.

The second point is not as crucial as the first (e.g. [function(x) for x in collection if predicate(x)] is generally fine).

To give an example, you have a sequence of RouteElement objects that have a location and an altitude attribute (among others), the sequence is assumed sorted ascending in location and you want to extract a list of all altitudes between a lower bound lb and an upper bound ub (assumed that lb < ub). Then in Python you can write

altitudes = [re.altitude for re in route_elements if lb <= re.location <= ub]

Such a thing is quite common and whatever comprehensions are excellent for this. By contrast in Java (but we'd have the same for Rust) the analogous code would be

double[] altitudes = routeElements
    .stream()
    .filter(re -> lb <= re.getLocation() && re.getLocation() <= ub)
    .mapToDouble(RouteElement::getAltitude)
    .toArray();

which is not bad but much more verbose than the Python code. It also involves executing two lambda expressions per item which involves all the usual callstack shenanigans, which the jvm jit compiler might not necessarily inline, whereas list comprehensions can operate purely by expressions, so it is fully inline. I am not exactly sure what the analogous C++ code would be but if I recall anything "functional" in C++ is butt-ugly and I'd probably write a for loop. Which is not bad, but again, way more verbose than what you can do in Python.

Of course Java streams and Rust iterators can also handle much more complex manipulations in a graceful and declarative manner and for those in Python I would write a for loop, or some combination of a for loop over a generator expression (which involves only one looping due to lazy evaluation), because Python is first and foremost an imperative language. But be that as it may, these "simple" data manipulations are very common and comprehensions make this super convenient in Python.

Weird ordering in ternary/one-line expressions.

This is rather subjective. But the Python ordering at least for ternary operators is pretty sensible to me because in my experience most of the time you write a ternary operator expression, you have a "main" branch which happens most of the time and an "edge case" which is exceptional or rarer.

For example

name_components = full_name.split() if full_name is not None else []

Here the person reading the code probably wants to know how the happy path is computed and then understand the rarer case of missing data, and this ordering emphasizes the happy path. (Of course this example is a bit shit or at least questionable why a missing name is a None instead of a "" but whatever)
In other languages which use the C style ordering, when I read the code I frequently wonder "why is type T being assigned from a boolean" before I realize it continues to a ternary operator.

I also dislike Rust's if-else expression but only because rustfmt insists on writing even simple expressions on many lines e.g.

let components: Vec<String> = if let Some(name) = full_name {
        name.split(' ').map(|s| s.to_string()).collect() // Ok this is actually complicated, I thought this'd be simpler when I started writing this.
    } else {
        vec![]
    };

Otherwise it's great.

There's no way to make dicts or complex objects immutable.

That's generally an issue with some object-oriented languages but for basic stuff MappingProxyType constructed from a dict literal works for making immutable dicts (you don't need a literal dict, but then you need to copy the dict in the constructor since this object is an immutable proxy to an existing dict so if you have any other reference to the constructing dict, it won't be truly immutable) and for complex objects a dataclass with frozen=True is pretty much immutable. You CAN violate this with enough effort, but should be fine for most usecases.

The many "magic" double-underscore attributes that you just have to memorize.

You can get by without knowing most of these since there are a couple of rather esoteric ones but how is this different from knowing the language mechanics of other languages? Any language with operator overloading will have similar or worse stuff and those that don't have it will compensate elsehow (e.g. in Java you need to learn a bunch of disparate stuff like Collections, Streams, Iterables, Iterators, Autoclosable, all the various functional interfaces in java.util.function, which are stuff whose functionalities are often covered by Python dunders).

You can't reliably catch all errors and their messages in one statement. Instead you have to use something like sys.exc_info()[0]. You shouldn't have a catch-all in production of course, but in development it's very useful, so this unintuitive extra step is annoying.

Huh? I don't understand this.

try:
    # code here
except Exception as e:
    # exception handling here

pretty much does this.

Dev environments. Setting up an environment is a problem in any langauge, but other languages have solved the problem better than Python. For example, while npm has its warts, it is widely accepted that a fresh environment should be set up with npm i && npm run [script]. Meanwhile each Python project seems to require a unique mish-mash of pip and pipenv and venv and other shell commands.

Yeah well at least it seems things start to standardize around uv but of course it doesn't help with old projects. Also since pyproject.toml is supposed to be a tool-independent project specifier this should go a long way to ensure you can set up a project regardless of the build/dependency/project management tool(s) used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment