Here is a collection of things that surprised me about Python. Some you probably already know, but I hope some will surprise you too.
Python has exploded in popularity in recent years. Even Poznań University of Technology has (almost) stopped teaching Delphi in favor of teaching Python. This leads to a common misconception that Python is a fairly new language. In reality Python is pretty old, it first appeared on February 20th 1991. For reference, USSR dissolved on December 26th 1991. That makes Python almost a full year older than the Russian Federation.
bool is literally an intIn Python everything is an object. That includes simple data types like str, int, float, bool, etc. In other words, there are no primitives.
This feature is not unique to Python but can be unintuitive if you (like me) come
from languages like Java, C++ or JavaScript, where “objects” are a way to group
“simple data types” together to form a “complex data type” and instances of
“simple data types” can exist without any “objects” associated with them.
This design decision has some interesting consequences.
This is a definition in builtins.pyi if you go to definition in your
IDE on bool.
@final
class bool(int):
def __new__(cls, o: object = False, /) -> Self: ...
# The following overloads could be represented more elegantly with a TypeVar("_B", bool, int),
# however mypy has a bug regarding TypeVar constraints (https://github.com/python/mypy/issues/11880).
@overload
def __and__(self, value: bool, /) -> bool: ...
@overload
def __and__(self, value: int, /) -> int: ...
@overload
def __or__(self, value: bool, /) -> bool: ...
@overload
def __or__(self, value: int, /) -> int: ...
@overload
def __xor__(self, value: bool, /) -> bool: ...
@overload
def __xor__(self, value: int, /) -> int: ...
@overload
def __rand__(self, value: bool, /) -> bool: ...
@overload
def __rand__(self, value: int, /) -> int: ...
@overload
def __ror__(self, value: bool, /) -> bool: ...
@overload
def __ror__(self, value: int, /) -> int: ...
@overload
def __rxor__(self, value: bool, /) -> bool: ...
@overload
def __rxor__(self, value: int, /) -> int: ...
def __getnewargs__(self) -> tuple[int]: ...
@deprecated("Will throw an error in Python 3.16. Use `not` for logical negation of bools instead.")
def __invert__(self) -> int: ... So in Python bool is a subtype of int. Expectedly, you can treat True as 1 and False as 0.
>>> True + True
2 Java, despite being an OO language like Python, doesn’t allow this.
jshell> true + true
| Error:
| bad operand types for binary operator '+'
| first type: boolean
| second type: boolean
| true + true
| ^---------^
JavaScript allows this but for a different reason, namely implicit type coercion. I personally find Python’s polymorphism to be more elegant in this case.
> true + true
2 C++ also allows it due to implicit integral promotion which is similar to JavaScript’s type coercion, but is less aggressive and only for integer-like types.
But what’s even more unique about Python, thanks to this design you can (but probably shouldn’t) write your own numeric classes like this one:
class modulo10(int):
def __add__(self, other):
return super().__add__(other) % 10
x: int = modulo10(5) # no errors, types match
assert x + 6 == 1 # passes None is a singleton, int is internedThis is actually not weird but an interesting tidbit.
If you read Design Patterns by GoF, then you’re already familiar with the popular singleton and flyweight patterns. If you haven’t already, I highly recommend you read it.
An interesting observation about Python is that design patterns generalize beyond the code you write and can be observed in the language itself.
None, True, False objects in Python are immortal. That means GC doesn’t
manage them. Exactly one instance of each is created on interpreter startup and
they “live” until interpreter shutdown. This can be observed by repeatedly
running id(None), id(True), id(False) and getting the same object IDs every
time.
>>> id(None), id(True), id(False)
(139746692948272, 139746692983840, 139746692983392)
>>> id(None), id(True), id(False)
(139746692948272, 139746692983840, 139746692983392)
>>> id(None), id(True), id(False)
(139746692948272, 139746692983840, 139746692983392) There are more examples of such objects in Python but those are the most prominent.
More interesting is int. At the time of writing this post, the current
implementation of CPython pre-allocates integers from -5 to 256.
>>> id(256), id(257)
(140071046923016, 140071025485360)
>>> id(256), id(257)
(140071046923016, 140071025482000)
>>> id(256), id(257)
(140071046923016, 140071025482032) This is essentially a so-called interning
pattern. It’s a
simplified version of the flyweight pattern where the flyweight object doesn’t
process external state. You can do the same optimization in your code by
overriding __new__ method in your classes or by making an interning factory.
Once you start thinking in patterns, there is no going back.
You can create a mem-leak in Python in just 4 lines of code (or even less if you
use ;)
import gc
gc.disable()
a = []
a.append(a) Python’s primary GC mechanism is reference counting, but it also does a graph
traversal to find garbage objects with reference cycles. In the above example a holds a reference to itself so it will never be garbage collected.
It’s hard to do that by mistake, though.
You can write a multithreaded program in Python but only one thread can execute at a time. It doesn’t matter how many cores you have in your CPU, you’ll only be using at best one at a time. It still makes sense to split IO-bound tasks into threads but for CPU-bound tasks it makes threads effectively pointless if not counterproductive. This limit is enforced by a so-called Global Interpreter Lock, aka GIL.
At first glance it isn’t obvious why GIL is necessary. Java, similarly to Python, supports multithreading, compiles to bytecode and has a garbage collector but doesn’t have GIL. Java can run as many threads simultaneously as you have cores.
Reasons for existence of GIL and its future (especially since Python 3.13 which introduced experimental free-threaded mode) are a very interesting topic and deserve a separate blog post on its own, so I won’t elaborate on it further here.
JavaScript, on the other hand, by design doesn’t support threads at all; only workers which are more akin to process-based parallelism.
__slots__Honorable mention.
__slots__ is a pretty well known feature of Python explained in detail in docs
TLDR: you trade the possibility to dynamically set attributes for a smaller memory footprint.
I don’t like using it because it makes me self-conscious about perf*rmance.
Honorable mention.
You’re probably familiar with this one too, so I won’t elaborate on this much. If you aren’t, then this article by Fredrik Lundh provides a great explanation.
This tripped me up a couple times but since I internalized that everything is an object in Python (this includes functions and function parameters), I’m of the opinion that it’s not dumb at all. It’s actually logical and consistent with the overall language design.
Honorable mention.
Metaclasses are another direct consequence of everything being an object in Python. I won’t be explaining them myself here because: a) It gets pretty confusing. b) I wouldn’t do a better job than this StackOverflow article (RIP [*])
Published: 2026-05-11