Tips and Tricks - language reference

Any Instead of Nested for-Loops (Permalink)

Suppose you have a list of strings and want to select all items, which match at least on of the patterns stored in a different list. Simply put, check against multiple string patterns. Instead of using two nested for-loops:

lst = ["hello", "fafaea", "hello world", "xxx world", "zzz"]
patterns = ["hello", "world"]
matches = []

for item in lst:
    for pattern in patterns:
        if pattern in item:
            matches.append(item)

print(matches)  # ['hello', 'hello world', 'xxx world']

... you can utilise Python's built-in any() function, which evaluates to True if at least one of the supplied items evaluates to True:

lst = ["hello", "fafaea", "hello world", "xxx world", "zzz"]
patterns = ["hello", "world"]
matches = [
    item
    for item in lst
    if any(pattern in item for pattern in patterns)
]

print(matches)  # ['hello', 'hello world', 'xxx world']

Flexible Decorators (Permalink)

Since Python 3.9, decorators are a lot more flexible than before. Any callable can now be a decorator. With that being said, a couple of (seen from the perspective of a Python 3.8 developer) crazy things can be done.

Suppose we have three decorators transforming the returned text of a given function:

# voices.py
import functools


def normal(func):
    return func


def shout(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()

    return wrapper


def whisper(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).lower()

    return wrapper

From Python 3.9 onwards, we can put all three into a dictionary, let the user decide, which one to use, and apply it to a given function as follows:

# previous code in voices.py
DECORATORS = {
    "normal": normal,
    "shout": shout,
    "whisper": whisper,
}

voice = input(f"Choose your voice ({', '.join(DECORATORS)}):")


@DECORATORS[voice]
def get_story():
    return "This is a sample text."


print(get_story())

Executing the script at hand gives us the following output:

$ python voices.py
Choose your voice (normal, shout, whisper): shout
THIS IS A SAMPLE TEXT.

Difference Between Walrus-Operator and for-Loop (Permalink)

Since Python 3.8, named expressions, also known as the walrus-operator :=, exist. This tip includes a simple use case, where a named expression can be useful and why a for-loop does not work.

Suppose you have a simple text file called data.txt with the following content.

This is just a sample text containing a bunch of words, letters,
you name it. It has absolutely no meaning, so do not spend too much
time adding any meaning to it.

Here, line breaks were only inserted for readability purposes. In the use case, the file consists of a single line. The idea is to print at most ten characters per line until the end of the file is reached. Utilising a named expression, this is fairly easy:

# t.py
f = open("data.txt")

while letters := f.read(10):
    print(letters)

f.close()

Executing it results in ...

$ python t.py
This is ju
st a sampl
e text con
taining a
bunch of w
ord, lette
rs, you na
me it. It
has absolu
tely no me
aning, so
do not spe
nd too muc
h time add
ing any me
aning to i
t.

One may have the idea to replace it with a for-loop as follows:

# t.py
f = open("data.txt")

for letters in f.read(10):
    print(letters)

f.close()

However, we end up with the following result:

$ python t.py
T
h
i
s

i
s

j
u

What happened here? When using a for-loop, the expression after the keyword in is evaluated only once: When the loop is initialised. Consequently, we iterate over a string of ten characters and print each of them on a separate line as shown in the previous output.

When using a named expression, the expression on the right side of the := operator is always evaluated, when before a new iteration starts. Thus, the variable letters holds at most ten characters. If it is empty, the expression evaluates to False and the execution is stopped.


When to Use NoReturn and When None (Permalink)

Let's assume we have a function, which has no real return value.

def greet():
    print("Hello World")

Should I annotate this function with typing.NoReturn, because no explicit return-statement was written? Technically seen, the function has a return-value: None. You can see it if you capture the return value of the built-in print() function:

value = print("Hello\n")
print(value)
# Hello
# None

Consequently, our greet() function needs to get the None return type annotated.

def greet() -> None:
    print("Hello World")

The type typing.NoReturn is meant to be used if something does really never return. This is the case in infinite loops, ...

from typing import NoReturn


def loop() -> NoReturn:
    while True:
        pass

... when the function terminates the script like sys.exit() or if an exception is raised:

from typing import NoReturn


def exc() -> NoReturn:
    raise ValueError("Let's terminate here...")