6 | Functions Revisited


Functions are a key part of all modern programming languages. When you use functions in your coding, you’re packaging code that has a specific purpose into a self-contained unit. You can then call the function whenever you want your code to perform that particular action. This is true whether you’re using functions you’ve written yourself or ones that you find in modules you import.

In the first part of this book, you learned how to define your own functions and how to include parameters. In this Chapter, you’ll dive deeper into this topic.

Function Names and Function Calls

In the White Room analogy, the name of a function is the label on the door that leads to the Function Room. The function name is not enough to run the function. It’s just a label indicating which function you are referring to. To execute the code in the function, you’ll need to call the function by adding parentheses after the function name.

You can explore this with a simple function that you define in the Console. Note that when you define functions in the Console, you’ll need to hit Enter/Return on the last line to indicate the end of the indented block:

>>> def do_something():
...    print("This function is not the most interesting one you will ever write!")
...    
>>> do_something
<function do_something at 0x7fe94592f0d0>

>>> do_something()
This function is not the most interesting one you will ever write!

If you just type the function name do_something, Python shows you that this name refers to a function, and it shows the location in memory where the computer stores this function—you don’t need to worry about this last bit! When you type the function name, you’re asking Monty, who I introduced in The White Room Chapter as the computer program, to look around to find the name do_something. He’ll find this name on the label on the door leading to the Function Room.

By adding the parentheses after the function name, you’re calling the function. Now, you’re asking Monty to go through the door into the Function Room.

Functions used as arguments in other functions

Functions can be used as input arguments for other functions, in the same way as other Python objects can. Consider the following function:

>>> def repeat_five_times(function_name):
...    for count in range(5):
...        print(f"Function call {count+1}")
...        function_name()
...        

In the function repeat_five_times() you have just defined, look carefully at how you’re using the parameter function_name. On the last line of the function definition, you’re using the parameter name followed by parentheses. This means that when you call repeat_five_times() with the name of a function as an argument, repeat_five_times() will call this function on its last line.

You can call repeat_five_times() with the function name do_something as an argument:

>>> repeat_five_times(do_something)
Function call 1
This function is not the most interesting one you will ever write!
Function call 2
This function is not the most interesting one you will ever write!
Function call 3
This function is not the most interesting one you will ever write!
Function call 4
This function is not the most interesting one you will ever write!
Function call 5
This function is not the most interesting one you will ever write!

You can also try calling repeat_five_times with another function as the argument:

>>> repeat_five_times(print)
Function call 1

Function call 2

Function call 3

Function call 4

Function call 5
 

In this instance, the last line of the function repeat_five_times will run print() which displays a blank line. However, if you try to pass another data type as an argument for the function, you’ll get an error:

>>> repeat_five_times("hello")
Function call 1
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 4, in repeat_five_times
TypeError: 'str' object is not callable

The argument is now a string, but a string is not callable, as the error message says. You cannot place parentheses () after a string. In the following Chapter, you’ll learn about classes in Python that are also callable.

Returning Empty-Handed. Returning None

You have seen how you can add a return statement in a function definition. The function returns the data in the return statement to the scope that’s calling the function. Often this means that the function returns the data to the main program which calls it, but a function can also be called by another function.

What happens if functions don’t have a return statement? Monty, the computer program, returns from the Function Room empty-handed. The function returns nothing.

Except that Python has something to represent nothing! This is the object called None. If you want to create a variable with nothing in it, you cannot simply write a name and an equals sign with nothing after it. Instead, you can do the following:

>>> my_variable = None

You have now created a box labelled my_variable, and you stored Python’s None in it, which is Python’s way of representing nothing. Like everything else in Python, every object is of a certain data type:

>>> type(my_variable)
<class 'NoneType'>

The NoneType data type only has one value, None. If this seems confusing, don’t worry. It takes a while to get used to the concept of something that represents nothing!

You can probably guess what’s returned by a function that has no return statement:

>>> result = do_something()
This function is not the most interesting one you will ever write!

>>> print(result)
None

You’re now assigning whatever comes back from do_something() to the variable result, but as the function has no return statement, the object None is stored in the variable result. Here’s a common bug that can creep in some code:

>>> some_numbers = [4, 6, 34, 1, 5]
>>> output = some_numbers.append(100)
>>> print(output)
None

You’ll recall that lists are mutable data types. Therefore the method append() changes the list itself. This means that append() doesn’t return anything. Its action is to change the list it’s acting on. But since all functions and methods must return something, append() returns the object None.

Introducing the Contact List Program

In this and the following sections of this Chapter, you’ll explore the various options you have when including parameters in a function definition and therefore passing arguments when calling a function.

You want to keep track of your contact list and you decide to create a data structure to store this:

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

The variable contacts is a list that contains several dictionaries. In this example, you’ve created three dictionaries, one for each contact. These dictionaries have some keys that are common to all of them, such as "First Name", "Last Name", and "Email". They also have other keys that are specific to each item.

Formatting you code

You may have noticed that the code that defines the list of dictionaries is formatted onto several lines with indents instead of being written all in one line. This code is identical to the following one:

contacts = [{"First Name": "Clark", "Last Name": "Kent", "Email": "super@superman.com", "Superpowers": ["Flight", "Super Strength", "Super Speed"],}, {"First Name": "Sherlock", "Last Name": "Holmes", "Email": "elementary@holmes.com", "Colleague": "Dr Watson",}, {"First Name": "Yoda", "Last Name": None, "Email": "my_email_this_is@jedi.com", "Colour": "green",}]

Which version is easier to read, write and follow? When we write code, we want to make it as readable as possible. This includes formatting the code to increase readability and make it consistent with what other Python programmers do. Python has a style guide for writing code. These are not strict grammar rules that will give an error if you break them. They’re a guide to help make Python code written by different programmers look more consistent. You’ll read about PEP 8, Python’s style guide, in the Snippets section at the end of this Chapter.

