Python was created in 1991 by Guido van Rossum as a strongly dynamically-typed, interpreted and object-oriented language with functional features. In its lifespan of over 30 years, it has become one of the most popular and influential programming languages.
Before we begin, I want to say that I am not talking about performance and/or security. Python is notorious for being extremely slow. I rather want to discuss it from a more theoretical point of view — the syntax and the semantics.
Note: If you already know that you need a fast and efficient application, just use C/C++ or Rust. If however, you already have a Python application that you want to optimize, that is absolutely no problem.
Levels of Abstraction
Assembly was the level before C. C was the level before Python.
Python managed to take so many different features that were ahead of its time and put it into a beautiful syntax to create something that I would almost consider art. Just look at this function that swaps columns and rows of a 2D-array for example
def flip2darr(arr2d):
return zip(*arr2d)
Just imagine how it would be to do something like this in any other language. Would it be this simple?
Table of Contents
Here are the reasons I specifically believe Python is superior to any other language at the moment from a programming language design perspective.
- Super intuitive and easy to read and write
- A mix of declarative, functional and object-oriented aspects
- A solid object API called “the Data Model”
- Awesome built-in data types and standard library (batteries included)
- Functional features include comprehensions, unpacking,
*,**and structural pattern matching - Exceptional indexing and slicing
- Cool decorators
- Lazy generators
- Context managers
Intuitiveness
IMHO, the most important thing about Python is its intuitiveness. Its syntax is clear, concise and easy to read. It has a small amount of keywords and the syntax is reduced to exactly the necessary.
You can also measure the intuitiveness by imagining someone who has never seen Python code but knows English and has some reasoning skills. When he reads Python, he will understand quite something. This is important because a lot of our thinking happens subconsciously.
Let’s take the ternary operator as an example, or as I like to think of it: “inline-if” or “if-expression”. Many despise the Python syntax because the condition comes between the results and not before, like in the if-statement
x if valid else y
# instead of "valid ? x : y" in other C inspired languages
But, when you read it out aloud, you see the advantage. The Python syntax can be read wonderfully: “x if valid, else y”. The C syntax not so — not even if you know what ? and : mean in this context. The same is with or, and and not instead of ||, && and !
As you will see, this pattern is a common theme in Python's syntax. Another perfect example is for or often better read as “for each” or “for every”.
for x in arr:
print(x)
Which reads as: “print every x in arr” or “for each x in arr print x” in comparison to something like this
for (let i=0;i<=arr.length;i++) {
console.log(arr[i])
}
Did you catch that this would actually cause an out-of-bounds read?
Everything is an object
That means a general API (Python's data model) and duck typing. Also, it means no integer overflows and no null-termination. The data model is discussed deeply in the documentation, but I’ll still give you a small example to get a taste. Specifically, this is the implementation of a general n-dimensional-vector
class Vector(tuple):
def __add__(self, other):
return Vector(s+o for s,o in zip(self, other))
def __sub__(self, other):
return Vector(s-o for s,o in zip(self, other))
def __mul__(self, other):
return Vector(x*other for x in self)
Let’s try it out
>>> vec = Vector((1,2))
>>> vec*2
(2, 4)
>>> vec += (3,5)
>>> vec
(4, 7)
>>> vec - (8,-2)
(-4, 9)
Built-in Data Structures
The built-in data structures, their literal forms and their comprehensions are just awesome, simple and powerful: tuples, lists, dictionaries and sets.
Most languages don't have powerful built-in data structures that just work seamlessly. Just think about using a dictionary in C. Even if you copy some implementation, you still don't get the built-in syntax like dict[key]=value, etc.
Unpacking and Pattern Matching
a,b = b,a
# instead of
swap = a
a = b
b = swap
# or just being able to return several values from a function.
# When you think about it, it makes no sense that a function is n:1.
# It takes n arguments but can only return one result.
# In other languages this inherent problem is "solved" by using a class only
# made to return several values (then often called {Some}Result)
# Not so in Python:
quotient, rest = divmod(5,3)
Since Python 3.10, we also have structural pattern matching using match and case. Think of simple argument parsing for example
import sys
match sys.argv[1:]:
case []:
... # do default action
case ["help"]:
... # print help
case [file_name] if file_name.endswith(".txt"):
... # read from file
case [*words]:
... # join words into one string
Compare this to the inferior version without structural pattern matching
args = sys.argv[1:]
if not args:
...
elif args == ["help"]:
...
elif len(args) == 1 and args[0].endswith(".txt"):
...
else:
...
Multi-paradigmatic
No one forces you to use a certain kind of programming style. I have written Python code for more than 1 year without ever touching a class!
The point is, you can choose how you want to program, depending on your preference and the task at hand. For example, you could only use objects for data modelling but rely on functional code for the data processing and use procedural code for asynchronous composition.
An Awesome Standard Library
It includes exactly what you need. That results in very little code that you have to write yourself. But the most important thing is that it is in itself cohesive. There are some set of rules that you can trust upon. Probably the most prominent being that ranges include the start but exclude the end. And even when std has a problem, it is corrected. The method random.randint can take two arguments and the result will include both ends. But then, to make std more cohesive, they added the function random.randrange which will act like any other range.
Implicit boolean conversion in boolean-statements
while stack:
print(stack.pop())
This is just a thousand times easier to read than
while (stack.len != 0) {
print(stack.pop())
}
The essence is that Python reduces the signal-to-noise ratio. Everything is actually important to the program and not just mainly boilerplate.
Significant Indentation
The thing is, you should indent your code anyway in the strive for structured code. And now this leads to so much joy and happiness when for example adding an if-statement. Which is so annoying in other curly-brace languages (if you know, you know). Also, significant indentation removes even more visual clutter, as can be seen in the above example where most brackets are gone. What do you think of this?
while stack:
print stack.pop
Here with all clutter removed you can see what I mean. Python goes a long way towards removing as much clutter as possible.
Revolutionized Function Arguments
We already talked about how you can return several values. But Python also revolutionized function arguments:
- default arguments
- keyword arguments
- *args and **kwargs
This allows us to do things like
max(a,b,c)
# but also
max(range(10)) # 9
# or
max(range(10), key=lambda x: -x) # 0
# Although I am not that fond of Python lambdas :(
I don’t know any other language that allows this kind of function.
Operators
The operators in Python are awesome. Let’s just look at those that are the least common in other languages
assert {"key":"value"} | {"key":"other value"} == {
"key":"other value"
}
The union operator | is basically updating the first dict with the second. This works analogously with sets and then also with the intersection operator &.
And then there is the asterisk *. Obviously, it is the multiplication operator, but you will see it being used in other ways — often associated with spreading. Here could be a simple version of a max function that can take any number of arguments
# in most languages just
def max(a,b):
return b if a<b else a
# in python we could do
def max(*args):
# the actual code to get the max value here
...
# if we now call max
max(a,b,c)
# then the args parameter would bind to the following tuple
(a,b,c)
# The cool thing is that we can call max the same way we defined it
max(*some_iterable)
So we already saw some use cases, but probably one of the most common and useful is this one:
def func_that_calls_another_func(another_func, *args, **kwargs):
return another_func(*args, **kwargs)
The cool thing is that you could take any other function and any arguments and work with them. Is there any other language that supports this? Here you also see the double asterisk, which is used for mappings instead of iterables. Here are more examples
You can join several dictionaries like this, not just with “|”
joined = {
"some default key":"default value",
**dict1,
**dict2,
**dict3,
}
You can also use * in unpacking and pattern matching like this
elem1, elem2, *rest, last_elem = some_iterable
This quick sort is from German Wikipedia
def quicksort(liste):
if len(liste) <= 1:
return liste
pivotelement = liste.pop()
links = [element for element in liste if element < pivotelement]
rechts = [element for element in liste if element >= pivotelement]
return quicksort(links) + [pivotelement] + quicksort(rechts)
We could write this more efficiently and neater at the same time
def quicksort(liste):
if len(liste) <= 1:
return liste
pivot, rest = liste
return [
*quicksort([element for element in rest if element < pivot]),
pivot,
*quicksort([element for element in rest if element >= pivot])
]
This feature makes code less imperative and more declarative, which is the ideal state of code in my opinion.
Indexing and Slicing
Python doesn’t just improve the declarative side of things. Python's indexing and slicing is literally on steroids. Check this out:
>>> my_list = [*range(10)]
>>> my_list[0]
0
>>> my_list[-1] # (the last item)
9
>>> my_list[:5] # (5 items starting by the first)
[0,1,2,3,4]
>>> my_list[5:] # (everything after the fifth element)
[5,6,7,8,9]
>>> my_list[5:-2] # (from the fifth element to the second last exclusive)
[5,6,7]
>>> my_list[::2] # every second element
[0,2,4,6,8]
>>> my_list[::-1] # reversed
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> my_list[:] = range(20) # update the list in-place
>>> my_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
>>> my_list[10:] = [] # delete everything after the tenth element
>>> my_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
I believe this is one of the powerful features that many totally miss out on.
Decorators
Now to something else — decorators are insanely useful and clean. The most common example to show the implementation of decorators is a timer decorator, but here I want to focus on the use of decorators instead.
def fibonacci(n):
return 1 if n<=1 else fibonacci(n-1)+fibonacci(n-2)
You probably know this recursive Fibonacci implementation, and you might know that it is basically O(2^n). So very, very slow. However, you can add just one line to make it O(n):
from functools import cache
@cache
def fibonacci(n):
return 1 if n<=1 else fibonacci(n-1)+fibonacci(n-2)
cache (also called memoize) saves the result of recursive fibonacci calls which increases the time-efficiency exponentially. This is insanely cool. With decorators, you can alter the behavior of functions very easily.
Generators
Another nice feature are generators.
The best example is probably traversing a tree
from dataclasses import dataclass
@dataclass
class Tree:
value
children: list[Tree]
def preorder(self):
yield self.value
for child in self.children:
yield from child.preorder()
# use like
for x in tree.preorder():
print(x)
Here again this lazy approach is not something to be found in every language. Most will have a function that returns a list containing the tree contents in preorder. But that is pretty inefficient, both memory-wise and time-wise. Especially if you only need lets say the first 10 items.
Contextmanagers
In programming, you often need to setup a context, e.g. an I/O connection, do something while you are in that context, and then deconstruct the connection later whatever happens. Python has a built-in solution that solves this problem explicitly using the with keyword and indentation. The most common use-case is opening a file
with open("file.txt") as file:
content = file.read()
# Here the file is automatically closed.
Importantly, the file is even closed if our code using the file raises an exception.
Conclusion
At this point you are probably either convinced that Python is one of the most important languages in programming language history or maybe even that it is the best language ever, or you will never be convinced.
But please stop complaining about having to type self in instance methods. You probably won’t find a better solution anyway. People smarter than me already discussed this topic extensively.
"The explicit self is wonderful. Instead of wondering why you have to type this in Python, I’ve always wondered why you don’t have to in other languages. It takes away the implicit “this” magic. Self makes perfect sense."
This comment makes even more sense in the context of The Zen of Python:
Explicit is better than implicit
As a final goodbye — an excellent guide on what it takes to be(come) an expert at Python. Every second is worth it.
Let me know what you think! What features is Python missing?
I think it's good lambdas.
Rashid Harvey

Top comments (0)