Advanced Python

Day 17: Exercise Solutions

Python Guru with a screen instead of a face, typing on a computer keyboard with a dark teal background to match the day 17 image.

Here are our solutions for the day 17 exercises in the 30 Days of Python series. Make sure you try the exercises yourself before checking out the solutions!

1) Create a function that accepts any number of numbers as positional arguments and prints the sum of those numbers.

Before we get started it's very important that we remember not to call our new function sum. Why? Because there's already a sum function as part of Python's built-ins, and we don't want to make that function inaccessible by reusing the name!

One common convention when we want to use the same name as a built in function, or a keyword, is to put an underscore in the name, like this: sum_. You could also go for a totally different name like multi_add if you like.

Let's go got multi_add because it sounds more fun.

def multi_add():
    pass

Since we want to take in any number of values here, we know we need the * operator for our parameter, but we need to decide what to call that parameter.

We could certainly used *args, but here we know what the arguments are. They're values or numbers. Either of the following would be fine:

def multi_add(*values):
    pass

def multi_add(*numbers):
    pass

I'm going to use the second option, because it's even more specific, in my opinion.

Now that we have this tuple of numbers in our numbers parameter, we can just pass this value to the build in sum function and print the result.

def multi_add(*numbers):
    print(sum(numbers))

Be careful not to write print(sum(*numbers)), as sum expects an iterable, not lots of values.

2) Create a function that accepts any number of positional and keyword arguments, and that prints them back to the user. Your output should indicate which values were provided as positional arguments, and which were provided as keyword arguments.

Unlike in the last exercise, we really have no idea what the data we're receiving is, so this is a case for *args and **kwargs. We need both here because we want to accept both any number of positional arguments, and any number of keyword arguments.

I'm going to call my function arg_printer, but feel free to choose whatever name you like, as long as it describes the function well.

def arg_printer(*args, **kwargs):
    pass

Now that we have the parameters set up, printing them is fairly simple. We really only need to do this:

def arg_printer(*args, **kwargs):
    print(f"Positional arguments are: {args}")
    print(f"Keyword arguments are: {kwargs}")

Now if we call the function with a range of random values as arguments:

arg_printer(1,  "blue",  [1,  23,  3], height=184, key=lambda x: x ** 2)

We get this:

Positional arguments are: (1, 'blue', [1, 23, 3])
Keyword arguments are: {'height': 184, 'key': <function <lambda> at 0x7f1b7c44f1f0>}

I think we can do a little better than this though. There are two things I want to accomplish here:

  1. I don't want the brackets to show up around the positional arguments. I just want a series of comma separated values.
  2. I want the keyword arguments to be a series of comma separated values in this format: height=184.

Let's tackle the positional arguments first. I'm going to use join here, but this means all of our arguments need to be strings, so we need to do a bit of processing of the argument list. I'm going to do this with a comprehension:

def arg_printer(*args, **kwargs):
    args = [str(arg) for arg in args]
    print(f"Positional arguments are: {', '.join(args)}")

This is good, but not perfect, because there's no difference between something like "1" and 1 in the output. That's potentially misleading.

Instead of using str, I'm going to use the repr function, which is going to give us a different kind of string representation that aligns more with how we actually define the values. This is going to preserve the difference between 1 and "1".

You can find more information on repr in the documentation.

def arg_printer(*args, **kwargs):
    args = [repr(arg) for arg in args]
    print(f"Positional arguments are: {', '.join(args)}")

Now let's take care of the keyword arguments.

I want to use join again, so I'm going to create a list of strings, just like before. This time I'm going to iterate over the dictionary using the items method, and I'm going to interpolate the key and value into the string.

def arg_printer(*args, **kwargs):
    args = [repr(arg) for arg in args]
    print(f"Positional arguments are: {', '.join(args)}")

    kwargs = [f"{key}={repr(value)}" for key, value in kwargs.items()]

Once again I'm using repr here, but only for the values. This makes the output more in line with how we pass in keyword arguments.

Now we can just join the new kwargs list while printing:

def arg_printer(*args, **kwargs):
    args = [repr(arg) for arg in args]
    print(f"Positional arguments are: {', '.join(args)}")

    kwargs = [f"{key}={repr(value)}" for key, value in kwargs.items()]
    print(f"Keyword arguments are: {', '.join(kwargs)}")

Using our old set of arguments, we now get much nicer output like this:

Positional arguments are: 1, 'blue', [1, 23, 3]
Keyword arguments are: height=184, key=<function <lambda> at 0x7f2deaeed700>

3) Print the following dictionary using the format method and ** unpacking.

country = {
    "name": "Germany",
    "population": "83 million",
    "capital": "Berlin",
    "currency": "Euro"
}

We have a lot of freedom in how to actually output the data here, so I'm just going to largely mimic the dictionary format.

My template is going to look like this:

country_template = """Name: {name}
Population: {population}
Capital: {capital}
Currency: {currency}"""

Because we're planning to use ** unpacking, we need to use named placeholders, because that's how format maps keywords to the placeholders.

Now we can use our template like this:

country = {
    "name": "Germany",
    "population": "83 million",
    "capital": "Berlin",
    "currency": "Euro"
}

country_template = """Name: {name}
Population: {population}
Capital: {capital}
Currency: {currency}"""

print(country_template.format(**country))

The output in my case looks like this:

Name: Germany
Population: 83 million
Capital: Berlin
Currency: Euro

If you want to try this out with more values, create a list of country dictionaries and pass your template to print in a for loop.

4) Using * unpacking and range, print the numbers 1 to 20, separated by commas.

One thing we have to be careful to remember here, is that the stop value when defining a range is not inclusive. That means we need range(1, 21).

We can actually do this entire exercise on one line, which shows some of the power of this approach:

print(*range(1, 21), sep=", ")

Here I've unpacked the range with *, turning it into 20 individual integers, and I've defined the character to go between the values using the sep parameter. The value I chose was a comma followed by a space.

If we run the code, we get output like this:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20

5) Modify your code from exercise 4 so that each number prints on a different line. You can only use a single print call.

The only change we need to make to our code is changing the separator string that we pass to sep. Instead a comma and a space, I'm going to use "\n", which means that we'll put a line break between each value.

print(*range(1, 21), sep="\n")

Now all of the numbers are on a different line when we print them.