Adding contacts to the contact list

You can now write the first draft of a function that will add a new contact to your list of contacts:

def add_person(first_name, last_name):
    contacts.append({"First Name": first_name, "Last Name": last_name})

You’ve defined a function with two parameters, first_name and last_name. Earlier in this book, you learned how you could use either positional arguments or keyword arguments when calling this function. When you include data in the same order as the parameters in the function signature, you’re using positional arguments:

add_person("James", "Bond")

You could also use keyword arguments by including the parameter name along with the value when calling the function:

add_person(first_name="James", last_name="Bond")

When using keyword arguments, you no longer need to follow the same order. The following function call is identical to the previous one:

add_person(last_name="Bond", first_name="James")

All of the three function calls above give the same result, so in this case, you can pick any one of the three to use in your code. You can now check what this function does by printing the list of contacts after the function call:

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(first_name, last_name):
    contacts.append({"First Name": first_name, "Last Name": last_name})

add_person(first_name="James", last_name="Bond")

print(contacts)

This code will now display the new list of dictionaries with the added contact:

[{'First Name': 'Clark', 'Last Name': 'Kent', 'Email': 'super@superman.com', 'Superpowers': ['Flight', 'Super Strength', 'Super Speed']}, {'First Name': 'Sherlock', 'Last Name': 'Holmes', 'Email': 'elementary@holmes.com', 'Colleague': 'Dr Watson'}, {'First Name': 'Yoda', 'Last Name': None, 'Email': 'my_email_this_is@jedi.com', 'Colour': 'green'}, {'First Name': 'James', 'Last Name': 'Bond'}]

It’s not easy to follow what’s going on in this output. Let’s fix this.

Displaying data structures using pretty print

It would be nice if Python could print out this data structure in a more readable way in the same way as how you’ve written it in your code. Luckily, there is a way. One of the modules in the standard library that’s installed on your computer when you install Python is pprint, which stands for pretty print. One of the functions inside the module pprint is also called pprint() and you can use it in the same way as you use the built-in print() function. You’ll also need to import the module first:

import pprint

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(first_name, last_name):
    contacts.append({"First Name": first_name, "Last Name": last_name})

add_person(first_name="James", last_name="Bond")

pprint.pprint(contacts)

The output from this code is the same list of dictionaries as before, but pprint() displays it in a more readable format:

[{'Email': 'super@superman.com',
  'First Name': 'Clark',
  'Last Name': 'Kent',
  'Superpowers': ['Flight', 'Super Strength', 'Super Speed']},
 {'Colleague': 'Dr Watson',
  'Email': 'elementary@holmes.com',
  'First Name': 'Sherlock',
  'Last Name': 'Holmes'},
 {'Colour': 'green',
  'Email': 'my_email_this_is@jedi.com',
  'First Name': 'Yoda',
  'Last Name': None},
 {'First Name': 'James', 'Last Name': 'Bond'}]

You can see that James Bond has been added as a new contact.

Using the function with other inputs

The design of this function is still not ideal. One of the main problems is that this function will only work with a list named contacts that must exist in the scope in which the function is called. This means that you can only use this function in a White Room in which there is already a box labelled contacts on the shelves, and this box must contain a list.

When you write functions, you should make them as flexible and reusable as possible. In this case, you can add an extra parameter so that when you call the function, you also include the list you want to use as an argument:

import pprint

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(contact_list, first_name, last_name):
    contact_list.append({"First Name": first_name, "Last Name": last_name})

add_person(contacts, first_name="James", last_name="Bond")

pprint.pprint(contacts)

The first parameter in the function definition is now contact_list. You’ve also included the name of the list contacts in the function call at the end of your code. This function can now be used on any list, not just contacts. You can create two contact lists called work_contacts and personal_contacts and experiment with adding new contacts to each list using the same function.

Functions With Optional Arguments: Default Values

Let’s start with an error message you’re already familiar with. If you call the function with one or more of the arguments missing, you’ll get an error message:

# ...
add_person()

This gives the following error:

Traceback (most recent call last):
  File "<path>/<filename>.py", line 27, in <module>
    add_person()
TypeError: add_person() missing 3 required positional arguments: 'contact_list', 'first_name', and 'last_name'

The message tells you that the function is missing required arguments from the function call. When you add parameters in the way you’ve done so far, you’re telling Python that you will provide arguments for each of those parameters. Monty must take three bits of information with him when he goes to the Function Room as there are three parameters listed in the function signature.

However, you can define functions with optional arguments. You can assign a default value to a parameter so that if you don’t pass any argument corresponding to that parameter, the function will use the default value. To assign a default value you can add it to the function signature as follows:

import pprint

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(contact_list, first_name, last_name=None):
    contact_list.append({"First Name": first_name, "Last Name": last_name})

add_person(contacts, first_name="James")

pprint.pprint(contacts)

There is now an equals sign following the parameter name last_name and the value None. This doesn’t mean that last_name will always be None. It means that if you don’t pass a value as an argument for last_name in the function call, then last_name will have the value None. Even though there’s no third argument in the function call now, this doesn’t give an error. The function adds a contact with the first name of James, but no last name:

[{'Email': 'super@superman.com',
  'First Name': 'Clark',
  'Last Name': 'Kent',
  'Superpowers': ['Flight', 'Super Strength', 'Super Speed']},
 {'Colleague': 'Dr Watson',
  'Email': 'elementary@holmes.com',
  'First Name': 'Sherlock',
  'Last Name': 'Holmes'},
 {'Colour': 'green',
  'Email': 'my_email_this_is@jedi.com',
  'First Name': 'Yoda',
  'Last Name': None},
 {'First Name': 'James', 'Last Name': None}]

