Coding#

Introduction#

This chapter covers more programming, building on Coding Basics. Some of it will come in useful as you do more in code.

This chapter has benefitted from the online book Research Software Engineering with Python, the official Python documentation, the excellent 30 days of Python, and the Hitchhiker’s Guide to Python.

Running on empty#

Being able to create empty containers is sometimes useful. The commands to create empty lists, tuples, dictionaries, and sets are lst = [], tup=(), dic={}, and st = set() respectively.

Exercise

What is the type of an empty list?

Casting variables#

Sometimes we need to explicitly cast a value from one type to another. We can do this using functions like str(), int(), and float(). If you try these, Python will do its best to interpret the input and convert it to the output type you’d like and, if they can’t, the code will throw a great big error.

Here’s an example of casting a float as an int:

orig_number = 4.39898498
type(orig_number)
float

Now we cast it to an int:

mod_number = int(orig_number)
mod_number
4

which looks like it became an integer, but we can double check that:

type(mod_number)
int

Tuples and (im)mutability#

A tuple is an object that is defined by parentheses and entries that are separated by commas, for example (15, 20, 32). (They are of type tuple.) As such, they have a lot in common with lists-but there’s a big and important difference.

Tuples are immutable, while lists are mutable. This means that, once defined, we can always modify a list using slicing and indexing, e.g. to change the first entry of a list called listy we would use listy[0] = 5. But trying to do this with a tuple will result in an error.

Immutable objects, such as tuples, can’t have their elements changed, appended, extended, or removed. Lists can do all of these things. Tuples aren’t the only immutable objects in Python; strings are immutable too.

You may wonder why both are needed given lists seem to provide a superset of functionality: sometimes in coding, lack of flexibility is a good thing because it restricts the number of ways a process can go awry. I dare say there are other reasons too, but you don’t need to worry about them and using lists is a good default most of the time.

Exercise

Are the entries in dictionaries mutable or immutable? To help you answer this, a simple dictionary can be defined as

my_dict = {"a": 1, "b": 2}

and the values are accessed by dictionary[key], eg my_dict["a"] for this example.

While loops#

while loops continue to execute code until their conditional expression evaluates to False. (Of course, if it evaluates to True forever, your code will just continue to execute…)

n = 10
while n > 0:
    print(n)
    n -= 1

print("execution complete")
10
9
8
7
6
5
4
3
2
1
execution complete

NB: in case you’re wondering what -= does, it’s a compound assignment that sets the left-hand side equal to the left-hand side minus the right-hand side.

You can use the keyword break to break out of a while loop, for example if it’s reached a certain number of iterations without converging.

Exercise

Making use of import string and then string.ascii_lowercase to get the characters in the alphabet, write a while loop that iterates backwards through the alphabet (starting at “z”) before printing “execution complete”.

Sets#

A set in coding is a collection of unordered and unindexed distinct elements (in analogy to the mathematical definition of a set). To define a set, the two commands are:

st = {}
# or
st = set()

These aren’t very interesting though! Here’s a set with some values in:

people_set = {"Robinson", "Fawcett", "Ostrom"}

What can we do with it? We can check its length using len(people_set) and we can ask whether a particular entry is contained within it:

"Ostrom" in people_set
True

We can add multiple items or another set using .update() or .union(), or a single item using:

people_set.add("Martineau")
people_set
{'Fawcett', 'Martineau', 'Ostrom', 'Robinson'}

We can remove entries with .remove(entry_name) or, to remove only the last entry .pop(). You can easily convert between lists and sets:

list(people_set)
['Fawcett', 'Martineau', 'Robinson', 'Ostrom']

The real benefits of sets are that they support set operations, though. The most important are intersection(),

st1 = {"item1", "item2", "item3", "item4"}
st2 = {"item3", "item2"}
st1.intersection(st2)
{'item2', 'item3'}

difference(),

st1 = {"item1", "item2", "item3", "item4"}
st2 = {"item2", "item3"}
st1.difference(st2)
{'item1', 'item4'}

and symmetric_difference().

st1 = {"item1", "item2", "item3", "item4"}
st2 = {"item2", "item3"}
st2.symmetric_difference(st1)
{'item1', 'item4'}

Truthy and falsy values#

Python objects can be used in expressions that will return a boolean value, such as when a list, listy, is used with if listy. Built-in Python objects that are empty are usually evaluated as False, and are said to be ‘Falsy’. In contrast, when these built-in objects are not empty, they evaluate as True and are said to be ‘truthy’.

(If you are building your own classes, you can define this behaviour for them through the __bool__ dunder method.)

Let’s see some examples:

def bool_check_var(input_variable):
    if not (input_variable):
        print("Falsy")
    else:
        print("Truthy")


listy = []
other_listy = [1, 2, 3]


bool_check_var(listy)
Falsy
bool_check_var(other_listy)
Truthy

The method we defined doesn’t just operate on lists; it’ll work for many various other truthy and falsy objects:

bool_check_var(0)
Falsy
bool_check_var([0, 0, 0])
Truthy

Note that zero was falsy, its the nothing of a float, but a list of three zeros is not an empty list, so it evaluates as truthy.

bool_check_var({})
Falsy
bool_check_var(None)
Falsy

Knowing what is truthy or falsy is useful in practice; imagine you’d like to default to a specific behaviour if a list called list_vals doesn’t have any values in. You now know you can do it simply with if list_vals.

Lambda functions#

Lambda functions are a very old idea in programming, and are part of the functional programming paradigm. Coding languages tend to be more object-oriented or functional, with the object-oriented approach originating with Alan Turing’s “Turing Machines” and the functional approach with Alonso Church’s “lambda calculus”. These two approaches are mathematically equivalent and, on a more practical note, high-level programming languages often mix both. As examples, Haskell is strongly a functional language, statistics language R leans toward being more functional, Python is slightly more object oriented, and powerhouse languages like Fortran and C are object-oriented. However, despite being less functional than some languages, Python does have lambda functions, for example:

plus_one = lambda x: x + 1
plus_one(3)
4

For a one-liner function that has a name it’s actually better practice here to use def plus_one(x): return x + 1, so you shouldn’t see this form of lambda function too much in the wild. However, you are likely to see lambda functions being used with dataframes and other objects. For example, if you had a dataframe with a column of string called ‘strings’ that you want to change to “Title Case” and replace one phrase with another, you could use lambda functions to do that (there are better ways of doing this but this is useful as a simple example):

import pandas as pd

df = pd.DataFrame(
    data=[["hello my blah is Ada"], ["hElLo mY blah IS Adam"]],
    columns=["strings"],
    dtype="string",
)
df["strings"].apply(lambda x: x.title().replace("Blah", "Name"))
0     Hello My Name Is Ada
1    Hello My Name Is Adam
Name: strings, dtype: object

More complex lambda functions can be constructed, eg lambda x, y, z: x + y + z. One of the best use cases of lambdas is when you don’t want to go to the trouble of declaring a function. For example, let’s say you want to compose a series of functions and you want to specify those functions in a list, one after the other. Using functions alone, you’d have to define a new function for each operation. With lambdas, it would look like this (again, there are easier ways to do this operation, but we’ll use simple functions to demonstrate the principle):

number = 1
for func in [lambda x: x + 1, lambda x: x * 2, lambda x: x ** 2]:
    number = func(number)
    print(number)
2
4
16

Note that people often use x by convention, but there’s nothing to stop you writing lambda horses: horses**2 (apart from the looks your co-authors will give you).

Exercise

Write a lambda function that takes the square root of an input number.

If you want to learn more about lambda functions, check out these short video tutorials.

Splat and splatty-splat#

You read those right, yes. These are also known as “unpacking operators” for iterables that are fed into functions as arguments (in the form of a tuple) and keyword arguments (in the form of a dictionary) respectively. Splat is * and splatty-splat is **. Because they unpack, they allow us to efficiency send packages of arguments or keyword arguments into functions without labouriously writing out every single argument.

Because function arguments are always tuples, the use of * must be accompanied by a tuple. Because function keywords are always dictionaries of key, value pairs, the use of ** must always be accompanied by a dictionary.

Let’s take a look at splat, which unpacks tuples into function arguments. If we have a function that takes two arguments we can send variables to it in different ways:

