💾 Archived View for yujiri.xyz › software › python.gmi captured on 2024-02-05 at 09:42:55. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-09-08)
-=-=-=-=-=-=-
I oppose the use of dynamic languages in general, but Python is a cut above the rest to me. I'll talk about both the good and bad.
Subtle benefits of static typing
Class boilerplate is pretty bad: each attribute name has to be written three times (listed in the constructor parameters and then `self.field = field` in the body), inheritance is nasty if your subclass needs its own constructor (you have to invoke the parent's dunder method directly, which looks like a hack, and repeat the argument list), and you don't even get a useful `__repr__` implementation for free - objects will just print like `<MyClass object at 0x801434f50>` by default.
However the stdlib has something called dataclasses, which lets you cut out all that boilerplate.
Python uses exceptions like most dynamic languages, but it has markedly better facilities for them than some others: an exception type hierarchy and multiple `except` clauses to catch different types, and an uncaught error comes with a full stack trace by default, with filenames, function names, line numbers, and the line text for each stack frame. It can be a bit verbose, but too much information is better than too little.
One annoying wart is that `NameError` is a subtype of `Exception` instead of `BaseException`, meaning you can't have an `except` clause that matches all "normal" error types but not typos. That's stupid, but true of all the dynamic languages I've seen.
Why do all the dynamic languages catch name errors by default?
A handy little feature that improves readability and plays well with default arguments:
def func(arg1, arg2): print('arg1 is', arg1, 'and arg2 is', arg2) func(arg2 = True, arg1 = False) # prints "arg1 is False and arg2 is True"
Keyword arguments provide the benefits of an argument dictionary (`func({'arg2': True, 'arg1': False})`) without the drawbacks: arguments attached to parameter names, parameters still listed in the function header (instead of `def func(args):`), and without the syntactic noise of braces and quotes.
Generators and comprehensions are a nifty pair of features with a lot of advantages. Generators provide lazy evaluation, and unlike the equivalent in, say, Rust, they're trivial to write with the `yield` keyword.
The inline generator expressions and comprehensions are also fantastic. In a lot of ways they function like a more readable version of map/filter. For example:
l = [num*100 for num in range(10) if num % 2 == 0]
That's equivalent to:
l = list(map(lambda num: num*100, filter(lambda num: num % 2 == 0, range(10))))
Not only does the comprehension do both operations in one, but the familiar English words *in* and *if* are more readable than "list... map... lambda... filter... lambda... wait what is this doing again?". And then that nauseating stack of parentheses that you'd probably miscount and get a syntax error. They are also faster than map and filter.
Python isn't the only language to have list comprehensions, but it also has set and even dictionary comprehensions! I love refactoring some ugly ten-line block that builds a sequence imperatively into a single statement that almost reads like an English sentence.
Python's built-in collection types have methods for most common operatios (eg. insert, delete, sort), but there's one painful omission: Find an item by criteria. There is nothing like Javascript Array.find. The closest thing is to abuse `next` with filtering (which throws a confusing exception if not found: `StopIteration`).
Python has a serious problem with concurrency that compounds the performance drawbacks of interpretation: the Global Interpreter Lock only allows one thread to execute Python code at a time, so you actually *can't* parallelize CPU-bound things.
There are lots of code analysis tools, but nothing really good. There's mypy as a TypeScript-like solution, but the same drawbacks of it not being part of the language apply. I've tried about 8 different linters, all of which were terrible. They all either output primarily noise about how inlining single-line `if` statements is evil and that I should have *2* blank lines after a class definition, or there are some cases where they insist on doing awful things. So despite loving the idea of linters and formatters, I don't use one in Python.
Python has good online documentation and a decent CLI tool, but the documentation you get from `pydoc` isn't the same as the online docs and is often incomplete. The most common deficiency I've seen is not making argument types clear, without which of course I don't know how to call the function.
I like that Python has a very extensive stdlib. CSV, JSON, HTTP, TLS, emails, regex, base64, and archive formats are just a tiny fraction of what it can do out of the box, so there's a lot of things for which you don't even need to reach for third-party packages.
When you do, though, the experience takes a drastic downward turn: there's a lot of competing build tools and package formats (egg, wheel, idfk), and like 5 different tools aimed at giving it a sandboxed environment system like node_modules. Also, it has a centralized package repository (PyPI) which is cringe.