You can still include a last name as an argument if you wish. In this case, the function will use this value and not the default value.

Adding parameters with default values for attributes needed for all contacts

As you’d like all contacts to have a key for the first name, last name, and email, you can add all three as parameters and assign default values. In this case, you’ll use None as the default value for each one:

import pprint

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(contact_list, first_name=None, last_name=None, email=None):
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )

add_person(contacts, first_name="James", last_name="Bond", email="007@mi6.com")
add_person(contacts, first_name="Spock")

pprint.pprint(contacts)

The call to contact_list.append() now spans three lines. This is not required in Python, but the PEP 8 style guide I mentioned earlier doesn’t like having long lines. Therefore, I’m splitting this into three lines for stylistic purposes only.

When you run this code, you’ll see that both James Bond and Spock have been added to the list of dictionaries. Spock has no last name or email, so these keys are still present in the dictionary but have the value None.

Order of items in a dictionary: A review

On a side note, you may have noticed that pprint() changes the order in which it displays the keys in a dictionary. You can probably guess the default order it’s using. As dictionaries are not sequences, the order of the items is not important. To stress this point, you can check what happens with these two dictionaries:

>>> first = {"a": 1, "b": 2}
>>> second = {"b": 2, "a": 1}
>>> first == second
True

These two dictionaries are equal, even though you didn’t enter the items in the same order when you created them. The same is not true for sequences such as lists:

>>> first = [1, 2]
>>> second = [2, 1]
>>> first == second
False

The order of the items is an important part of a sequence. Therefore, these two lists are not equal.

Let’s get back to functions with default parameters. The function you’ve written earlier used None as the default value for all three parameters. However, there may be instances where the default value may need to be another data type. Have a look at the following examples:

>>> def greet_person(name="there"):
...    print(f"Hello {name}. How are you doing today?")
    
>>> greet_person("Bob")
Hello Bob. How are you doing today?

>>> greet_person()
Hello there. How are you doing today?

And another example:

>>> def repeat_message(message, repeats=1):
...    for _ in range(repeats):
...        print(message)
        
>>> repeat_message("Good Day!", 4)
Good Day!
Good Day!
Good Day!
Good Day!

>>> repeat_message("Good Bye!")
Good Bye!

Did you spot that lonely underscore symbol in the for loop statement? You’ll recall that in the for loop statement, you need to create a variable to store the items you’re iterating through. This variable name goes after the for keyword. However, there are times when you don’t need to use this value in the for loop. You cannot leave the space between for and in blank, though, as that would raise a SyntaxError. You can either create a variable name that you never use or just put an underscore symbol. The underscore is the preferred option in these cases.

Dangers of Using Mutable Data Types as Default Arguments

Although you can use any data type as default value when you define a function, it doesn’t mean you should. Let’s look at a common pitfall by extending the contact list example you’ve already been working on.

You would like to add a default value for the contact_list parameter as well. The function should create an empty list if you don’t pass an existing list as an argument. You may therefore try the following:

import pprint

# Leaving out definition of the contacts list of dictionaries from this display

def add_person(contact_list=[], first_name=None, last_name=None, email=None):
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

personal_friends = add_person(first_name="Albert", last_name="Einstein")
pprint.pprint(personal_friends)

There are two changes to the function definition compared to the last version you worked on. The parameter contact_list now has a default value which is an empty list. You also added a return statement to return the list. Earlier, you could get away without a return statement as you were guaranteed that the list already exists in the main scope. However, you must include it now.

You’re also assigning the list returned by the function to a variable name when you call the function. In this case, these are your personal friends, so the variable name reflects this. The output of this code is what you would expect:

[{'Email': None, 'First Name': 'Albert', 'Last Name': 'Einstein'}]

You have a list containing one dictionary with Albert Einstein’s details.

What’s the problem with mutable default values?

You can now try to create a second contact list using the same method, this time for your work_colleagues:

import pprint

# Leaving out definition of the contacts list of dictionaries from this display

def add_person(contact_list=[], first_name=None, last_name=None, email=None):
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

personal_friends = add_person(first_name="Albert", last_name="Einstein")

work_colleagues = add_person(first_name="Isaac", last_name="Newton")
pprint.pprint(work_colleagues)

However, the output from this code is not what you were hoping for:

[{'Email': None, 'First Name': 'Albert', 'Last Name': 'Einstein'},
 {'Email': None, 'First Name': 'Isaac', 'Last Name': 'Newton'}]

The work_colleagues list contains two dictionaries, one with Albert Einstein’s details and the other with Isaac Newton’s. But Einstein shouldn’t be there are he’s a personal friend and not a work colleague. To explore this mystery a bit further, you can try to display the personal_friends list again at the end of your code:

import pprint

# Leaving out definition of the contacts list of dictionaries from this display

def add_person(contact_list=[], first_name=None, last_name=None, email=None):
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

personal_friends = add_person(first_name="Albert", last_name="Einstein")

work_colleagues = add_person(first_name="Isaac", last_name="Newton")
pprint.pprint(personal_friends)

And you’ll end up with both Einstein and Newton again in your contact list:

[{'Email': None, 'First Name': 'Albert', 'Last Name': 'Einstein'},
 {'Email': None, 'First Name': 'Isaac', 'Last Name': 'Newton'}]

Your code didn’t create a new list each time you called the add_person() function without arguments. Instead, it’s using the same list that the program has put aside to use just in case the default is needed. Each function call uses the same default list.

When you define add_person() with an empty list as the default value for contact_list, you’re placing an empty list in the Function Room, ready to be used if you need the default value. If Monty enters the Function Room empty-handed, he’ll use this default and place it in the box labelled contact_list in the Function Room. The function will store data in this list.

If Monty comes back later to the same Function Room and needs to use the default list again, this list will still have the information from Monty’s first visit.

Fixing the mutable default value problem

This is the reason why you shouldn’t use mutable data types as default values when defining a function. Immutable data types don’t suffer from this problem as they cannot be changed. If you’re using None or 0 or "hello" as your default values, it doesn’t matter that you’re using the same item of data each time you call the function as this will always remain the same value.

The correct approach to create a new list when you don’t pass an existing list as an argument is as follows:

import pprint

# Leaving out definition of the contacts list of dictionaries from this display

def add_person(contact_list=None, first_name=None, last_name=None, email=None):
    if contact_list is None:
        contact_list = []
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

personal_friends = add_person(first_name="Albert", last_name="Einstein")
work_colleagues = add_person(first_name="Isaac", last_name="Newton")

print("Personal Friends:")
pprint.pprint(personal_friends)

print("\nWork Colleagues:")
pprint.pprint(work_colleagues)

You’ve now set the default value for contact_list to None, and then you started your function definition by checking whether contact_list is the same as the value None. If you call the function with a list as an input argument, this if statement will evaluate as False. However, if there is no argument and contact_list defaults to None, your function creates a new empty list and reassigns contact_list to this new list. The function will create a new list each time it runs as long as the if block is executed.

The output of this code is now what you want from your function:

Personal Friends:
[{'Email': None, 'First Name': 'Albert', 'Last Name': 'Einstein'}]

Work Colleagues:
[{'Email': None, 'First Name': 'Isaac', 'Last Name': 'Newton'}]

The moral of the story: beware of mutable data types as default values in function definitions.

The Contact List program with default parameters

Before moving on to another type of optional argument, let’s tidy up the code you’ve got so far by adding all the parts you’ve worked on into a single script, with some updates to take into account the latest changes you made:

import pprint

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(contact_list=None, first_name=None, last_name=None, email=None):
    if contact_list is None:
        contact_list = []
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

contacts = add_person(
    contacts, first_name="James", last_name="Bond", email="007@mi6.com"
)
contacts = add_person(contacts, first_name="Spock")

personal_friends = add_person(first_name="Albert", last_name="Einstein")
work_colleagues = add_person(first_name="Isaac", last_name="Newton")

print("Personal Friends:")
pprint.pprint(personal_friends)

print("\nWork Colleagues:")
pprint.pprint(work_colleagues)

print("\nMain Contacts List:")
pprint.pprint(contacts)

As you’ve added a return statement to the function now, you have added an assignment to the calls to add_person() that added James Bond and Spock. Now, you have three separate lists:

  • contact is the original list that you defined at the top of your script and then later modified twice
  • personal_friends was created when you called add_person() with no input arguments and added Einstein
  • work_colleagues was created when you called add_person() with no input arguments and added Newton

There’s one more topic we can introduce now before you learn about *args and **kwargs.

Unpacking

Consider the following list and the two print() lines:

>>> numbers = [3, 6, 4, 23, 45, 67, 3]
>>> print(numbers)
[3, 6, 4, 23, 45, 67, 3]

>>> print(*numbers)
3 6 4 23 45 67 3

In the first print() function, you’re asking Python to print out the list. There is only one argument in the call to print(). However, in the second print() function, you’ve placed an asterisk * before the variable name. This asterisk is the unpacking operator. It’s unpacking the list into its constituent items. The second print() function has seven arguments, not one. You’ll notice that the second print() output doesn’t show the square brackets and the commas used to display lists.

The second print() is equivalent to the following line:

>>> print(3, 6, 4, 23, 45, 67, 3)
3 6 4 23 45 67 3

Let’s look at another example of unpacking. First, you can create three variables in a single line as follows:

>>> first, second, last = [35, 10, 90]
>>> first
35
>>> second
10
>>> last
90

The list following the assignment operator = has three items, and you’re creating three variables called first, second and last. Now let’s make this a bit more interesting:

>>> first, *rest, last = [35, 10, 100, 2, 34, 90]
>>> first
35
>>> rest
[10, 100, 2, 34]
>>> last
90

An asterisk precedes the variable rest. This means that the variable rest will collect all the items except for the first and last ones which have their own variables.

Functions With Optional Arguments: args and kwargs

Let’s consider a function that will work out how many times a certain letter appears in a string:

>>> def count_letters(letter, word):
...    return word.count(letter)
...
>>> count_letters("a", "hello")
0
>>> count_letters("l", "hello")
2

The function has two parameters:

  • the letter that you want to look for
  • the word that you want to investigate

Now, you want to make this function more flexible by allowing more words to be added as arguments and find the overall number of times the letter appears. You could add more parameters, one for each word. However, you want to be able to use this function with any number of words.

Using args

The solution is to use the *args parameter. Let’s first have a look at what the function signature looks like:

>>> def count_letters(letter, *args):

An asterisk precedes the parameter name args. This means that you can pass any number of optional arguments, and the function will collected all of them in the variable args. Is everything clear? No? Let’s explore this further:

>>> def count_letters(letter, *args):
...    print(args)
...    print(type(args))
...    
>>> count_letters("l", "hello")
('hello',)
<class 'tuple'>

>>> count_letters("l", "hello", "bye", "something else")
('hello', 'bye', 'something else')
<class 'tuple'>

Notice that both calls to count_letter() are valid, even though they have a different number of arguments. In both instances, the first argument "l" is the value for the parameter letter. All the remaining arguments are stored in args, which is a tuple. You can even call the function with just one argument:

>>> count_letters("l")
()
<class 'tuple'>

The tuple args is empty in this case. You’ll often see this parameter listed as *args, along with its cousin **kwargs, which you’ll meet soon. However, there’s nothing special about the names args and kwargs. They’re just variable names like any others. It’s the single and double asterisks that give them their properties, not the names. So you can write:

>>> def count_letters(letter, *words):
...    print(words)
...    print(type(words))
... 

Using descriptive variable names is always preferable. Therefore you may prefer not to use the names args and kwargs but instead to use variable names that best represent the data they hold.

You can now make a few additions to count_letters to take into account that you may have one or more words stored in the tuple words:

>>> def count_letters(letter, *words):
...    print(words)
...    print(type(words))
...    number = 0
...    for word in words:
...        number = number + word.count(letter)
...    return number
...

The print() lines are no longer needed, but I’ve left them in the code above. You’re initialising the variable number with the value 0 before the start of the loop. The function then iterates through the tuple words and adds the count of the letter you’re looking for to number:

>>> count_letters("l", "hello")
('hello',)
<class 'tuple'>
2

>>> count_letters("l", "hello", "bye", "something else")
('hello', 'bye', 'something else')
<class 'tuple'>
3

In the second call, the function adds up all occurrences of "l" in all of the strings.

Adding args to the Contact List program

Let’s use *args in the contact list example now. You’ll add another function to your script:

import pprint

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

def add_person(contact_list=None, first_name=None, last_name=None, email=None):
    if contact_list is None:
        contact_list = []
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

contacts = add_person(
    contacts, first_name="James", last_name="Bond", email="007@mi6.com"
)
contacts = add_person(contacts, first_name="Spock")

personal_friends = add_person(first_name="Albert", last_name="Einstein")
work_colleagues = add_person(first_name="Isaac", last_name="Newton")

# Following lines are commented out as you don't need the print outs now
# print("Personal Friends:")
# pprint.pprint(personal_friends)
#
# print("\nWork Colleagues:")
# pprint.pprint(work_colleagues)
#
# print("\nMain Contacts List:")
# pprint.pprint(contacts)

def show_all_contacts(*contact_lists):
    for contact_list in contact_lists:  # contact_lists is a tuple
        for person in contact_list:  # Each contact_list is a list of dictionaries
            print(person)  # And therefore person is a dictionary

show_all_contacts(contacts)

The function show_all_contacts() can take any number of arguments. In this case, the function call on the last line only has one contact list as an input. There are nested for loops in this function definition. A nested loop is when there’s a for loop inside another for loop.

You’ll need to do some mental gymnastics to keep track of the various data structures here. Let’s go:

  • contacts: the original list of dictionaries
  • contact_lists: a tuple since any parameter in a function signature preceded by * is a tuple. However, in this case, contact_lists is a tuple that contains only one item, contacts
  • contact_list: this is defined in the for statement and therefore, in this case, it’s the same as contacts, since contacts is the only item in contact_lists. This will make a bit more sense in the next example below
  • person: a dictionary, since contact_list is a list of dictionaries.

You’ll now need a strong coffee before you reread those bullet points!

The output from this code is:

{'First Name': 'Clark', 'Last Name': 'Kent', 'Email': 'super@superman.com', 'Superpowers': ['Flight', 'Super Strength', 'Super Speed']}
{'First Name': 'Sherlock', 'Last Name': 'Holmes', 'Email': 'elementary@holmes.com', 'Colleague': 'Dr Watson'}
{'First Name': 'Yoda', 'Last Name': None, 'Email': 'my_email_this_is@jedi.com', 'Colour': 'green'}
{'First Name': 'James', 'Last Name': 'Bond', 'Email': '007@mi6.com'}
{'First Name': 'Spock', 'Last Name': None, 'Email': None}

There are five dictionaries printed out because contacts contains five items, the three defined at the top of the script and then the extra two added when you called add_person() with contacts as an argument.

Calling the function with several optional arguments

However, show_all_contacts() can be called with more arguments. You can replace the last line in your script with the following:

# ...

def show_all_contacts(*contact_lists):
    for contact_list in contact_lists:  # contact_lists is a tuple
        for person in contact_list:  # Each contact_list is a list of dictionaries
            print(person)  # And therefore person is a dictionary

show_all_contacts(contacts, personal_friends)

In this case, there are two arguments in the function call, both of which will be stored in the tuple contact_lists within the function. The output in this case also shows Einstein as it includes all contacts in both lists:

{'First Name': 'Clark', 'Last Name': 'Kent', 'Email': 'super@superman.com', 'Superpowers': ['Flight', 'Super Strength', 'Super Speed']}
{'First Name': 'Sherlock', 'Last Name': 'Holmes', 'Email': 'elementary@holmes.com', 'Colleague': 'Dr Watson'}
{'First Name': 'Yoda', 'Last Name': None, 'Email': 'my_email_this_is@jedi.com', 'Colour': 'green'}
{'First Name': 'James', 'Last Name': 'Bond', 'Email': '007@mi6.com'}
{'First Name': 'Spock', 'Last Name': None, 'Email': None}
{'First Name': 'Albert', 'Last Name': 'Einstein', 'Email': None}

And let’s finish off by adding the third of the contact lists you have in your program:

# ...

def show_all_contacts(*contact_lists):
    for contact_list in contact_lists:
        for person in contact_list:
            print(person)

show_all_contacts(contacts, personal_friends, work_colleagues)

Let’s go through the various data structures as you did in the first example:

  • contacts, personal_friends and work_colleagues: all lists of dictionaries
  • contact_lists: a tuple as any parameter in a function signature preceded by * is a tuple. In this case, contact_lists is a tuple that contains three items: contacts, personal_friends and work_colleagues
  • contact_list: is defined in the for statement. The outer for loop will iterate three times as that’s how many items there are in contact_lists. The variable contact_list will therefore be equal to contacts in the first iteration, personal_friends in the second, and work_colleagues in the last
  • person: a dictionary. The inner loop will iterate for as many dictionaries as there are in each list of dictionaries

