Idiomatic Python - miscellaneous part 1

Comprehensions

In [1]:
original_data = (1, 2, 3, 4)

Don't do this.

In [2]:
# list
square_roots_list = []
for val in original_data:
    square_root = val**(1/2) 
    square_roots_list.append(square_root)
print(square_roots_list)

# set
square_roots_set = set()
for val in original_data:
    square_root = val**(1/2) 
    square_roots_set.add(square_root)
print(square_roots_set)

# dict
square_roots_dict = {}
for val in original_data:
    square_root = val**(1/2) 
    square_roots_dict[val] = square_root
print(square_roots_dict) 

# dict with a condition
integer_square_roots_dict = {}
for val in original_data:
    square_root = val**(1/2)
    if square_root.is_integer():
        integer_square_roots_dict[val] = square_root
print(integer_square_roots_dict) 
[1.0, 1.4142135623730951, 1.7320508075688772, 2.0]
{1.0, 2.0, 1.7320508075688772, 1.4142135623730951}
{1: 1.0, 2: 1.4142135623730951, 3: 1.7320508075688772, 4: 2.0}
{1: 1.0, 4: 2.0}

Note: in case you're using 2.X version of Python for some reason, the result of 1/2 is 0 instead of 0.5.

Use comprehensions!

In [3]:
square_roots_list = [val**(1/2) for val in original_data]
print(square_roots_list)

square_roots_set = {val**(1/2) for val in original_data}
print(square_roots_set)

square_roots_dict = {val: val**(1/2) for val in original_data}
print(square_roots_dict)

integer_square_roots_dict = {
    val: val**(1/2)
    for val in original_data if (val**(1/2)).is_integer()
}
print(integer_square_roots_dict)
[1.0, 1.4142135623730951, 1.7320508075688772, 2.0]
{1.0, 2.0, 1.7320508075688772, 1.4142135623730951}
{1: 1.0, 2: 1.4142135623730951, 3: 1.7320508075688772, 4: 2.0}
{1: 1.0, 4: 2.0}

Using in for checking presence of an element in a collection

In [4]:
name = 'John Doe'

Don't do it like this.

In [5]:
if name == 'John' or name == 'Doe' or name == 'John Doe':
    print('This seems to be our guy')
This seems to be our guy

Do it like this!

In [6]:
if name in ('John', 'Doe', 'John Doe'):
    print('This seems to be our guy')
This seems to be our guy

Chained comparisons

In [7]:
a, b, c, d = 1, 2, 3, 4

Don't do it like this.

In [8]:
if b > a and c > b and d > c:
    print('from lowest to highest: a, b, c, d')
from lowest to highest: a, b, c, d

Do it like this!

In [9]:
if a < b < c < d:
    print('from lowest to highest: a, b, c, d')
from lowest to highest: a, b, c, d

Falsy/truthy values

In [10]:
# These are falsy
my_list = []
my_dict = {}
my_set = set()
my_tuple = tuple()
zero = 0
false = False
none = None
my_str = ''

# Basically the rest are truthy
# for example:
my_second_list = ['foo']

Don't do it like this.

In [11]:
if len(my_list) == 0:
    print('Empty list is so empty')
    
if not len(my_dict):
    print('Empty dict is also very empty')
    
if not len(my_set) and not len(my_tuple):
    print('Same goes for sets and tuples')
    
if not bool(zero) and not bool(false) and not bool(none) and len(my_str) == 0:
    print('These are also falsy')
    
if len(my_second_list) > 0:
    print('This should be true')
Empty list is so empty
Empty dict is also very empty
Same goes for sets and tuples
These are also falsy
This should be true

This is much better!

In [12]:
if not my_list:
    print('Empty list is so empty')
    
if not my_dict:
    print('Empty dict is also very empty')
    
if not my_set and not my_tuple:
    print('Same goes for sets and tuples')
    
if not zero and not false and not none and not my_str:
    print('These are also falsy')
    
if my_second_list:
    print('This should be true')
Empty list is so empty
Empty dict is also very empty
Same goes for sets and tuples
These are also falsy
This should be true

any & all

In [13]:
example_collection = ['a', True, 'Python is cool', 123, 0]

Don't do it like this.

In [14]:
any_value_truthy = True
for val in example_collection:
    if val:
        any_value_truthy = True
        break

all_values_truthy = True
for val in example_collection:
    if not val:
        all_values_truthy = False
        break
        
print('any truthy: {}, all truthy: {}'.format(any_value_truthy, all_values_truthy))
any truthy: True, all truthy: False

Do it like this!

In [15]:
any_value_truthy = any(example_collection)
all_values_truthy = all(example_collection)
print('any truthy: {}, all truthy: {}'.format(any_value_truthy, all_values_truthy))
any truthy: True, all truthy: False

Pythonic substitute for ternary operator

Many other programming languages have a ternary operator: ?. A common use case for the ternary operator is to assign a certain value to a variable based on some condition. In other words, it could be used like this:

variable = some_condition ? some_value : some_other_value

Instead of doing this.

In [16]:
some_condition = True  # just a dummy condition

if some_condition:
    variable = 'John'
else:
    variable = 'Doe'
print(variable)
John

You can do it like this!

In [17]:
variable = 'John' if some_condition else 'Doe'
print(variable)
John

Function keywords arguments

For better readability and maintainability.

In [18]:
def show_person_details(name, is_gangster, is_hacker, age):
    print('name: {}, gangster: {}, hacker: {}, age: {}'.format(
        name, is_gangster, is_hacker, age))

This is not good. It's hard to tell what `True`, `False` and `83` refer here if you are not familiar with the signature of the `show_person_details` function.

In [19]:
show_person_details('John Doe', True, False, 83)
name: John Doe, gangster: True, hacker: False, age: 83

This is much better!

In [20]:
show_person_details('John Doe', is_gangster=True, is_hacker=False, age=83)
name: John Doe, gangster: True, hacker: False, age: 83

Extra: keyword only arguments after `*`

This might be useful for example if the signature of the function is likely to change in the future. For example, if there's even a slight chance that one of the arguments may be dropped during the future development, consider using *.

In [21]:
def func_with_loads_of_args(arg1, *, arg2=None, arg3=None, arg4=None, arg5='boom'):
    pass

# This won't work because only keyword arguments allowed after *
#func_with_loads_of_args('John Doe', 1, 2)

# This is ok
func_with_loads_of_args('John Doe', arg4='foo', arg5='bar', arg2='foo bar')

Multiple assigment

Let's say we want to swap the values of two variables.

Don't do it like this.

In [22]:
# original values
a = 1
b = 2

# swap
tmp = a
a = b
b = tmp
print(a, b)
2 1

Do it like this!

In [23]:
# original values
a = 1
b = 2

# swap
a, b = b, a
print(a, b)
2 1

(Un)packing

In [24]:
my_list = [1, 2, 3, 4, 5, 6]

Don't do something like this.

In [25]:
first = my_list[0]
last = my_list[-1]
middle = my_list[1:-1]
print(first, middle, last)

packed = [first] + middle + [last]
assert packed == my_list
1 [2, 3, 4, 5] 6

This is the Pythonic way!

In [26]:
# unpacking
first, *middle, last = my_list
print(first, middle, last)

# packing
packed = [first, *middle, last]
assert packed == my_list
1 [2, 3, 4, 5] 6