Some more Python tips and tricks

There are a few useful but often underutilised Python 3 syntactic tricks that I have picked up over the last few years; I have chosen to continue in the spirit of Mark and share them here.

args and kwargs
This one may be old news for some, but it managed to bypass me for a couple of years before my argument-flexibility awakening. When writing a function, sometimes it can be useful for it to be able to take an arbitrary number of arguments or keyword arguments:

def func(*args, **kwargs):
    for idx, arg in enumerate(args):
        print('Argument {0}: {1}'.format(idx, arg))
    for key, value in kwargs.items():
        print('Keyword {0} : {1}'.format(key, value))
func('Hello', 'World', argtype='keyword_argument')
I get a lot of use out of this function

A single asterisk before the argument in the function definition denotes a variable number of inputs stored as a tuple which can be iterated inside the function; a double asterisk gives a dictionary of named variables, again accessible from within the function.

Unpacking iterables into function arguments
Something that complements *args functions is the ability to unpack iterable data structures into function calls. This can be useful when, for example, your function takes a variable number of arguments which are all stored in a tuple or list:

def squared_distance(*args):
    if len(args) % 2 == 1 or not len(args):
        raise ValueError('squared_distance takes an even number of arguments (2 or more)')
    dims = len(args) // 2
    res = 0
    for i in range(dims):
        res += (args[i] - args[dims-1+i])**2
    return res

p1 = (1, 3)
p2 = (-2, 7)

print('Ugly call:', squared_distance(p1[0], p1[1], p2[0], p2[1]))
print('Much nicer:', squared_distance(*p1, *p2))
Imagine manually unpacking an iterable in 2019

Perhaps this example looks contrived (because it is); we could just modify the function to take two iterables as arguments. When using libraries written by other people it often isn’t easy nor desirable to modify function definitions, and if your iterables are longer it can be tedious to unpack them manually into the function call (as in the first call in the above code). A real use of unpacking iterables in this fashion is this method I wrote to check whether my binary search tree is, in fact, a valid binary search tree:

class BinaryTree:
# Some other methods and an __init__

    def is_tree(self):
            return self._is_tree(self.root, -np.inf, np.inf)
    
    def _is_tree(self, node, left_bound, right_bound):
        if node is None:
            return True
        left_bounds = (left_bound, node.value)
        right_bounds = (node.value, right_bound)
        return node.value > left_bound and \
            node.value < right_bound and \
            self._is_tree(node.left, *left_bounds) and \
            self._is_tree(node.right, *right_bounds)

More useful print statements from your custom classes
There’s something irritating about this:

class MyClass:
    def __init__(self, data):
        self.data = data
    
inst = MyClass(42)
print(inst)
Yeah, cheers for that

We can change this, though. If we implement the __str__ method of our class, the print function will look there first instead of spitting out the nonsense above:

class MyClass:
    def __init__(self, data):
        self.data = data

    def __str__(self):
        return 'Instance of class MyClass with data: {0}'.format(self.data)
    
inst = MyClass(42)
print(inst)
Much better

So there we have it: 3 sneaky tricks to help make your code a teeny, tiny bit better.

Author