Moving through different data types within an algorithm and keeping track of what’s happening is an acquired skill. You’ll get better at it as you write more code. Here’s the output from your final script:

{'First Name': 'Clark', 'Last Name': 'Kent', 'Email': 'super@superman.com', 'Superpowers': ['Flight', 'Super Strength', 'Super Speed']}
{'First Name': 'Sherlock', 'Last Name': 'Holmes', 'Email': 'elementary@holmes.com', 'Colleague': 'Dr Watson'}
{'First Name': 'Yoda', 'Last Name': None, 'Email': 'my_email_this_is@jedi.com', 'Colour': 'green'}
{'First Name': 'James', 'Last Name': 'Bond', 'Email': '007@mi6.com'}
{'First Name': 'Spock', 'Last Name': None, 'Email': None}
{'First Name': 'Albert', 'Last Name': 'Einstein', 'Email': None}
{'First Name': 'Isaac', 'Last Name': 'Newton', 'Email': None}

All of your contacts are now included in the output printed.

Using kwargs

Using *args as a parameter allows you to include any number of optional arguments when you call the function. Using **kwargs will allow you to include any number of optional keyword arguments.

Let’s introduce kwargs with a ‘toy function’ first:

>>> def do_something(**kwargs):
...    print(kwargs)
...    print(type(kwargs))
...    
>>> do_something(name="Jane", age=32, profession="Scientist")
{'name': 'Jane', 'age': 32, 'profession': 'Scientist'}
<class 'dict'>

The function will collect any number of keyword arguments into one variable named kwargs which is a dictionary. The parameter names become the dictionary keys, and the values of the arguments are the dictionary values. The function do_something() can take any number of keyword arguments:

>>> do_something()
{}
<class 'dict'>

>>> do_something(x=5, y=True)
{'x': 5, 'y': True}
<class 'dict'>

You can now return to the contact list project.

Using kwargs in the Contact List program

Let’s have a look again at the first list of dictionaries you created:

contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

All contacts have a first name, a last name, and an email entry, but they can have different attributes beyond that. You’ve listed some of Superman’s powers in his entry, Sherlock Holmes’s colleague, and Yoda’s colour. You can now modify the function add_person() to include **kwargs as a parameter so that when you call the add_person() function, you’ll be able to add any attribute you wish in addition to first name, last name, and email.

As was the case for args, there’s nothing special about the variable name kwargs. It’s the double-asterisk that matters. You can now add an extra parameter to the function. I’m again formatting the code to adhere to the Python style guide, but it’s fine if your version has different formatting:

import pprint

def add_person(
    contact_list=None, first_name=None, last_name=None, email=None, **additional_details
):
    if contact_list is None:
        contact_list = []
    # Create the dictionary with the keys that will always be present, even if their
    # value can be None
    data = {"First Name": first_name, "Last Name": last_name, "Email": email}
    # Iterate through the dictionary additional_details and add these attributes to data
    for key, value in additional_details.items():
        data[key] = value
    # Append the dictionary data to the contact list
    contact_list.append(data)

    return contact_list

# Call function without the contact_list argument, which means a new list is created
scientists = add_person(
    first_name="Rosalind",
    last_name="Franklin",
    subject="Chemistry",
    colleagues=["James Watson", "Francis Crick"],
)

# Function is now called with the keyword argument contact_list=scientists
scientists = add_person(
    contact_list=scientists,
    first_name="Grace",
    last_name="Hopper",
    subject="Computer Programming",
    firsts="Use of the term 'bug'",
)

scientists = add_person(
    contact_list=scientists,
    first_name="Marie",
    last_name="Curie",
    honours=["Nobel Prize in Chemistry", "Nobel Prize in Physics"],
)

pprint.pprint(scientists)

The function signature now has an extra parameter **additional_details compared to the last version of the function. The dictionary additional_details will store all the keyword arguments other than first_name, last_name, and email.

In the function definition:

  • You first create the dictionary data to store the information using the keys that must always be present: "First Name", "Last Name", and "Email"
  • Then, you loop through the dictionary additional_details using the method you learned about in the first part of this book. As you do so, you add the keys and their values to the dictionary data
  • Finally, you add data to the list containing the contacts, which you then return

Your first function call uses the fact that if there is no contact_list argument in the call, a new contact list is created, which you’re calling scientists. The rest of the function calls then use scientists as an input argument to add new items to this list.

Note that different people have different attributes stored in your list. Here’s the output from pprint():

[{'Email': None,
  'First Name': 'Rosalind',
  'Last Name': 'Franklin',
  'colleagues': ['James Watson', 'Francis Crick'],
  'subject': 'Chemistry'},
 {'Email': None,
  'First Name': 'Grace',
  'Last Name': 'Hopper',
  'firsts': "Use of the term 'bug'",
  'subject': 'Computer Programming'},
 {'Email': None,
  'First Name': 'Marie',
  'Last Name': 'Curie',
  'honours': ['Nobel Prize in Chemistry', 'Nobel Prize in Physics']}]

Using Docstrings

Let’s go back to a shorter and simpler version of the add_person() function for this section. In my version, the script is called testing_docstrings.py. You’ll need to use the name of this script later on:

# testing_docstrings.py

def add_person(contact_list=None, first_name=None, last_name=None, email=None):
    if contact_list is None:
        contact_list = []
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

When you write functions, it’s best practice to write some documentation to explain what the function does and how to use it. You can include this information as a string enclosed in triple double quotes """ """ immediately after the function signature. This is called a docstring. In IDEs such as PyCharm, if you add triple double quotes after the function signature and press Return/Enter, the IDE will automatically add a template for your docstring that you can then fill in.

