7 Lesser-Known Python Tips to Write Elegant Code — Part 1 | by Zeya | Jan, 2022

Looping through iterable objects such as lists, tuples or strings is very common. Some of us may do something like this:

>>> lst = ["a", "b", "c", "d"]
>>> for idx in range(len(lst)):
... print(f"Index: {idx} --> Element: {lst[idx]}")
Index: 0 --> Element: a
Index: 1 --> Element: b
Index: 2 --> Element: c
Index: 3 --> Element: d

If we are simply going through the list, it is redundant to compute the range of its length. Instead, we can use enumerate() , which is a built-in function meant for such a purpose. The enumerate() function returns an enumerate object, which holds pairs of indices and elements of an iterable object.

>>> enumerate(lst)
<enumerate at 0x1dde1211e80>

You can see the pairs if you wrap it around list():

>>> list(enumerate(lst))
[(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd')]

Using enumerate(), the earlier code can be re-written as:

>>> lst = ["a", "b", "c", "d"]
>>> for idx, item in enumerate(lst):
... print(f"Index: {idx} --> Element: {item}")
Index: 0 --> Element: a
Index: 1 --> Element: b
Index: 2 --> Element: c
Index: 3 --> Element: d

Suppose you want the starting index to be 1, instead of 0. You can specify this using the optional start argument:

>>> lst = ["a", "b", "c", "d"]
>>> for idx, item in enumerate(lst, start=1):
... print(f"Index: {idx} --> Element: {item}")
Index: 1 --> Element: a
Index: 2 --> Element: b
Index: 3 --> Element: c
Index: 4 --> Element: d

You have probably used the int()function to convert a string or a float into an integer.

# Converting a string into an integer
>>> var = "5"
>>> print(int(var))
>>> print(type(int(var)))
5
<class 'int'>

For a string, it must be a valid string representation of an integer, or else it will raise an error.

# Converting a string into an integer
>>> var = "5.4321"
>>> print(int(var))
>>> print(type(int(var)))
ValueError: invalid literal for int() with base 10: '5.4321'

Interestingly, when converting a string representation of an integer into an integer, the int() function allows for the presence of whitespaces.

>>> int("5n") == int("   5  ") == int("tn 5 nt ") == 5True

For a floating-point number, the int()function truncates away the decimals.

# Converting a float into an integer
>>> var = 5.4321
>>> print(int(var))
>>> print(type(int(var)))
5
<class 'int'>

Apart from strings and floats, did you know that int()can also be used to parse binary numbers as integers? We simply need to specify the base=2 argument.

# Converting binary numbers into an integer
>>> print(int("1000", base=2))
>>> print(int("111110100", base=2))
8
500

The assert statement is a Boolean expression that checks if a condition is true or not. The condition comes after the assert keyword. If the condition is true, nothing happens, and program moves to the next line of code.

>>> x = "Hello, World!"
>>> assert x == "Hello, World!"
>>> print(x)
Hello, World!

However, if the condition is false, the code stops and triggers an error.

>>> x = "Hello, World!"
>>> assert x == "Hi, World!"
>>> print(x)
AssertionError:

You can also specify an custom error message after the condition:

>>> x = "Hello, World!"
>>> assert x == "Hi, World!", "Uh oh, the condition is not met!"
>>> print(x)
AssertionError: Uh oh, the condition is not met!

So, the general syntax is this:

assert <insert condition>, <insert optional error message>

Because the program stops whenever a condition is not met, assertstatements are a great tool for debugging — to see which parts of your code fails and why. For instance, you can use it to check data types or values ​​of a particular input into a function, or check the value of an output from a function given certain fixed inputs.

Unpacking and packing are useful and convenient features in Python. You can unpack values ​​stored within iterables such as tuples, strings and lists on the right-hand side of an assignment operator and assign them to variables on the left-hand side of the assignment operator. The variables will be assigned values ​​according to their relative positions in the iterable.

# Unpacking a tuple
>>> a, b, c = (1, 2, 3)
>>> print(a)
>>> print(b)
>>> print(c)
1
2
3
# Unpacking a string
>>> a, b, c = "123"
>>> print(a)
>>> print(b)
>>> print(c)
1
2
3
# Unpacking list
>>> a, b, c = [1, 2, 3]
>>> print(a)
>>> print(b)
>>> print(c)
1
2
3

On the other hand, packing uses the * operator, which allows you to pack multiple values ​​in a single variable.

# Packing with a tuple
>>> a, *b = (1, 2, 3)
>>> print(a)
>>> print(b)
1
[2, 3]
# Packing with a string
>>> *a, b = "123"
>>> print(a)
>>> print(b)
['1', '2']
3
# Packing with list
>>> a, b, *c = [1, 2, 3]
>>> print(a)
>>> print(b)
>>> print(c)
1
2
[3]

Besides tuples, lists and strings, packing and unpacking can also apply to generator objects, sets and dictionaries. It’s a handy tool when you want to swap values ​​between variables or making multiple assignments simultaneously. It makes your code more readable.

# Swapping values between variables
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> print(a)
>>> print(b)
2
1
# Parallel assignment of multiple values using unpacking
>>> first_name, last_name, gender = ["Jane", "Doe", "Female"]
>>> print(first_name)
>>> print(last_name)
>>> print(gender)
Jane
Doe
Female
# Parallel assignment of multiple values using packing
>>> *full_name, gender = ["Jane", "Doe", "Female"]
>>> print(full_name)
>>> print(gender)
['Jane', 'Doe']
Female

Here’s a well-explained article about packing and unpacking, if you’re interested to learn more.

Suppose you have a dictionary that maps a key to its corresponding value. In the example below, to retrieve the English word for a key in the num_to_words dictionary, you may be tempted to use square brackets.

>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> print(num_to_words[2])
two

What if you want to retrieve the English word for a key that is not present in the num_to_words dictionary? Yep, you guessed it — it triggers an error!

>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> print(num_to_words[4])
KeyError: 4

Instead of square brackets, the .get() method would be a more robust and practical choice. The .get() method returns the value of a key that is present in the dictionary, no different from using a square bracket.

>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> print(num_to_words.get(2))
two

Now, the benefit of .get() becomes apparent when querying a dictionary for the value of a key that is not present. Instead of triggering a KeyError and breaking your code, it returns a value of None by default and keeps your code running.

>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> print(num_to_words.get(4))
None

The beauty of .get() is that you can even specify a custom value to return when the key is not found.

>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> print(num_to_words.get(4, "Uh oh... key is not found!"))
Uh oh... key is not found!

One use-case where .get() is useful is when replacing values ​​in a list with its corresponding values ​​in a dictionary. In the example below, we want to replace each element of num_list with its corresponding English word using the num_to_words dictionary. The code breaks if you use square brackets, since 4 is absent from the keys of the dictionary.

# Using square brackets
>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> num_list = [1, 2, 3, 4, 5, 6]
>>> word_list = [num_to_words[num] for num in num_list]
>>> print(word_list)
KeyError: 4

However, the code works if we use .get() method instead.

# Using .get() method with default value of None
>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> num_list = [1, 2, 3, 4, 5, 6]
>>> word_list = [num_to_words.get(num) for num in num_list]
>>> print(word_list)
['one', 'two', 'three', None, None, None]

Perhaps, it’s still not ideal for .get() to return None for keys that are not present. We can specify the second argument of .get() method so that it returns the key itself, if it is not found in the num_to_words dictionary.

# Using .get() method with customised default value
>>> num_to_words = {1: 'one', 2: 'two', 3: 'three'}
>>> num_list = [1, 2, 3, 4, 5, 6]
>>> word_list = [num_to_words.get(num, num) for num in num_list]
>>> print(word_list)
['one', 'two', 'three', 4, 5, 6]

Another way of avoiding a KeyError As a result of querying a key not present in a dictionary is to use defaultdict from the built-incollections module.

With defaultdict, you can specify a “default value factory”, a function that returns the default values ​​we want for any non-existent key. Here, we use a lambda function to do this when initialising a defaultdict object.

>>> num_to_words_dd = defaultdict(lambda: "Uh oh... key is not found in `defaultdict`!")
>>> num_to_words_dd[1] = 'one'
>>> num_to_words_dd[2] = 'two'
>>> num_to_words_dd[3] = 'three'
>>> print(num_to_words_dd)
defaultdict(<function <lambda> at 0x000001DDE3B589D0>, {1: 'one', 2: 'two', 3: 'three'})

When querying a defaultdictobject for a key that exists, it is no different from an ordinary dictionary object. For the purpose of illustrating how defaultdict works, the code snippets below use square brackets, instead of the .get() method.

>>> num_to_words_dd[2]'two'

However, if you query a non-existent key, the default value gets returned.

>>> num_to_words_dd[5]'Uh oh... key is not found in `defaultdict`!'

You can also initialise a defaultdict with theint or list keywords. If you initialise it with int , then a defaultdictobject is created with a default value of 0. When you query a key that does not exist, it returns 0.

>>> counter = defaultdict(int)
>>> lst = [0, 1, 2, 2, 3, 1, 1, 0]
>>> for num in lst:
... counter[num] += 1
>>> print(counter[0]) # Key that exists
>>> print(counter[5]) # Key that does not exist
2
0

Similarly, if you initialise it with list, then a defaultdictobject is created with an empty list by default, and querying a non-existent key returns [] .

>>> country_list = [('AU','Australia'), ('CN','China'), 
... ('FR','France'), ('SG', 'Singapore'),
... ('US', 'United States'), ('PT', 'Portugal')]
>>> country_dict = defaultdict(list)
>>> for code, country in country_list:
... country_dict[code].append(country)
>>> print(country_dict['AU']) # Key that exists
>>> print(country_dict['BX']) # Key that does not exist
['Australia']
[]

To sum up, a defaultdict is a good option to use whenever you need to create a dictionary and that each element’s value needs to have a certain default value.

If you wish to sort an iterable, like a list, a string or a tuple, you can use the sorted() function. It returns a list with the original elements in a sorted manner, without changing the original sequence.

For strings, it returns a list of characters, sorted with punctuations or white spaces first, by upper cases and lower cases in alphabetical order.

>>> sorted("Hello, World!")[' ', '!', ',', 'H', 'W', 'd', 'e', 'l', 'l', 'l', 'o', 'o', 'r']

For a list of numbers, it returns a sequence sorted in ascending order.

>>> sorted([5, 2, 4, 1, 3])[1, 2, 3, 4, 5]

For a list of strings, it returns a sequence sorted in alphabetical order based on the first characters:

>>> fruits = ["apple", "watermelon", "pear", 
... "banana", "grapes", "rockmelon"]
>>> sorted(fruits)
['apple', 'banana', 'grapes', 'honeydew', 'pear', 'watermelon']

You can also reverse the sequence, by specifying the reverse as True in the sorted() function.

>>> sorted(fruits, reverse=True)['watermelon', 'pear', 'honeydew', 'grapes', 'banana', 'apple']

Did you also know that you can customise how you wish to sort the iterable? You can specify a function and assign it to the key argument in the sorted function. For example, if you want to sort the list of fruits by the length of each word in ascending order, you can do this:

>>> sorted(fruits, key=len)['pear', 'apple', 'banana', 'grapes', 'honeydew', 'watermelon']

Or, if you want to sort the fruits by the number of letter “n”s in each word, you can specify a lambda function like this:

>>> sorted(fruits, key=lambda x: x.count('n'))['apple', 'pear', 'grapes', 'watermelon', 'honeydew', 'banana']

We can also do the sorting by referencing another iterable. In the example below, we are sorting the fruits in increasing prices according to the prices dictionary.

>>> prices = {"apple": 1.4, "watermelon": 4, "pear": 1.2, 
... "banana": 2.3, "grapes": 3.5, "honeydew": 2.6}
>>> sorted(fruits, key=lambda x: prices.get(x))
['pear', 'apple', 'banana', 'honeydew', 'grapes', 'watermelon']

Leave a Comment