Chuniversiteit logomarkChuniversiteit.nl
“Heap, Heap, Array!”

Five things in Python that I only learned this year

Many people learn Python as their first or second programming language, but only few people bother to master it.

Python logo with graduate caps on the snakes’ heads
My experience with Python has its ups and downs

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

Link

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 %:

Dictionaries

Link

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:

Classes and value objects

Link

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:

Abstract base classes

Link

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:

Argument passing

Link

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 **: