Five things in Python that I only learned this year
![Python logo with graduate caps on the snakes’ heads](/images/content/2024/python-mastery.png)
Python occupies a somewhat peculiar place in the programming language landscape. The programming language is often touted as beginner-friendly and used in introductory programming courses like Harvard University’s popular CS50: Introduction to Computer Science course. But only few people go on to become Python experts. I think there are a couple of reasons for that:
-
Python may be a good language for beginners, but it is not necessarily a good language for those who want to build mobile apps, web apps, or software that needs to be safe and performant. People who develop software professionally often move on to other programming languages like C#, Java, Rust, or JavaScript.
-
Those who continue to use Python as their main language usually fall into one of two categories. Some develop web applications, for which (memory) safety and performance are less of a concern. Others, like data scientists and analysts, prefer to focus on solving problems rather than bikeshedding endlessly about things like design patterns and architecture.
As a result, many people – myself included – rarely feel the need to learn more about Python than what they need to get the job done. And that’s quite a pity, because Python has quite some features that allow you to write less and better code. In this article I describe a few of the features that I found useful, but didn’t learn about until much later.
Formatting strings in Python via string concatenation can be quite cumbersome, because you will have to manually convert any variable that is not a string:
Python 3.6 introduced formatted string literals (more commonly known as f-strings) which make it a lot easier to construct formatted strings:
When numeric values need to be formatted in a specific way, e.g. with a certain number of decimals, you often see people write code like this:
There is no need to do this, because you can also use format specifications to
format variables directly! Format specifications can be used in f-strings, but
also with format()
and %
:
Python dictionaries provide an easy way to store arbitrary values in a single data structure, which makes them quite versatile:
Dictionary keys are usually simple values like strings or numbers. Not many people know that tuples can also be used as composite dictionary keys:
Dictionaries are often used to keep track of things. The following snippet shows a fairly common pattern for constructing such dictionaries using a for loop:
In this example destination_lines
is a dictionary that contains the transit
lines that serve each destination. The lines are stored in a list, which is
initialised the first time a destination is added.
You can save yourself a bit of work by simply using a defaultdict, which is a
special version of dict
that automatically initialises dictionary entries with
a value of a particular type (in this case list
):
Counter is another useful dict
subclass that is especially helpful when you
need to count things, as it also includes methods that allow you to easily
retrieve the sum of all counts and the n most common elements:
Dictionaries are flexible, which makes them useful during prototyping and in situations where the keys are not known beforehand. In all other cases, it’s usually better to use a class, which allows you to read and modify values using dot notation:
You can also add a @dataclass annotation to a class to turn it into a data
class. This makes Python automatically generate __init__()
and __repr__()
functions:
Under the hood a Python class is basically with some functions. Sadly, Python makes it very easy for you to unintentionally add new attributes to a class:
This is where __slots__ comes in. Setting this variable prevents Python from using a dict to store values. Not only does this help you catch typos, it’s also more memory-efficient:
If you use data classes you can also simply add the slots=True
parameter to
the @dataclass
decorator:
One important thing to note is that data classes are mutable. If you need an immutable version of a data class, create a named tuple:
Many contemporary programming languages might be described as Java-like. Python is an exception to that rule; not only because of its lack of curly braces and its use of significant indentation, but also because it actually predates Java and thus didn’t get the opportunity to “steal” from it.
For instance, there are no such things as interfaces in Python. Instead, you’ll have to use an abstract base class (or simply ABC) to declare abstract methods that must be implemented by classes that inherit from it:
While Python does not have “real” interfaces, it does have something that you won’t find in languages like Java: multiple inheritance.
Multiple inheritance allows you to define classes with multiple base classes, which coincidentally is also an easy way to make it hard for everyone to reason about your code. But when used with restraint, multiple inheritance can actually be very useful. For instance, it allows us to implement traits and mixins:
Functions in Python can generally be called using positional arguments, keyword arguments, or a combination thereof. Positional arguments take less effort to write, but can be harder to understand and are more error-prone.
As a programmer you can explicitly forbid the use of positional arguments by
adding a *
to the function signature. Any arguments that follow the *
must
be provided using keywords.
In the example below all arguments must be passed using keyword arguments:
In the next example the first argument can (but doesn’t have to) be positional, but the second argument must use a keyword:
Speaking of keyword arguments, if you already have a dictionary with keys that
correspond with the keyword argument names, you can simply pass that dictionary
to the function directly using **
: