February 12, 2018

Cool Python features that you might not know [part 1]

Python is an incredibly pleasant language to learn. With its clean and intuitive syntax as well as countless powerful libraries, you can pick up the basic in a few hours if you have some previous programming experience.

However, if you first learn programming in another language say, Java or C, you might not be exploiting all the awesome features Python has to offer -- or as they say, your code is not 'pythonic'.

Side note: If you want to gain more insight on what it meant to be writing 'pythonic code', I highly recommend you check out this talk by Raymond Hettinger. (Shout out to Evan for showing me this!)

I this article I will assume you have some basic understanding of Python, at the end of this you will enjoy coding in python even more.

List comprehension

Ok, this is not exactly a little known feature, but just in case some of you haven't heard of it I will go through it briefly just because it is that cool!

List comprehension is very useful when you have a list (let's call it lst1) and you want to generate another list lst2 by applying some operation on the first list.

I know what you are thinking, "that sounds like a job for for-loop!", and you would be right.

In many way, List comprehension is merely syntactic sugar for for-loop, but it can make you code much neater when use appropriately.

Let say you have a list lst1

lst1 = [1, 2, 3, 4, 5]

and you want to create another list lst2 with only odd value from lst1. You can use a for-loop:

lst2 = []
for n in lst1:
    if n % 2 != 0:

This does the job, but with List Comprehension, the code can look much simpler:

lst2 = [n for n in lst1 if n % 2 != 0] 

Let's quickly go through another example:
You have a list of various files names, you want to extract all the files that end with .htm and you want to change the file extension to .html.

files_name = ['ducks.htm', 'are.css', 'freaking.htm', 'cool.jpeg']

html_files = [name + 'l' for name in lst1 if name.endswith('.htm')]

Unpacking and Packing

Unpacking is exactly what it sounds like. Suppose you have a tuple and you want to print it

tup = (1, 2, 3, 4, 5)
print(tup) #This will output: (1, 2, 3, 4, 5)

But what if you want to print it as number seperately and not as a tuple? This is where unpacking, denoted with the symbol *, can be helpful:

tup = (1, 2, 3, 4, 5)
print(*tup) #This will output: 1 2 3 4 5

Now let's see how this can be useful by looking at a similar concept call multiple assignment. We will use the same tuple (Note: this feature works for list and and set as well) and we will try to assign each of the values to a seperate variable, we could do:

a = tup[0]
b = tup[1]
c = tup[2]
d = tup[3]
e = tup[4]

This works as expected, however, there must be a better way! With unpacking, we can achieve the same result with this instead.

a, b, c, d, e = tup 

If you are still not convinced, here is another example of how you can use unpacking to improve the readability of your code.

Let say you have a dictionary matching animal and their colour,

dic = {'cat': 'yello', 'rhino': 'grey', 'duck': 'blue'}

and you want to iterate over the dictionary and print out each animal and their colour. You can do this with unpacking:

for animal, color in dic.items():
    print(animal + ' is ' + color) 

Packing is the opposite of unpacking and it can be a bit confusing as it use the same * syntax. Have you ever seen some function with definition that looks something like def example(a, b, *args)? Well, well we will see how this works with an example:

def example(first, second, *the_rest):

example(1, 2, 3, 4, 5)
# This will output:
# 1
# 2
# [3, 4, 5]

Essentially, the * before the variable pack the arguments into a list.

Confusingly, unpacking and packing use the same notation and therefore you may encounter something like this:

def example(a, b, *the_rest):
example(1, 2, 3, 4, 5) #This output: 3 4 5 not [3, 4, 5]

In this case, the first and second * perform the opposite operation.

Fun fact: This also how some builtin function such as `print` accept an unlimited number of arguments!

Map, Filter, Zip

For all intents and purpose, all of these functions can be used replaced with for-loops or list comprehension. However, using these specialised function can be a strong semantic indication of what the script is trying achieve and we should use it whenever it is appropriate. Let's us spend a bit of time to explore each of them.

This function generate a list of the same size by performing an operation on each element of an input list. We will start with a very simple example where we try to increment each element in an integer list.

Firstly, we will define a function inc(num) as follow:

def inc(num):
    return num + 1

We can then use it in our script

lst = [1, 2, 3, 4]
inc_lst = []
for i in lst:

While this is fairly straightforward, we can simplify this script even further with map.

lst = [1, 2, 3, 4]
inc_lst = map(inc, lst)

In addition to being a much cleaner code, the second code also benefit from the semantic of the map function, which allow us to quickly comprehend what the code is supposed to do.

Similar to map, this function also generate a list from another list. It has this pattern:

new_lst = filter(condition, lst)

where 'condition' is a function that take in the list's element and return a boolean. We'll illustrate this by rewriting the odd values script we wrote with list comprehension above.

lst = [1, 2, 3, 4, 5]
def is_odd(num):
    return num % 2 != 0 

odds = filter(is_odd, lst) # -> [1, 3, 5]


Zip is used to combine two list into a single list with tuples of element pairs. For example:

lst1 = [1, 2, 3]
lst2 = ['a', 'b', 'c']
list(zip(lst1, lst2)) # -> [(1, 'a'), (2, 'b'), (3, 'c')]

I know what you're thinking, what if the list lengths are not equal? Let see what happen if we update lst2.

lst2 = ['a', 'b', 'c', 'd']
list(zip(lst1, lst2)) # -> [(1, 'a'), (2, 'b'), (3, 'c')]

It will simply take the shorter list as the limit!

Combining with unpacking feature of python, zip is very useful to loop through two list in parallel.

animal = ['cat', 'rhino', 'duck']
colour = ['yello', 'grey', 'blue']

for a, c in zip(animal, colour):
    print(a, c)
# This will out put:
# cat yello
# rhino grey
# duck blue

Lambda function

Ok, some people might get a bit annoyed to see this included in the list. In fact, Guido himself wanted to remove the features.

Recall how we wrote an inc function for our map script? Wouldn't it be nice if we don't have to explicitly define a function everytime? This is especially true if we only need the function once.

Lambda function is also known as annonymous function since the function is not given a name unlike function defined with def. Each lambda expression is composed of 3 parts.

lambda [inputs goes here]:[operations goes here]

This is best illustrated with an example:

lambda x, y: x + y

Notice how this lambda equivalent of sum is not given a name? In fact, let's assign this expression to a variable just to drive home the idea that in many way, lambda expression are just like normal function.

sum = lambda x, y: x + y
sum(17, 25) # -> 42

If we take another look at our map function, we realize that we can use a lambda function in place of a predefined function.

inc_lst = map(lambda x: x + 1, lst)

Lambda expressions are powerful, and we might be tempted to write complex operation with them. However, we must make sure we don't sacrifice readibility for it.

That's all we will go through for now. I originally planned this to be a single post, but this is getting really long; we will revisit other features next time when I have time to write more.

Til next time. =)

Comments powered by Disqus