Published on

Lesser Known Idiomatic Python

Authors

Foreword

This is the first part of an upcoming series of tutorials providing insights into Python idioms, which in my opinion deserve to be used more frequently.

Idiomatic Snippets

Typing Package

Having this somewhere at the end of your imports in every file won't hurt:

import typing as t

some_var: t.Any

Way more convenient than, say, importing each type definition separately:

from typing import List, Dict, Tuple, Any

Function Definitions Formatting

In my code (and in the codebases I work with) I tend to follow the following style:

import typing as t

def func_which_accepts_many_params(
    foo: str,
    bar: int,
    buzz: t.Any,
    *args,
    **kwargs,
) -> None:
    pass

Mind the vertical stacking of the function arguments with the trailing comma, which I will be using excessively throughout all of my upcoming posts (and which may cause some linting troubles if your linter is not configured properly). This is done due to the fact, that one may eventually want to change the order of function arguments, and this is easily achieved via a keyboard shortcut (Ctrl+Shift+Up or Ctrl+Shift+Down by default in VSCode) if the arguments are stacked vertically. Without the comma, you may eventually swap the last line with some other line and get a syntax error (no comma in between arguments), but this very setup will save you a keystroke. This is a good habit to pick up.

However, if your function signature grows way beyond something reasonable, you may look into passing a dictionary object (or "context") as the single parameter without the use of **.

I would argue for the usage of the vertical signature even for the functions accepting a single argument, since you may want to add other arguments later or comment it out for some reason:

def simple_func(
    number: int,
):
    return number + 1

Filtering a Sequence Based on a Condition

Oftentimes the code I see in production is something along the lines of

import typing as t

def filter_sequence_and_get_first_item(
    sequence: t.Any,
) -> t.Any:
    return [el for el in sequence if condition(el)][0]

def condition(
    element: t.Any,
) -> bool:
    return bool(element)

Not only is this code slower and utilizes more memory than the suggested alternative, the low-hanging fruit of sequence[0] will drastically reduce the readability for a huge codebase (trust me, been there, done that). Now, onto the alternative:

import typing as t

def get_first_item_satisfying_condition(
    sequence: t.Any,
) -> t.Any:
    try:
        return next(
            el for el
            in sequence
            if condition(el)
        )

    except StopIteration as e:
        # return None
        raise e

def condition(
    element: t.Any,
) -> bool:
    return bool(element)

The list comprehension is swapped with a generator. Sequence access by index is replaced by a next. We use a generator function to actually perform lazy computing - we don't need all the elements of the sequence (since only the first one satisfying the condition is of interest to us). We use the next built-in to trigger the __next__ magic method which is present on any iterable. A StopIteration exception may be nessesary if we expect our input to be an empty sequence or to contain no elements satisfying the condition. Depending on your taste, you may raise the exception and catch it later or silently ignore it (which I warn against)

Sorting a Dictionary By Value

This would work only with Python 3.6 and later, since dictionaries below that version do not preserve the order. Notice how I use the trailing underscore for dict_ to avoid confusion with a built-in type. I suggest you do the same instead of using some cryptic abbreviations like d or somewhat similar words like dictionary or items.

dict_ = {
    'a': '5',
    'b': '4',
    'c': '10',
    'd': '1',
    'e': '3',
    'f': '7',
    'g': '8',
    'h': '2',
    'i': '6',
    'j': '9',
}

def sort_dict_by_value(
    dict_: dict[str, str],
):
    return sorted(
        dict_.items(),
        # item is actually a named tuple of (key, value),
        # so we select the value as index 1
        key=lambda item: item[1],
    )

print(sort_dict_by_value(dict_))

# outputs
# [('d', '1'), ('c', '10'), ('h', '2'), ('e', '3'), ('b', '4'), ('a', '5'), ('i', '6'), ('f', '7'), ('g', '8'), ('j', '9')]

Oops! If you followed carefully, the values are of type str, not int, so the function sorts them alphabetically (but maybe that's what you've intended to do). We can modify our code slightly:

def sort_dict_by_int_value(
    dict_: dict[str, str],
    # You may have noticed, the return type is the list of tuples of length two,
    # with both tuple items being strings
) -> list[tuple[str, str]]:
    return sorted(
        dict_.items(),
        # coercing value to `int`
        key=lambda item: int(item[1]),
    )

sorted_list = sort_dict_by_int_value(dict_)
print(sorted_list)

# outputs
# [('d', '1'), ('h', '2'), ('e', '3'), ('b', '4'), ('a', '5'), ('i', '6'), ('f', ' 7'), ('g', '8'), ('j', '9'), ('c', '10')]

Great! Now let's assemble our dict_ (sorted_list after the actual sort) back:

sorted_dict = dict(sorted_list)

And that's it! One more adjustment to our function and we're done here:

def sort_dict_by_int_value(
    dict_: dict[str, str],
) -> dict[str, str]:
    return dict(
        sorted(
            dict_.items(),
            key=lambda item: int(item[1]),
        )
    )

sorted_dict = sort_dict_by_int_value(dict_)
print(sorted_dict)

# ouputs
# {'d': '1', 'h': '2', 'e': '3', 'b': '4', 'a': '5', 'i': '6', 'f': '7', 'g': '8', 'j': '9', 'c': '10'}

Closing thoughts

In this article we have learned about

  • the convenience of importing the typing module with all the contained types at once
  • saving yourself a few keystrokes and making code mode readable with vertically stacked function definitions
  • lazily-loading and accessing the first element in a filtered sequence
  • sorting a dictionary by value

Mind sharing some tips? Tell me in the comments and be sure I'll include those in the upcoming posts with the author attribution.


Any questions? Just ask! Any corrections? Be sure to mention those too!

Enjoy what you're reading?
Join the newsletter to stay notified and get access to early releases.