Felix' Ramblings
<< I Hate: C++ (and its whole toolchain)
>> An Update on Various Things

2023.04.30
I Hate: Python

The official Python implementation is dogshit slow; although you can argue that one can use implementations like PyPy, which is mostly a drop-in replacement with significantly better performance way better performance. Regardless, the few times I actually use Python (for e.g. quick and dirty data processing / visualization), there's is a surprising amount of friction and pitfalls despite it being recommended for beginners.

Type safety

One of my biggest issues is the complete lack of type safety. I get why people might not want to worry about types, but for any project with >10 lines it's a pain in the ass. What is usually a quick glance at the variable definition becomes a tedious search for the type one is working with. Certain refactorings which are trivial in other languages where you can be certain that shit works if the compiler doesn't complain becomes a minefield where you have to test every single modified codepath for runtime errors.

To counter this problem, one can either use an excessive amount of comments or encode the type within the variable name. However, both of these approaches need to be maintained "manually", as Python itself simply does not care. Another """solution""" is to use "type hints", a feature which is so utterly bizarre and useless to me:


def testFunc(name: str) -> str:
    print(name)
    return 5
    
testFunc(42)
This feature provides the ability to annotate the type of function parameters and return values. Now, you might think: "Wait a moment, the types of this code snippet don't match at all. Is there some type conversion happening?". No. Not at all. The Python runtime completely ignores these type annotations. The provided code snippet runs without any errors or warnings. The official documentation notes that they "can be used by third party tools", meaning these are just glorified and standardized comments. Requiring third party tools to make certain language features even remotely useful is certainly an interesting strategy.

Rounding

How would you round 1.5? What about 2.5? This might be a cultural thing, but Python's answer completely threw me off:


round(0.5)  # 0
round(1.5)  # 2
round(2.5)  # 2
round(3.5)  # 4
The concept of "Banker's rounding" was completely new to me, where people round halfway numbers to the nearest even number. With experience in programming this one isn't too bad, but beginner's which were not exposed to various rounding methods would probably not expect their bug to because round() doesn't work as they expected.

Globals

The rational behind globals in Python makes sense if you don't care about shadowing variables. But for small scripts where you don't want to pass variables to every single function, this behaviour is a nasty one:


var = 5  # Global variable

def printGlobal():
    print(var)
    
def modifyGlobal(value):
    var = value

    
printGlobal()     # 5
modifyGlobal(42)
printGlobal()     # Still 5 :)
Python decided it would be a great idea that global variables can be read from without issue, but writing to them creates a local variable which shadows the global one. If you want to modify the global variable, you have to write the function as follows:

def modifyGlobal(value):
    global var
    var = value
Writing global ... in every single function where you want to modify the global variable is incredibly tedious and error prone. I suppose this is Python's way of discouraging the use of global variables, but honestly: Fuck off.

I have to at least give them credit that runtime checks prevent accessing and editing global variables within the same function:


var = 5

def printAndModGlobal(value):
    print(var)
    var = value
    
printAndModGlobal(90)  # "UnboundLocalError"

Programming Philosophy

Some Python syntax choices really throw me off. The global variable thing has burnt me often enough that I know that I need to include the global keyword somewhere. A good friend of mine has recently made the following innocent mistake:


global var = 5

def modifyGlobal(value):
    var = value
The global keyword in this context is literally useless, it has no effect outside of a function. modifyGlobal still creates a local variable which shadows the global. I'm confused why you'd even allow the keyword to be used that way (maybe grep-ability?) as it just incudes a false sense of security ("no error, that must have worked :)").

Other times, Python can be very concise, most of the time to the detriment of readability, e.g. return ... if ... else .... But I can choose to simply not write my programs that way. Some syntax choices such as // as the integer division operator or not supporting the logical operators && / || / ! in favor of and / or / not feels very clumsy to me, but that is probably just me not getting used to the language. I get that you don't want to duplicate operators, but then again, both == and and exist, which have a different runtime speed for some reason:


def testVariable(var):
    if var == None:
        print("Blub")

for i in range(10000000):
    testVariable(5)


> time python ./python.py

________________________________________________________
Executed in  641.75 millis    fish           external
   usr time  641.24 millis  167.00 micros  641.07 millis
   sys time    0.01 millis   15.00 micros    0.00 millis

========================================================
========================================================

def testVariable(var):
    if var is None:
        print("Blub")

for i in range(10000000):
    testVariable(5)

> time python ./python.py

________________________________________________________
Executed in  529.14 millis    fish           external
   usr time  525.52 millis    0.00 micros  525.52 millis
   sys time    3.19 millis  197.00 micros    2.99 millis

Or the fun lesson that Python uses "decorators to make class functions static", but if you try to save yourself writing a constructor you create static class members by accident:


class a:
	var = 5                # Static member variable

class b:
	def __init__(self):
		self.variable = 5  # Non-static member variable

Python's construction of a class is fucky in general. When calling a member function, the first parameter is implicitly the class instance, so you get to write func(self, ...) for every member function. Then, it has very fucky access """restrictions""" on class functions - which don't really restrict the access at all:


class A:
    def __init__(self):
        pass
    
    # Public function
    def funcA(self):
        print("A")
        
    # Public function, "but pls dont use" by convention
    def _funcB(self):
        print("B")
        
    # """private""" function, 
    def __funcC(self):
        print("C")
    
    # Public function calling the private function
    def funcD(self):
        self.__funcC()
    
i = A()
i.funcA()     # A
i._funcB()    # B
#i.__funcC()  # AttributeError
i.funcD()     # C
i._A__funcC() # C

But out of all the random tricks, I cannot believe that Python does not support breaking out of nested loops. I'm serious. Online workarounds suggest putting the loops into a function you can return from or throwing and catching an exception instead. Wowie. It gets even worse though: There was a proposal for labeled break and continue which was rejected. This is already sad enough, but take a look at the given rational, which claims that

This straight up feels like gaslighting.


<< I Hate: C++ (and its whole toolchain)
>> An Update on Various Things
 Felix' Ramblings