Yeah, I highly dislike working with python, everything is special syntax or different for no reason. In addition to what you mentioned:
* try catch is different keywords from virtually all other languages.
* walrus tusks, the general order in list comprehensions generally where bindings come after expression
* typing is awful, unsound in a lot of cases, there’s a ton of special non straightforward plugins in mypy. This is partially better in pyright but not considerably.
* package management is silly. Poetry is like a poor imitation of npm. Venvs are a pain in the butt.
> try catch is different keywords from virtually all other languages
Python : try/except/finally/else
C# : try/catch/finally (no equivalent of “else”)
VB.net : try/catch/finally (no equivalent of “else”)
JavaScript : try/catch/finally (no equivalent of “else”)
Java : try/catch/finally (no equivalent of “else”)
Ruby: begin/rescue/ensure (no equivalent of “else”)
C++: try/catch (no equivalent of “finally” or “else”)
Python’s try/except/finally/else seem to use nearly the same keywords as many other popular languages (Ruby is an oddball here, using similar semantics with different keywords), except that it uses “except” in place of “catch”, and “else” is a unique feature, and C++ lacks “finally”.
EDIT: The above has been edited throughout to correct an initial thinko that had Python using the common “catch” instead of “except”, which I really can’t explain because I've been wading throw Python exception handling code a lot recently.
Ruby does have a good reason to be oddball IMO. begin isn't always required. When inside a method, module, class, or do...end block definition (read: most of the time), you're able to use the rescue keyword solo. Anything raising an exception in the method above the rescue keyword will be caught.
It can also be used postfixed to a statement that might raise an exception.
Ruby is oddball here because it's subtly different.
def a_method
@value = dangerous_method_call rescue "default value in case of exception"
do_something_else @value
rescue
puts "an error, oh no"
end
2. Are there any tolerable solutions you've found?
3. Are there any non-Python languages you'd recommend?
The root of Python's typing issues seems to be ad-hoc syntax recycling as a stand-in for a thoroughly planned type system. Understandably, Guido and others advise using Protocol types to make up for earlier design decisions. It sort of works, but:
1. It's effectively an unspoken deprecation for parts of the standard library
2. Current design choices create new problems
3. It still doesn't reliably prevent runtime errors
Things break down further as you move beyond the built-ins:
1. Pydantic and other tools come with their own problems
2. Ugly things happen at boundary lines in APIs (ctypes vs OOP)
3. All of these get even worse as Sphinx gets involved
The last two items came up in a recent PR discussion. The other commenter's point seemed correct: simple, specific, and rigid types prevent problems. But then why should we have Generic and Union at all?
I'm still going to use Python when necessary, but it makes the grass look very green near languages like OCaml and Elm. If Rider can make .NET's packaging tolerable, maybe F# would be good too. Are there any others you'd suggest?
I concede about multi line lambdas but far too many languages take a “syntax is bikeshedding and unimportant” approach to language design. New languages are still using “&&” and “||” as if it’s 1975 and syntax highlighting doesn’t exist. Looking at C++, Rust and even Zig code is unnecessarily complicated for someone who isn’t intimately familiar with the syntax meanwhile for the most part anyone can understand Python syntax (well, at least Python syntax that doesn’t abuse decorators and obscure dunder overloads)
I personally find && and || easier to read than "and" and "or" because instead of using words to separate words, it's symbols separating words. Sure syntax highlighting helps, but I don't think there's an objective choice here
> New languages are still using “&&” and “||” as if it’s 1975
Because it's consistent and people don't want to relearn minor details each time they switch. And Python has a lot of those little differences. Though the "and" and "or" keywords are among the few things I actually like.
> meanwhile for the most part anyone can understand Python syntax
What matters more to me is how the syntax works in the long term. I have no trouble reading and writing a lot of Rust code, but even after years with Python I still can't remember the difference between "[-1:]", "[1:]", "[:1]" etc for slicing lists/strings. Recursive list comprehensions with their lack of parentheses are unreadable to me to this day (e.g.: `[item for sublist in list_of_lists for item in sublist]`).
Python is very easy and quick to pick up, really productive for prototyping, but I don't consider its syntax to be its big advantage.
> Recursive list comprehensions with their lack of parentheses are unreadable to me to this day (e.g.: `[item for sublist in list_of_lists for item in sublist]`)
1. there's no recursion here
2. it's literally just a double nested loop flattened (i.e. exactly matching the semantics) to be on the same line instead of indented and on two lines:
[item for sublist in list_of_lists for item in sublist]
is the same as
for sublist in list_of_lists:
for item in sublist:
item
like why would you need parens when you know the for starts the next nested loop.
> why would you need parens when you know the for starts the next nested loop
Well that's the thing, I don't know the "for" starts the next nested loop because there's no indicator for that, and aside from that it's harder to see it at a glance. Nested loops have a colon, line break and deeper indentation inbetween, here you get nothing.
To me it feels like it breaks Python's own rules: the language omits curly braces for scopes but ensures readability with colon & indentation. But here the chained list comprehension nests two loops without any visible scope boundaries.
Perhaps it's just me and others can read it just fine, but that's my two cents on it.
> Well that's the thing, I don't know the "for" starts the next nested loop because there's no indicator for that
`for` is a keyword in the language - you know that the `for` starts the next loop by the same reason you know `for` starts any loop.
The remainder of your complaint makes zero sense. You expect us to sympathize with an archetype (you) that is familiar enough with the language to know what a `for` loop is (basically week 1) but not familiar enough to spot the keyword. This is an archetype with zero instantiations (ie I'm calling bs on even you personally experiencing this problem).
> I don't know the "for" starts the next nested loop because there's no indicator for that
my point: there is no other role that the sequence of characters `for` can play in python (because it's a keyword) so you have all indication that you need.
- I get that people miss them, but you can define nested functions if you want. Nameless functions are nifty, but I’ve never lost sleep over putting `def foo` in front of what would’ve been a lambda in other languages.
- I kinda like the Python ternaries.
Preferences are A-OK, and I’m not saying you’re wrong. Just chiming in that the things you mention aren’t universally disliked.
I'd like to add that forced whitespace is infuriating. It's annoying to navigate (in vim I can't easily go to the beginning/end of a code block like I can in languages where scopes are defined by braces), and it's annoying to read because it's not always intuitive how deep I am within nested scopes
> ... it's not intuitive how deep I am within nested scopes
The ONLY piece of information that is available to you regardless of how deep you are into a function is INDENTATION. Your curly braces won't help you.
Besides that, you should seriously reconsider your coding style if this is an actual problem for you.
>The ONLY piece of information that is available to you regardless of how deep you are into a function is INDENTATION.
...No? It sounds like you write very long, messy code blocks. Consider the following example, I will write a nested loop in 2 languages:
In Python:
for _ in range(10):
for _ in range(10):
print("Inner")
print("Outer")
Here's something similar in JS:
for (const _ of foo) {
for (const _ of foo) {
console.log("Inner");
}
console.log("Outer");
}
In isolation, while I'm writing this comment, I would say these code blocks are equally easy to parse. In the context of frantically debugging, scurrying around a file, I may (and have!) missed the indentation difference in the Python example.
Curly braces allow me to have 3 visual indicators that a block is finished: a visual text object (the brace itself), an extra line separator (the closing brace lives on its own line), and indentation. In Python, I only have one of those things (just indentation). I can have 2 (indentation + line sparation) if I always insert a newline before `print("Outer")`, but empty lines can get deleted and I may forget to include them as I write code. You'd need to configure a linter to guarantee the line separation indicator in a language that delimits scopes by whitespace.
> witter pulled the same trick earlier this year where they made For You the only option, then they would show For You each time you open the app, then they finally gave the Following feed back.
Obviously. You're using 2 spaces indentation for Python and 4 for JS. Don't be surprised that it is harder to see the indentation.
I think I might have indentation related bug early on, when I was new to Python, but I don't remember any recently. It could be that Python 2 allowed to mix tabs and spaces for indentation, while this is no longer allowed in 3.
Also this is how I would format the code:
for _ in range(10):
for _ in range(10):
print("Inner")
print("Outer")
Yeah it was pretty good in the Python 2 era but definitely not something to copy now.
Another big issue is that it's not expression based. For example you can't do `foo = (yield a).b` you have to do `x = yield a; foo = x.b`.
Having a special syntax for the expression version of if else is another example as you said.
I was disappointed to discover Dart made the same mistake there. There's a totally different syntax for match statements and match expressions. Why?? Ok maybe backwards compatibility.. but still. They fixed nullability properly and it wasn't too bad.
Ah I misremembered. I was actually trying to do this
x = [yield a for a in b]
Which yields (heh) an `Invalid Syntax` error. Thanks for the link to that PEP though - turns out the answer is that you almost always have to parenthesise it.
The fact python is statement-heavy to me shows that it was originally conceived in another age. It is really annoying after you use expression oriented languages
I agree (and would add explicit self argument, the fact that all your imports are exported and more) I think the point here is to reuse the Python ecosystem especially in ML.
Most AI/ML work is done in Python. It’s very compute intensive and there’s value in making it faster. There’s also a lot money sloshing around for doing AI stuff. Therefore there might be money for Modular in making a Python-compatible thing to make AI stuff faster.
In other words, I don’t think this is about the syntax at all
Some problems off the top of my head:
- The lack of new variable declaration keyword makes scopes less explicit. nonlocal is not simpler in the large than `let x = ...`
- The lack of multi-line lambdas is very limiting for anyone coming from JS, C#, Rust, Kotlin... heck even Java
- The ternary operator (... if ... else ...) is needlessly different for other mainstream languages