Here’s an example of a docstring for the function above:

# testing_docstrings.py

def add_person(contact_list=None, first_name=None, last_name=None, email=None):
    """
    Function that adds a new entry to a contact list.
    :param contact_list: list of dictionaries, default=None. If None, a new list is
    created. Otherwise, new person added to existing list
    :param first_name: str, default=None
    :param last_name: str, default=None
    :param email: str, default=None
    :return: list of dictionaries

    Usage examples:
    >>> add_person(first_name="Mary", last_name="Smith")
    >>> add_person(first_name="Mary", last_name="Smith", email="mary@mamrysmith.com")
    >>> add_person(contacts, "Mary", "Smith")
    >>> add_person(
    ... contact_list=contacts,
    ... first_name="Mary",
    ... last_name="Smith",
    ... email="mary@marysmith.com",
    ... )
    """
    if contact_list is None:
        contact_list = []
    contact_list.append(
        {"First Name": first_name, "Last Name": last_name, "Email": email}
    )
    return contact_list

There are different format styles for docstrings, so you’ll see different versions as you explore Python code written by others. However, most docstrings will have the following information:

  • A brief description of the function
  • The parameters, including what data type is expected and any default parameters
  • Information about the data returned, including the data type

In the example above, the docstring also includes some examples of function calls. Docstrings are there to help others understand how to use the function. They will also help you remember the details of a function you wrote.

The docstring in functions is the help that’s displayed when you use the help() built-in function. Let’s access the function defined in the script above from a Console session. In my version, the script above is a file named testing_docstrings.py, however, your version may have a different file name. The file name shouldn’t have any spaces:

>>> from testing_docstrings import add_person
>>> help(add_person)
Help on function add_person in module testing_docstrings:
add_person(contact_list=None, first_name=None, last_name=None, email=None)
    Function that adds a new entry to a contact list.
    :param contact_list: list of dictionaries, default=None. If None, a new list is
    created. Otherwise, new person added to existing list
    :param first_name: str, default=None
    :param last_name: str, default=None
    :param email: str, default=None
    :return: list of dictionaries
    
    Usage examples:
    >>> add_person(first_name="Mary", last_name="Smith")
    >>> add_person(first_name="Mary", last_name="Smith", email="mary@mamrysmith.com")
    >>> add_person(contacts, "Mary", "Smith")
    >>> add_person(
    ... contact_list=contacts,
    ... first_name="Mary",
    ... last_name="Smith",
    ... email="mary@marysmith.com",
    ... )

This style of importing is a bit different to the import statements you’ve used so far. You’ll read more about this in a later chapter. The help() function displays the function signature and the docstring for the function.

Conclusion

You now have more in-depth knowledge of how to use functions in Python.

In this Chapter, you’ve covered:

  • What’s the difference between function names and function calls
  • When functions return None
  • How to define functions with optional arguments—parameters with default values
  • How to use args and kwargs
  • What docstrings as and how to use them to document functions

You need to understand functions for two reasons. You’ll need to write your own ones when you write programs, and when you import modules you’ll encounter many functions which you’ll need to learn how to use. Having a good understanding of how to recognise required and optional arguments, args and kwargs, return values, and other characteristics of functions will make it easier to learn how to use more advanced modules.


Snippets


Readability and Python’s Style Guide (PEP 8)

Consider the following code:

def ins(n):return n.split()[0][0].upper()+". "+n.split()[1][0].upper()+"."

Does it make perfect sense to you what this code does? No? You’re not alone. This code is difficult to understand, not because it’s difficult but because it lacks readability. There are many bad practices in the line of code above and many breaches of Python’s style guide too.

Before I outline the flaws in the above code, consider the following code:

def get_initials(full_name):
    """
    Get initials from a full name with two parts, such as 'John Smith'
    :param full_name: str, full name containing one first name and one last name,
    separated by a space
    :return: str with initials in the form 'J. S.'
    """
    full_name = full_name.upper()
    first_name, last_name = full_name.split()  # Assumes full_name has two words
    return f"{first_name[0]}. {last_name[0]}."

Does this code make sense? The two functions above perform the exact same task. Try them out:

# ...
print(ins("william shakespeare"))
print(get_initials("william shakespeare"))

The output from these two functions is the same:

W. S.
W. S.

Readability matters. It doesn’t matter for Monty, the computer program. However, when you write code, you’re not writing it only for the computer to read and execute. You’re writing code for humans, too, including your future self.

Let’s compare the two functions:

ins()get_initials()
Function nameThe function name is not clear. If you already know that this function gets initials from a name, then you may be able to guess that ins stands for initials, but if you’re trying to understand what the function does, then the name is not helpful.In this case, the function name gives a very good clue of what it does. It also follows the convention of starting with a verb.
Parameter nameThe parameter name n must stand for the word name. But once again, you need to know what the function does to be able to guess this. The single letter n doesn’t help the reader to understand how the function works.full_name is much clearer. You could use name too, but full_name provides more information. Yes, it’s longer, but the little extra time it will take to write a few more characters can save you a lot more time later. And IDEs have autocompletion too.
Number of linesThis function is a one-liner. It will save you from having to scroll lots in your code. But this “benefit” comes at a high price. Chaining lots of functions and indices to each other makes the logic harder to follow.Separating the various operations onto separate lines makes the logic easier to understand. You don’t need to go over the top, though. You could have added more lines to define first_letter_of_first_word and second_letter_of_second_word, say, but this would seem like overkill in this case.
Comments and DocstringsNone present. You have to deduce anything you can from the code.The docstring explains what the function does and what’s expected of the user, including the data types and formats of the argument needed and the return value.
SpacesCan you easily spot where one section ends and another starts? As there are hardly any spaces used in this line, there are no hints to the structure of the algorithm.Spaces around operators such as = and after commas make it easier to spot the structure and logic in a line of code.
Choosing the right toolsThis algorithm relies on string concatenation using the + operator. You’re adding four strings together, the ones coming from the split() and indexing, and the ones representing the full stops or periods.The use of an f-string shows the format of the final string more clearly.

