Python is weird - Maciej Kowalski
Python is weird<br>Intro<br>Here is a collection of things that surprised me about Python. Some you probably<br>already know, but I hope some will surprise you too.<br>Python is older than Russian Federation<br>Python has exploded in popularity in recent years. Even Poznań University of<br>Technology has (almost) stopped teaching Delphi in favor of teaching Python. This<br>leads to a common misconception that Python is a fairly new language. In reality<br>Python is pretty old, it first appeared on February 20th 1991. For reference,<br>USSR dissolved on December 26th 1991. That makes Python almost a full year older<br>than the Russian Federation.<br>bool is literally an int<br>In Python everything is an object . That includes simple data types like str, int, float, bool, etc. In other words, there are no primitives.<br>This feature is not unique to Python but can be unintuitive if you (like me) come<br>from languages like Java, C++ or JavaScript, where “objects” are a way to group<br>“simple data types” together to form a “complex data type” and instances of<br>“simple data types” can exist without any “objects” associated with them.<br>This design decision has some interesting consequences.<br>This is a definition in builtins.pyi if you go to definition in your<br>IDE on bool.<br>@final<br>class bool(int):<br>def __new__(cls, o: object = False, /) -> Self: ...<br># The following overloads could be represented more elegantly with a TypeVar("_B", bool, int),<br># however mypy has a bug regarding TypeVar constraints (https://github.com/python/mypy/issues/11880).<br>@overload<br>def __and__(self, value: bool, /) -> bool: ...<br>@overload<br>def __and__(self, value: int, /) -> int: ...<br>@overload<br>def __or__(self, value: bool, /) -> bool: ...<br>@overload<br>def __or__(self, value: int, /) -> int: ...<br>@overload<br>def __xor__(self, value: bool, /) -> bool: ...<br>@overload<br>def __xor__(self, value: int, /) -> int: ...<br>@overload<br>def __rand__(self, value: bool, /) -> bool: ...<br>@overload<br>def __rand__(self, value: int, /) -> int: ...<br>@overload<br>def __ror__(self, value: bool, /) -> bool: ...<br>@overload<br>def __ror__(self, value: int, /) -> int: ...<br>@overload<br>def __rxor__(self, value: bool, /) -> bool: ...<br>@overload<br>def __rxor__(self, value: int, /) -> int: ...<br>def __getnewargs__(self) -> tuple[int]: ...<br>@deprecated("Will throw an error in Python 3.16. Use `not` for logical negation of bools instead.")<br>def __invert__(self) -> int: ... So in Python bool is a subtype of int. Expectedly, you can treat True as 1 and False as 0.<br>>>> True + True<br>2 Java, despite being an OO language like Python, doesn’t allow this.<br>jshell> true + true<br>| Error:<br>| bad operand types for binary operator '+'<br>| first type: boolean<br>| second type: boolean<br>| true + true<br>| ^---------^<br>JavaScript allows this but for a different reason, namely implicit type<br>coercion. I<br>personally find Python’s polymorphism to be more elegant in this case.<br>> true + true<br>2 C++ also allows it due to implicit integral<br>promotion which is similar to JavaScript’s type coercion, but is less aggressive and only for<br>integer-like types.<br>But what’s even more unique about Python, thanks to this design you can (but<br>probably shouldn’t) write your own numeric classes like this one:<br>class modulo10(int):<br>def __add__(self, other):<br>return super().__add__(other) % 10
x: int = modulo10(5) # no errors, types match<br>assert x + 6 == 1 # passes None is a singleton, int is interned<br>This is actually not weird but an interesting tidbit.<br>If you read Design Patterns by GoF, then you’re already familiar with the<br>popular singleton and flyweight patterns. If you haven’t already, I highly<br>recommend you read it.<br>An interesting observation about Python is that design patterns generalize beyond<br>the code you write and can be observed in the language itself.<br>None, True, False objects in Python are immortal. That means GC doesn’t<br>manage them. Exactly one instance of each is created on interpreter startup and<br>they “live” until interpreter shutdown. This can be observed by repeatedly<br>running id(None), id(True), id(False) and getting the same object IDs every<br>time.<br>>>> id(None), id(True), id(False)<br>(139746692948272, 139746692983840, 139746692983392)<br>>>> id(None), id(True), id(False)<br>(139746692948272, 139746692983840, 139746692983392)<br>>>> id(None), id(True), id(False)<br>(139746692948272, 139746692983840, 139746692983392) There are more examples of such objects in Python but those are the most<br>prominent.<br>More interesting is int. At the time of writing this post, the current<br>implementation of CPython pre-allocates integers from -5 to 256.<br>>>> id(256), id(257)<br>(140071046923016, 140071025485360)<br>>>> id(256), id(257)<br>(140071046923016, 140071025482000)<br>>>> id(256), id(257)<br>(140071046923016, 140071025482032) This is essentially a so-called interning<br>pattern. It’s a<br>simplified version of the flyweight pattern where the flyweight object doesn’t<br>process external state. You can do the same optimization in your code by<br>overriding __new__ method in your classes or by making an interning...