def add(a, b):
    return a + b


print(add(5, 10))

func_args = (6, 11)

print(add(*func_args))
15
17

The splat operator, *, unpacks the variable func_args into two different function arguments.

Perhaps surprisingly, we can use the splat operator in the definition of a function. For example, sum_elements below

def sum_elements(*elements):
    return sum(*elements)


nums = (1, 2, 3)

print(sum_elements(nums))

more_nums = (1, 2, 3, 4, 5)

print(sum_elements(more_nums))
6
15

Exercise

Write a function multiply that multiplies two input numbers, a and b, together and returns the answer. Send the argument (10, 12) to it using the splat operator.

Splatty-splat, **, unpacks dictionaries into keyword arguments (aka kwargs):

def function_with_kwargs(a, x=0, y=0, z=0):
    return a + x + y + z


print(function_with_kwargs(5))

kwargs = {"x": 3, "y": 4, "z": 5}

print(function_with_kwargs(5, **kwargs))
5
17

Exercise

Using a dictionary and splatty-splat with the function_with_kwargs function, find the sum of 9, 6, 13, and 2.

Time#

Let’s do a quick dive into how to deal with dates and times. This is only going to scratch the surface, but should give a sense of what’s possible. For more, see the Introduction to Time chapter.

The built-in library that deals with datetimes is called datetime. Let’s import it and ask it to give us a very precise account of the datetime (when the code is executed):

from datetime import datetime

now = datetime.now()
print(now)
2024-01-05 15:32:46.912755

You can pick out bits of the datetime that you need:

day = now.day
month = now.month
year = now.year
hour = now.hour
minute = now.minute
print(f"{year}/{month}/{day}, {hour}:{minute}")
2024/1/5, 15:32

Exercise

Using an f-string, add seconds to the date and time string above.

To add or subtract time to a datetime, use timedelta():

from datetime import timedelta

new_time = now + timedelta(days=365, hours=5)
print(new_time)
2025-01-04 20:32:46.912755

To take the difference of two dates:

from datetime import date
year_selection = 2030
new_year = date(year=year_selection, month=1, day=1)
time_till_ny = new_year - date.today()
print(f"{time_till_ny.days} days until New Year {year_selection}")
2188 days until New Year 2030

Note that date and datetime are two different types of objects-a datetime includes information on the date and time, whereas a date does not.

Miscellaneous Fun#

Here are some other bits of basic coding that might be useful. They really show why Python is such a delightful language.

You can use unicode characters for variables

α = 15
β = 30

print(α / β)
0.5

You can swap variables in a single assignment:

a = 10
b = "This is a string"

a, b = b, a

print(a)
This is a string

itertools offers counting, repeating, cycling, chaining, and slicing. Here’s a cycling example that uses the next keyword to get the next iteraction:

from itertools import cycle

lorrys = ["red lorry", "yellow lorry"]
lorry_iter = cycle(lorrys)

print(next(lorry_iter))
print(next(lorry_iter))
print(next(lorry_iter))
red lorry
yellow lorry
red lorry

itertools also offers products, combinations, combinations with replacement, and permutations. Here are the combinations of ‘abc’ of length 2:

from itertools import combinations

print(list(combinations("abc", 2)))
[('a', 'b'), ('a', 'c'), ('b', 'c')]

Find out what the date is! (Can pass a timezone as an argument.)

from datetime import date

print(date.today())
2024-01-05

Because functions are just objects, you can iterate over them just like any other object:

functions = [str.isdigit, str.islower, str.isupper]

raw_str = "asdfaa3fa"

for str_func in functions:
    print(f"Function name: {str_func.__name__}, value is:")
    print(str_func(raw_str))
Function name: isdigit, value is:
False
Function name: islower, value is:
True
Function name: isupper, value is:
False

Functions can be defined recursively. For instance, the Fibonacci sequence is defined such that \( a_n = a_{n-1} + a_{n-2} \) for \( n>1 \).

def fibonacci(n):
    if n < 0:
        print("Please enter n>0")
        return 0
    elif n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)


[fibonacci(i) for i in range(10)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]