💾 Archived View for yujiri.xyz › software › python.gmi captured on 2022-07-16 at 14:27:59. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-06-03)
-=-=-=-=-=-=-
I oppose the use of dynamic languages in general, but Python is a cut above the rest to me.
As a dynamic language, Python has excellent metaprogramming facilities. All the operators use dunder methods, so you can give custom types behavior for `<`/`>`, `in`, indexing, iteration (generators are amazing!), you name it. There's basically no "magic" that only works for builtin types. Libraries like SQLAlchemy leverage these features to implement incredible stuff like syntactic suport for ORM queries.
Class boilerplate is pretty bad with the basic way of implementing constructors: 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 whole 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.
But the stdlib has something really useful called dataclasses, which lets you cuts out all that boilerplate for two lines.
Python uses exceptions like most dynamic languages, but it has markedly better facilities for them than some others: an exception hierarchy and multiple `except` clauses to catch different types, and the `finally` clause, which executes on the way out regardless of exceptions. `finally` is quite flexible as it can be used even without any `except` clauses (it just needs a `try`).
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.
Pretty much my only dissatisfaction with Python's error handling 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 every dynamic language I've seen).
Why do all the dynamic languages catch name errors by default?
Python's syntax has a few unusual but beneficial aspects:
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, order insignificant, 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 pretty nifty pair of features with a lot of advantages. Generators provide *lazy evaluation*, which can be important for performance. 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 also give better performance over map and filter:
https://dev.to/yujiri8/python-performance-benefits-of-generator-expressions-49od
Some other languages have list comprehensions, but Python 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 has great built-in helpers for almost all common sequence type operations:
Just one painful omission: Find an item by criteria. There is nothing like Javascript Array.find. The closest thing I'm aware of is to abuse `next` (which throws a confusing exception if not found).
Python has serious problems with concurrency that compound the performance drawbacks of interpretation. Asynchronous IO is not bad, but you actually *can't* do parallel processing even with native threads, because the Global Interpreter Lock only allows one thread to execute Python code at a time. The only way around it is to stoop to multiprocessing or something.
Mitigating performance issues, there exists PyPy, an alternative interpreter for the language (the standard one being CPython). PyPy runs a *lot* faster - within a reasonable factor of statically compiled languages, and pypy-stm is an attempt to make true multithreading possible (I'm not sure about its current status). But PyPy has some compatibility issues: it doesn't use reference counting in its garbage collection which can cause resource leaks for some programs written for CPython, and can't use certain modules that are implemented as CPython extensions (like Pygame, and therefore Renpy). It's also a pain because CPython and PyPy have separate module search paths, etc. PyPy also isn't *always* faster; it uses JIT compilation instead of being a true interpreter, so on short scripts that execute in less than a second anyway it can actually be slower.
https://doc.pypy.org/en/latest/cpython_differences.html
Python's main solution for resource management is the context manager interface and the `with` keyword that uses it. The most common example is with files:
with open(filename) as f: # Use the file # ...
The file is automatically closed when the `with` block exits, even if an exception's thrown.
And this isn't just a special case for builtin types - you can implement the interface on custom types, or as a function with `contextlib.contextmanager`.
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 like reformatting `a = b = c`-type statements into some bizarre multiline parenthesis-fest that must have been a bug (that was Black, the one with no configuration). So despite loving the idea of linters, 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.
Due to Python's age and popualarity, it has one of the biggest and most mature ecosystems. Just the standard library is so extensive, CSV, JSON, HTTP, TLS, emails, regex, base64, and archive formats are just a tiny fraction of what it can do out of the box. If somehow you need something that isn't there, there's guaranteed to be a mature package.