Python has a style guide which sets out some conventions on how code should be formatted, how things should be named, and more. The purpose of a style guide is to:

  • Ensure that the ethos of the Python language is maintained as much as possible
  • Ensure consistency in Python code written by different programmers

Python’s style guide is outlined in PEP 8. PEPs —PEP stands for Python Enhancement Proposal— are documents that propose improvements to the language. Once Python’s Steering Council accepts them, they become part of the language!

You can read through PEP 8 when you feel you’re ready for this, but don’t worry too much about breaking the style guide for now. Think of these as guidelines rather than rules. However, as you become more proficient, you’ll want to start following these guidelines more closely.

I won’t go through every part of the PEP 8 style guide, but I’ll pick a few examples, some of which you have already read about in this or earlier Chapters.

Line length

You may have noticed a vertical line in your IDE at about two-thirds of the way in the script editor. One of the style guide’s recommendations is about line length. Look at the two version of the code below. You’ll need to scroll sideways to read the first one:

# Version 1
contacts = [{"First Name": "Clark", "Last Name": "Kent", "Email": "super@superman.com", "Superpowers": ["Flight", "Super Strength", "Super Speed"],}, {"First Name": "Sherlock", "Last Name": "Holmes", "Email": "elementary@holmes.com", "Colleague": "Dr Watson",}, {"First Name": "Yoda", "Last Name": None, "Email": "my_email_this_is@jedi.com", "Colour": "green",},]

# Version 2
contacts = [
    {
        "First Name": "Clark",
        "Last Name": "Kent",
        "Email": "super@superman.com",
        "Superpowers": ["Flight", "Super Strength", "Super Speed"],
    },
    {
        "First Name": "Sherlock",
        "Last Name": "Holmes",
        "Email": "elementary@holmes.com",
        "Colleague": "Dr Watson",
    },
    {
        "First Name": "Yoda",
        "Last Name": None,
        "Email": "my_email_this_is@jedi.com",
        "Colour": "green",
    },
]

This is a rather extreme example but it illustrates why you don’t want very long lines. Here is another example from code in this chapter:

# Version 1 (function signature only)
def add_person(contact_list=None, first_name=None, last_name=None, email=None, **additional_details):
  
# Version 2 (function signature only)
def add_person(
    contact_list=None, first_name=None, last_name=None, email=None, **additional_details
):

Unless you’re using a very small font size, you’ve had to scroll sideways to view all of the signature in version 1. With complex projects, it’s also very common to have several files open at the same time, and often you’ll want to view two of them side-by-side. Avoiding long lines will make life easier in those situations.

Variable and Function Names

You’ve seen how a name in programming cannot have any spaces, but often a name is made up of several words. There are various ways we can merge separate words together:

  • manywordsinonename
  • ManyWordsInOneName
  • manyWordsInOneName
  • many_words_in_one_name
  • Many_Words_In_One_Name

Different programming languages have different preferences on which of these are used for different things. In Python, variable and function names all use the lower snake case version: many_words_in_one_name. File names (which are module names) should also follow this convention.

UpperCamelCase is also used in Python. You’ve already seen this used for the names of error types such as SyntaxError and in the Boolean values True and False. You’ll see that this format is also the convention for the names of classes, which you’ll learn about soon.

Spaces

PEP 8 talks a lot about spaces, too—when to use them and when not to. Here are some examples:

# No spaces between function name and parentheses
print ("hello")  # No
print("hello")  # Yes

# And same when using square brackets for indexing and slicing
numbers = [3, 4, 5]
numbers [0]  # No
numbers[0]  # Yes

# Spaces around operators such as =
numbers=[3, 4, 5]  # No
numbers = [3, 4, 5]  # Yes

# But, no spaces when defining default values in a function signature
def do_something(some_parameter = None):  # No
def do_something(some_parameter=None):  # Yes

Incidentally, it’s quite common to have trailing commas, such as:

numbers = [3, 4, 5,]

The PEP 8 document also includes guidelines on where you should add blank lines, such as around function definitions. There’s even one recommendation that may seem bizarre: you should include a blank line at the end of a file. There is a good reason for this one too, but don’t worry if you ‘break’ this or any other rule.

Tools to Find and Fix PEP 8 Formatting Issues

You don’t need to worry too much about all the style guidelines for now. You certainly shouldn’t try to memorise the PEP 8 document! But there are some tools to help you adhere to the style guide.

If you’re using PyCharm as your IDE, you may notice short horizontal lines on the far right margin of the editor window. If you have errors in your code, these will be red. But you may also see yellow lines. The yellow lines are warnings. If you hover on these lines, you can see the warnings, and you’re likely to find that some of them say they’re PEP 8 warnings. You can also see these by opening the Problems tab from View/Tool Windows. Other IDEs will have similar features too.

There are also other tools that can check and indeed update the formatting for you automatically. One of these is Black. You’ll read more about formatting using Black later on.

You can read the blog post about Python readability for more about coding style and PEP 8.


Coming Soon…

The Python Coding Place

Sign-Up For Updates

The main text of the book is complete—I’m planning some additions and exercises and I’ll make this book available in other formats soon. Blog posts are also published regularly.

Sign-up for updates and you’ll also join the Codetoday Forum where you can ask me questions as you go through this journey to learn Python coding.

Follow me on Twitter, too