Py2o1 Course

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 213

Programming Paradigms

Welcome to your second module on the Python programming language. If


you haven't worked through the first module yet, then go there and make
sure you understand what's covered. This course will assume that you
already understand the foundational concepts that you learned about there.

Programming Paradigms

In the previous module, you wrote Python scripts that operated in a


procedural way. This means that your code executed from top to bottom,
and is a common way to write automation scripts, such as the ones you
wrote before.

There are a few different ways to write programs that approach this
challenge differently. Sometimes, these different ways are called
programming paradigms and are often divided into the following three
paradigms:

Procedural programming
Object-oriented programming
Functional programming

Some languages are designed to be written to primarily follow one of these


paradigms. For example, Java is focused on the object-oriented paradigm,
and Haskell focuses on functional programming.

Python is different. Python makes it relatively easy to write your code in any
of these three paradigms. This can be nice, because it allows you to tackle
challenges in different ways, but it can also muddy the waters and make it
harder to understand what either of these paradigms is, and when you would
want to use one over another.

But Python is pragmatic, and this set of modules will introduce you to each
of these paradigms and show you how you can follow them using Python.
This will give you a great start to learn a wide variety of other, more focused
programming languages if you ever feel the need to.

Procedural Programming Part Two

This module of the course will continue to focus on the procedural way of
writing code. You'll write scripts with your code instructions, and Python will
execute them from top to bottom.

However, it won't be a completely strict top-to-bottom. You've used loops


before to run certain tasks multiple times, and you've used control flow logic
to skip lines or jump to specific lines, depending on certain conditions.

In this course, you'll see additional ways that the flow of your program can
be altered by introducing functions. Functions are a common and important
aspect of programming. However, just because you'll learn to write
functions, doesn't mean that you're doing functional programming yet. You'll
learn more about functional programming towards the end of the course.

Strap on your learning boots and crack your knuckles. This second module
on procedural programming in Python packs a punch and by the end, you'll
have a good working understanding of Python. This module will fill your
Python knowledge so you'll feel as if you've gotten to know both snakes that
make up the Python logo:
After finishing this module, you'll know how to tackle almost any challenge
that you can handle with programming in Python. Your code won't be the
most effective, and it might not be the most elegant yet, but it'll work and
get the job done. You also won't understand everything that's happening yet,
but you'll know how to use Python to solve puzzles and build solutions.

Beyond this module, you'll dive deeper and keep pulling off more layers to
get a better understanding of programming in general and the Python
language specifically.
In the next lesson, you'll start by having a bit of fun with Python :)
Additional Data Types
In the previous course module you mainly got to know three foundational
data types:

1. Integer numbers: int


2. Floating-point numbers: float
3. Strings: str

In the following lessons, you'll get to know additional common data types in
Python. You'll read over a description of the data type, and then you'll have
access to a concise cheat sheet that sums up the most important aspects of
it.

Info: Don't attempt to remember everything. Get to know the data types,
spend some time with them, and then come again for a visit whenever you
feel like it.

So what are additional data types in Python, and why would you even need
them? You already got text and numbers covered, and you learned that you
can use them for different scenarios. The additional data types are just like
that. It makes sense for you to use them in specific situations because of
their specific characteristics.

One thing you'll remember about strings is that they are sequences of
characters. Strings are not the only sequential type of data in Python. In the
next lesson, you'll learn about another sequential data type called tuples.
Hello Tuples
Another data type that represents a sequence in Python is the tuple.

Info: The jury is out on how to correctly pronounce the word tuple. Some call
it a [toopl], others [tapl]. Seems like either one is fine ¯\_(ツ)_/¯

Tuples are similar to strings in some ways:

you can iterate over them


they are immutable

However, while strings are made up only of characters, tuples can consist of
elements of any data type, as described in the Python docs on Tuples and
sequences:

Tuples are immutable, and usually contain a heterogeneous sequence of


elements that are accessed via unpacking [...] or indexing [...].

You've already learned about indexing strings using square brackets ([]),
and you'll see that it works the same with tuples. You'll learn about the new
concept of unpacking tuples in this section. But before that, you'll read
about how to create a tuple.

Creating A Tuple

You can create a tuple in three different ways:

1. Commas: By separating multiple values with commas (,) in a variable


assignment
2. Parentheses: By wrapping multiple values into parentheses (()) and
separating them by commas (,)
3. Tuple Function: By using the tuple() function and passing it a
sequence object
The code snippet below will show you all three different ways of creating
tuples. They are interchangeable and all end up with the same tuple object:

# Commas
tup1 = 1, 42, "hello!"
print(tup1) # (1, 42, "hello!")

# Parentheses
tup2 = (1, 42, "hello!")
print(tup2) # (1, 42, "hello!")

# Tuple Function
tup3 = tuple([1, 42, "hello!"])
print(tup3) # (1, 42, "hello!")

As you can see, each of these ways to create a tuple gives you the same
result. You may also notice that, unlike with strings, your tuple can consist of
all sorts of different types of elements. In the case shown above, your tuples
consist of three elements each, two of the type int and one of the type str.

Note that you have to pass a sequence if you want to use the tuple()
constructor, which you do in this example with square brackets ([]). For now,
feel free to disregard this and just create your tuples using commas or
parentheses.

Tuples Are Sequences

Since tuples are sequences, just like strings, they also have a length that
you can access using len():

empty_tuple = ()
print(len(empty_tuple)) # OUTPUT: 0
tup = ("hello", "there")
print(len(tup)) # OUTPUT: 2

You can use len() just like you did with strings, in order to find out how
many elements are in a given tuple.

Because tuples are sequences, you can also iterate over their elements
using loops. The syntax for that is the same as if you'd iterate over a string.
However, instead of accessing each character of a string, you'll access each
element of your tuple:

for element in (1, 42, "hello!"):


print(element)

This code snippet creates a tuple using parentheses and iterates over each
element using a for loop. The variable name element that you're temporarily
assigning each item in the tuple to is an arbitrary variable name that you
should change to make it more descriptive for your specific use case.

For example, if you had a tuple of usernames and wanted to print each of
them out, you could write the same for loop more descriptively:

for user in ("Ada", "Bob", "Carol"):


print(user)

This loop construct does the same as the one further up. You're creating a
tuple and iterating over its elements to print each of them out. This is
possible because tuples are sequences, just like strings.

Tuple Elements Have An Index


You also learned that you can access characters in a string using indexing.
You can do the same with tuples, and access the elements in a tuple with the
same syntax you used for string indexing:

tup = (1, 42, "hello!")


print(tup[1]) # OUTPUT: 42

Just like a character in a string sequence, any element in a tuple sequence


has a unique index that you can use to access it by. Just like in strings, tuples
are 0-indexed, which means that you start counting with 0 instead of 1.

In the code snippet above, you first create a tuple with three elements, then
access the second element through its index, 1. To do this, you use the
square-bracket syntax you got to know earlier when working with strings.

Recap

Tuples are another immutable sequence data type in Python that behaves
similarly to strings in some aspects:

Tuples have a length that you can access with len().


You can iterate over a tuple using a loop.
You can access elements in a tuple using indexing with the square-
bracket-notation.

Tuples can contain any other data type as an element, which makes them
very versatile and useful.

Another aspect that tuples and strings have in common is that they are both
immutable. You'll revisit what that means and how it applies to tuples in the
upcoming lesson.
Tuples Are Immutable
Another aspect that connects strings and tuples is that they are both
immutable. Just like you can't replace a character in a string, you also can't
replace an element in a tuple.

You can, however, create a new tuple from an old tuple:

tup = (1, 42, "hello!")


tup2 = tup[:2]
print(tup2) # OUTPUT: (1, 42)

Just like with strings, you can also use the + operator to add two tuples
together and create a new tuple with the combined elements:

tup = (1, 42)


tup2 = (123, 999)
tup3 = tup + tup2
print(tup3) # OUTPUT: (1, 42, 123, 999)

As you can see, the new tuple tup3 contains all elements of tup and tup2
stuck together just like you'd expect it with a string.

Keep in mind that neither tup nor tup2 has changed at all. They are
immutable and can't change, but you can use them to create new tuple
elements. Like with strings, you can also overwrite the variable reference to
an existing tuple with a new tuple object:

tup = (1, 42)


tup2 = (123, 999)
tup = tup + tup2
print(tup) # OUTPUT: (1, 42, 123, 999)

Here you did the same thing as in the code snippet above, but instead of
assigning the output of the tuple concatenation to a new variable, tup3, you
overwrote the existing variable tup with a new tuple object value.

Info: The tuple object that tup was originally referring to never changed.
Instead, you discarded the reference to it and assigned the variable name
tup to a different tuple object. Python automatically finds and deletes such
abandoned objects that don't have any reference to them anymore.

You can train your understanding of this new data type by mobilizing your
knowledge about strings. Both data types behave similarly. However, you'll
soon notice that they have different use cases since tuples can be used as
containers for other data structures.

Tasks

Create a for loop and iterate over one of your tuples. Print out each
element.
Create a tuple that is a collection of only str elements. Access the
second letter of the second element in your tuple using indexing.
Iterate over your tuple full of strings and print out the last letter of each
string only. For this, you'll need to combine iteration and indexing.

Recap

Tuples are another immutable sequence data type in Python that behaves
similarly to strings. You can't change a tuple, but you can create a new tuple
object with changed content.

Unlike strings, tuples can also contain any other data type as an element,
which makes them very versatile and useful.
In the next lesson, you'll meet another sequence data type in Python called
list, which is similar to tuples in that it can contain any other data type, but
different because you can change the elements in a list.

Additional Resources

Python Documentation: Tuples and sequences


Hello Lists
Lists are an extremely common data type in Python. You'll likely use them all
the time in your programming work. Just like tuples, lists are sequences of
any other data type.

You can create lists of strings, lists of strings and numbers, lists of tuples
and integers, and even lists containing more lists, each of which contains a
string with the value "inception"... You get the point!

However, unlike tuples, lists are not carved in stone. You can change the
elements in a list, you can add to the same element without needing to
create a new one, as well as remove items from it. You'll start by creating a
new list that you'll work with for the rest of this lesson.

Creating Lists

There are two different ways to create a list in Python:

1. Using square brackets ([]) around elements that are separated by


commas (,)
2. Using list() on a sequence of elements that can be converted to a list

The code snippet below will show you both of these ways of creating a list:

# Square Brackets
list1 = [1, 42, "hello"]
print(list1) # OUTPUT: [1, 42, "hello"]

# List Function
tup = (1, 42, "hello")
list2 = list(tup)
print(list2) # OUTPUT: [1, 42, "hello"]
Using list() like in the example above is a type conversion similar to the
ones you have seen using str() and int(). To create a list from scratch,
you'll generally use the square brackets.

Lists Are Usually Homogenous

As you can see in the code snippet above, lists seem very similar to tuples so
far. They are sequences of elements, where the elements can be whatever
you want.

However, while it is possible and not a mistake at all to combine different


types of elements into a list in Python, you usually want to keep your lists
homogenous.

Info: Many programming languages enforce the data type of all the elements
in a sequence so that you wouldn't be able to mix them in the first place.
Python is more chill about it, but that doesn't mean that you need to abuse
the freedom that Python gives you! Often it can be helpful to apply some
limitations, even if they aren't enforced.

That means that you should avoid mixing data types inside a list, and instead
only use elements of one data type, e.g. only integers, or only strings, in a
single list.

Info: When you feel you need to combine different data types in one
sequence, it's often better to use a tuple instead. The fact that tuples are
immutable makes it less likely that you'll end up with unexpected bugs.

In the code snippet below, you'll create a homogenous list that contains only
strings, a bucket list of things a person might want to do in their life:

bucket_list = ["climb Mt. Everest", "eat fruits from a tree"]


If you don't relate to these life events, feel free to change the string elements
in your own bucket_list. Next, you'll revisit how you can pick out a single
element from a list.

List Elements Have Indices

Just like tuples and strings, list elements have indices that allow you to
access each element by its unique index position. Lists support indexing
through the same syntax that you've already gotten to know:

print(bucket_list[0]) # OUTPUT: "climb Mt. Everest"

You can also use the familiar square-bracket notation to create slices of your
lists or access them in steps.

Tasks

Revisit the lesson on string slicing and apply the concepts to your list.
Access the last element in a list using negative indexing.
Create a longer bucket list with at least six items. Then use slicing and
steps to pick out only every second element in the list. Don't use a loop
for this!

As you can see, both indexing and slicing work the same on lists as it does
on other data types that you've already gotten to know. Since lists are
sequences, there is yet another aspect that works the same as with these
data types.

Lists Support Iteration

You can iterate over a list using loops in the same way you did with strings
and tuples:
for task in bucket_list:
print(task)

Of course, there are more interesting things you can do with each list item
than just printing it out, but for now, you're just learning about the basic
concepts and you are always encouraged to go ahead and play around with
the concepts with your own ideas :)

In fact, iterating over lists to perform operations on each item is a powerful


and common operation that can help you to achieve a lot of tasks in
programming. Python's for loop makes this iteration intuitive.

Tasks

Refresh your understanding of how to use Python for loops by revisiting


the lesson on loops in the previous module.

Recap

Lists are sequences of elements that can be of any data type. However, it is
often useful to keep lists homogenous and only consist of elements of the
same data type.

You can index list elements, create slices, and iterate over lists, using the
same syntax as for strings and tuples.

After revisiting how lists are the same as these other data types, you are now
ready to find out how lists are different.

Additional Resources

Official Python Tutorial: Lists


Lists Are Mutable
When you're able to change an object after you've created it, you say that it
is mutable. Lists in Python are mutable, which gives you a whole set of new
possibilities, but also some challenges over immutable data types, such as
tuples.

Changing Elements In A List

First, you'll look at some of the cool things that you can do due to the fact
that lists are mutable. Remember your bucket list:

bucket_list = ["climb Mt. Everest", "eat fruits from a tree"]

After spending a month in solitude and pondering about life, the universe,
and everything, you realized that the second entry doesn't quite cover what
you tried to express with it. You want to keep your bucket_list object intact,
but now you want to reach in there, grab the second item, and change it to
match what you wanted to say. Because lists are mutable, you can do just
that:

bucket_list[1] += " that I planted"


print(bucket_list) # OUTPUT: ["climb Mt. Everest", "eat fruits from a tree that I pl

In this code snippet, you're using indexing to access the second element in
your bucket_list. This is the same process that you've seen in the previous
lesson. However, now you're not just accessing the element, but you are
changing it. Using the += operator, you're creating a new string object that
consists of the content of your old string ("eat fruits from a tree") and a
new string that you concatenated to the end of it (" that I planted").
You can see both types of mutability in action here:

1. Immutable: Strings are immutable, so you need to create a new string


object by concatenating the two strings. The new string object points to
the value "eat fruits from a tree that I planted". However, you
are not creating a new list object that has the second value replaced.
2. Mutable: Instead, you're replacing the second element inside the
existing bucket_list object. You do this by accessing it
(bucket_list[1]) and replacing its value (+= " that I planted").

Note: Remember that += is a shorthand assignment operator. a += 1 is the


same as a = a + 1. Return to the operators section in the previous module
to get a refresher on Python operators and how to use them.

By changing an element in your list, you effectively changed the existing list
object. If you attempted to do the same with a tuple, you'd run into a
descriptive TypeError.

Go ahead and give it a try! You might also find out that what you're doing
here is called item assignment. It means that you're assigning a value to an
item in your list.

Working with mutable objects can be useful, but it also has some drawbacks.
Sometimes strange things can happen when you forget that a list has
changed due to some action you took in another part of your code.

Aliasing

The mutability of lists can be especially tricky when you have created an
alias of a list.

Variables are pointers to values. You can have more than one pointer that
each point to the same value. This is called aliasing, and in the case of
mutable objects, such as lists, it can lead to undesired results:
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b) # True
print(a is b) # False

In this code snippet, you've created two lists, a and b, that both have the
same values. However, they are pointing to different objects in memory.
You're testing this with the equality operatory (==) and the identity operator
(is).

Info: If you want a refresher on the different Python operators and how they
work, check back to the relevant section in the first module of this course.

So you've established that you have two different lists that both hold the
same values. Now you're reassigning one of your variables to the first list so
that both variables a and b now point to the same list object:

b = a
print(a == b) # True
print(a is b) # True

There's nothing wrong with doing this, but it can introduce subtle bugs if you
don't stay aware of what you're doing here. You might think that you're still
referencing different objects with a and b, but in reality, they point to one and
the same list. Therefore, and because you can change lists, if you now make
a change to b, then it'll be reflected also in a:

b[0] = 4
print(a) # OUTPUT: [4, 2, 3]
After reassigning b to a, you were only dealing with one list object anymore.
However, you had two references to the same object. This is called aliasing.
Now when you used the variable b to change something in your list object,
you'll see the same changes also when you access the list through your
other reference to it, a.

Tasks

Check out this great interactive visual explanation to drill your understanding
of how mutability can have confusing effects when you alias lists.

Recap

Lists are mutable, which means that you can change a list object in place.
This can be helpful if you want your list to change state throughout your
program. However, it's also a common source of confusion and bugs,
especially when you're using aliasing to refer to the same list object with
different variables.

Lists are a commonly used data type in Python programs, so you'll get to
know some useful list methods in the next lesson.
List Methods
You've gotten to know string methods in a previous lesson. Many Python
objects you'll work with have methods associated with them. You'll learn
more about what methods are in an upcoming lesson. For now, you can think
of a list method as a way to do something with your list.

Appending And Popping Elements

You can use list methods to change your list, for example, to add elements to
the end of your list, and remove them as well:

bucket_list = ["climb Mt. Everest", "eat fruits from a tree that I planted"]

# Add an element
bucket_list.append("sail across the Atlantic ocean")
print(bucket_list) # OUTPUT: ["climb Mt. Everest", "eat fruits from a tree that I pl

# Remove an element
bucket_list.pop()
print(bucket_list) # OUTPUT: ["climb Mt. Everest", "eat fruits from a tree that I pl

Using list.append() you can add elements to the end of your list, and with
list.pop() you can remove the last item in your list.

In addition to removing the last element, list.pop() also returns the


removed element, which means that you can assign it to a variable and keep
using it in your program:

bucket_list = ["climb Mt. Everest", "eat fruits from a tree that I planted"]
next_task = bucket_list.pop()

print(next_task) # OUTPUT: "eat fruits from a tree that I planted"


print(bucket_list) # OUTPUT: ["climb Mt. Everest"]

You've successfully removed the last item from your bucket_list and
assigned it to a new variable next_task.

Removing And Inserting Items

Sometimes, however, you might want to remove a different item than the last
one. For this you can use a different list method called list.remove():

bucket_list = ["climb Mt. Everest", "eat fruits from a tree that I planted"]
bucket_list.remove("climb Mt. Everest")
print(bucket_list) # OUTPUT: ["eat fruits from a tree that I planted"]

When using list.remove() you can specify the value of the element that you
want to remove from your list. Keep in mind that you'll need to pass it the
exact value of the item you want to take out. You can make sure to avoid any
typos by indexing your bucket_list, which returns the value of the element:

bucket_list = ["climb Mt. Everest", "eat fruits from a tree that I planted"]
bucket_list.remove(bucket_list[0])
print(bucket_list) # OUTPUT: ["eat fruits from a tree that I planted"]

Using indexing to access the value of a list element and passing this value to
.remove(), allows you to effectively target the item that you want to take out
of your list.

Tasks

Practice using list.remove() by creating a list a = [1, 2, 3, 2] and


then removing the value 2.
What do you notice when you run this code?
Do all occurrences of 2 get removed?
If not, then which 2 has been removed? What direction does
list.remove() operate in?

Similarly to removing elements from specific positions in your list, you can
also insert them at specific positions. For that, you'll use list.insert(),
and you'll need to specify both the index where you want to insert the new
element, as well as its value:

bucket_list = ["climb Mt. Everest", "eat fruits from a tree that I planted"]

# Add an element
bucket_list.insert(1, "sail across the Atlantic ocean")
print(bucket_list) # OUTPUT: ["climb Mt. Everest", "sail across the Atlantic ocean",

You'll first add a number to specify where your new item should be inserted.
In this case, you chose the number 1, which means the value will be inserted
after the first position, which has the index of 0. The inserted element will be
at the index you specify here.

The second argument to list.insert() is the value of the element that you
want to add to your list. In the code snippet above, that is the string "sail
across the Atlantic ocean". Run this example in your Python interpreter
and confirm that you're getting the same result.

Sorting Lists

Another useful list method is list.sort(), which allows you to sort the items
in your list. You can apply this to lists of strings, which will be sorted
alphabetically by the beginning letters of your elements, or also to lists of
numbers:
a = [3, 0, 999, 42]
a.sort()
print(a) # OUTPUT: [0, 3, 42, 999]

When you call .sort() on your list, the items in the list get sorted in place.
This means that your list a has changed. This is only possible because lists
are mutable. You can confirm that your list has been sorted by printing it out.

Applying Operators On Lists

Some of the Python operators that you covered earlier can also be used with
lists. Revisit the previous lessons to refresh your knowledge on Python
operators, then use them to better get to know your new data type friend,
list.

Tasks

Try to apply Python operators such as +, *, /, etc. that you used with
numbers and strings to lists. Which ones work and which produce
errors?
Which operators do what you'd expect them to do?
Which operators work but produce unexpected results?

Many use cases of Python operators that apply to lists are intuitive and make
working with lists quicker.

Recap

You can perform actions on list objects via list methods. Some especially
useful methods are:

list.append(item)
list.pop()
list.remove(item)
list.insert(index, item)
list.sort()

All of these methods change the list that you are calling them on, which is
possible because lists are mutable.

You can also apply some of the Python operators you've gotten to know
earlier on lists.

Additional Resources

Python Documentation: More On Lists


List Comprehensions
When you venture out and hack through the Python jungle on the Internet,
you'll eventually encounter Python's list comprehensions:

names = ["Ada", "Bertha", "Carol"]


lowercase_names = [x.lower() for x in names]

In this code snippet, you define a list called names that contains a few names
as strings. In the second line, you're assigning the output of a list
comprehension to the variable lowercase_names. If you printed the result,
you'd see that it consists of a list with the same names in lowercase:

# OUTPUT:
['ada', 'bertha', 'carol']

The list comprehension in this code snippet is made up of the following


logic:

[x.lower() for x in names]

This might look like a strange sequence of code that you've never
encountered before. But you might also notice some parts of it that could
seem familiar:

the square brackets on each end make it look like a list


the for and in keywords remind you of for loops

In fact, list comprehensions are only one-liner shortcuts for for loops in
Python. Anything you can do with a list comprehension, you can also do with
a for loop. It just takes you a couple more lines of code:

names = ["Ada", "Bertha", "Carol"]

lowercase_names = []
for x in names:
lowercase_names.append(x.lower())

print(lowercase_names) # OUTPUT: ['ada', 'bertha', 'carol']

As you can see, the for loop did the same thing as the list comprehension
further up, the list comprehension was just more compact in achieving the
task.

Info: List comprehensions are one-liner for loops.

Unless you've already fallen in love with the concept of list comprehensions,
stick with writing your for loops in full length for now.

You'll revisit list comprehensions later on in this course when you'll also learn
about some other similar constructs that are generally known by the name of
comprehensions.

For now, it's enough that you understand what list comprehensions do and
that they're no mystery when you see them in a StackOverflow answer.

Tasks

Search StackOverflow for code snippets that contain list


comprehensions.
Rewrite the list comprehensions as a plain old for loops.

Recap
List comprehensions are a concise way to write for loops in Python. They
don't introduce any new functionality but can be an elegant way to avoid
writing verbose loops for short and uncomplicated code logic.

In the next lesson, you'll return to looking at data types as you'll get to know
another sequence type, called set.

Additional Resources

DigitalOcean: Understanding List Comprehensions


Hello Dictionaries
You've heard about numbers, strings, booleans, tuples, lists, and sets, and
now there's yet another data type to learn about. So what is a dictionary? Is
it a book with words in it:

Yes, that's right! And yes, that's a bad joke! But apart from that, it's also a
great metaphor. The name for this Python data type was chosen on purpose.

A Python dictionary (dict) is a mapping of key-value pairs:

Keys allow you to access their associated values.

As you might see, this is equivalent to how you can use a word in a real-
world dictionary to access that word's definition.

Another metaphor that you can think about, is comparing keys and values in
dictionaries to variables and values in programming. The dictionary key
represents a reference to the associated value in the dictionary.

Creating A Dictionary

You can create a dictionary using curly braces ({}) and adding keys and
values that are separated by a colon (:). Multiple entries are separated by
commas (,), just like elements in collections:

my_dict = {"greeting": "hello", "name": "martin"}

The dictionary my_dict has two items in it:

1. "greeting": "hello"
2. "name": "martin"

Each of these two items consists of a key and a value. If you take apart the
first element, then you'll see that it consists of:

1. key: "greeting"
2. value: "hello"

Tasks

What are the key and value of the second item in your dictionary?

In this example, you're using strings both as keys as well as values of your
dictionary. But that's not a requirement. You can use any Python data type as
values in your dictionary, however, you can only use immutable data types as
keys:
Keys need to be immutable data types, such as str, int, tuple, and
need to be unique
Values can be any Python data type and can contain duplicates

Dictionaries are different from collections you've encountered before in that


you access their values not by index, but by through a unique key. This also
means that you can't have duplicate keys in a Python dictionary. There can,
however, be many duplicate values in every dictionary.

Dictionaries are extremely useful data structures that allow you to build
complex programs and interactions. Now that you've created a new
dictionary, how do you access a value from it?

Performing A Dictionary Lookup

Accessing a value in a dictionary given a key is called a dictionary lookup.


In the following example dictionary users, that represents some fictional
users and their age, you'll access the value of the key "mary" using a
dictionary lookup:

users = {"mary": 22, "caroline": 99, "harry": 24}

print(users["mary"]) # OUTPUT: 22

You can access any of the other integer values with their respective keys.

Editing A Value

You can use a similar syntax as for a dictionary lookup also to change a value
in the dictionary:

users = {'mary': 22, 'caroline': 99, 'harry': 24}


users['harry'] = 20
print(users['harry']) # OUTPUT: 20

Because you're able to change a dictionary in place, this means that


dictionaries are mutable objects, just like lists. This allows you to use
dictionaries in your programs to keep track of the state and change it
throughout the lifetime of your program.

However, it also means that you can run into the same issues with aliasing
as you did when using lists. Return to the lesson on mutability in lists to
refresh your memory on what that means and why it's important to keep
track of.

Adding A Key-Value Pair

You can again use a similar syntax to add a new key-value pair to the
dictionary:

users = {'mary': 22, 'caroline': 99, 'harry': 24}

users['ludvik'] = 9
print(users['ludvik']) # OUTPUT: 9
print(users) # OUTPUT: {'mary': 22, 'caroline': 99, 'harry': 24, 'ludvik': 9}

If you're using the syntax to change the value of a key that doesn't exist yet,
it gets added as a new entry to your dictionary.

Recap

Dictionaries (dict) in Python are mutable mappings that consist of key-


value pairs:
Keys need to be immutable data types, such as str, int, tuple, and
need to be unique
Values can be any Python data type and can contain duplicates

Dictionaries are a commonly used data structure that gives you a lot of
flexibility to build complex programs with.

In the next lesson, you'll learn more about dictionaries as iterables, and how
you can iterate over their keys, values, or both.
Hello Sets
Another type of mutable collection in Python is a set. The Python Docs on
Sets use the following paragraphs to describe what sets are and how they
are used:

Python also includes a data type for sets. A set is an unordered


collection with no duplicate elements. Basic uses include membership
testing and eliminating duplicate entries. Set objects also support
mathematical operations like union, intersection, difference, and
symmetric difference.

If you're familiar with mathematical terms, then a set might seem already
intuitive to you. If not, then don't worry, you'll go over how to create one and
what you can use them for in this lesson.

Creating A Set

You can create a set in Python using set() and pass it to another collection
object. You can also create a new set by wrapping comma-separated values
in curly braces ({}):

s1 = {1, 2, 3} # OUTPUT: {1, 2, 3}


s2 = set([1, 2, 3]) # OUTPUT: {1, 2, 3}
s3 = set() # OUTPUT: {}

Both s1 and s2 are equivalent ways of creating a set. s2 uses set() to


convert a list into a set. The third example, s3, creates an empty set.

Note: If you want to create an empty set, then you need to use set(), not {},
as the latter creates an empty dictionary. You'll learn more about dictionaries
in an upcoming lesson.
After you've learned how to create a set, you'll now look at an example of
how sets can be useful.

Eliminating Duplicates

One frequent use of sets is to eliminate duplicate entries in a collection.

Imagine that you fetched all links from a website with your web scraping
script, and now you want to follow those links programmatically. However,
you only want to visit each destination once and some pages have multiple
paths to get to the same page, which is why you'll end up with multiple URLs
in your list.

You can convert the list of URLs to a set, which will remove all duplicates
and present you with a collection of unique URLs:

url_list = ['http://www.example.com', 'http://www.setsareuseful.com', 'http://www.exa

unique_urls = set(url_list)
print(unique_urls) # OUTPUT: {'http://www.example.com', 'http://www.setsareuseful.co

With a single line of code, you effectively removed all duplicate links from
your list. Your result set is also an iterable collection, which means that you
can use it in the same way you'd use a list to access each of the URLs in
your script.

Using Set Operations


You can do more with sets. In fact, if you are familiar with mathematical sets,
then you'll see that you can apply many set operations in the same way,
that you would in mathematics:

Set Operator
Set Methods Syntax Effect
Syntax
Returns a new set that contains all
A | B A.union(B)
elements of A and B.
A |= B A.update(B) Adds all elements of B to the set A.
Returns a new set that contains the
A & B A.intersection(B)
elements that are in both A and B.
Returns the the elements that are
A - B A.difference(B)
included in A, but not included in B.
Removes all elements of B from the set
A -= B A.difference_update(B)
A.

A < B Equivalent to A <= B and A != B


A > B Equivalent to A >= B and A != B

There are even more set operations. You don't have to learn all of them, but
rather keep aware that they exist and look them up if you need them.

Tasks

Practice using .union() and .update().

Recap

Python sets work similarly to mathematical sets. In practice, you'll likely use
them to quickly remove duplicates from a collection. You can create sets
with curly braces ({}) or the set() function. You can use set operations and
set methods to perform actions with sets.

In the next lesson, you'll get to know another very important data type that
you'll use frequently in programming. This data type is called a dictionary.

Additional Resources

Wikipedia: Basic Set Operations


Official Python Tutorial: Python Sets
Python Documentation: Python Sets
Real Python: Python Sets
Snakify: Set Tutorial
Dictionary Mappings
Dictionaries are iterable mappings, which means that you can iterate over
them similar to how you would with a collection, such as a string, a tuple, a
list, or a set. However, because dictionaries consist of keys and values, you'll
need to tackle iteration a bit differently:

users = {'mary': 22, 'caroline': 99, 'harry': 20}

for user, age in users.items():


print(user, age)

The dict.items() method gives you a list of tuple objects that you can
iterate over.

While there's also another way of accessing items in a dictionary, the pattern
shown in the code snippet above is the most straightforward to read. It
clarifies that you're iterating both over the keys and values of the dictionary.

But what if you want to only access the keys or the values?

Access Only Keys Or Only Values

You can also iterate over only the keys or only the values in a dictionary. In
fact, if you directly iterate over a dictionary without calling a method, such as
.items() on it, Python will iterate over the dictionary's keys:

users = {'mary': 22, 'caroline': 99, 'harry': 20}

for k in users:
print(k)

# OUTPUT:
# mary
# caroline
# harry

Alternatively, you can use the distinct dictionary method .keys() to iterate
over just the keys:

users = {'mary': 22, 'caroline': 99, 'harry': 20}

for k in users.keys():
print(k)

# OUTPUT:
# mary
# caroline
# harry

And if you need to access all values in a dictionary, you'll need to use
.values():

users = {'mary': 22, 'caroline': 99, 'harry': 20}

for v in users.values():
print(v)

# OUTPUT:
# 22
# 99
# 20

As you can see, dict.keys() allows you to iterate over only the keys of your
dictionary, while dict.values() allows you to access all the values.
Tasks

Change the age of "caroline" from 99 to 26.


Use the code playground below to age each user for 10 years.
Play around with the code and add or delete some dictionary entries.
How does that affect your output?

Explore More Dictionary Methods

Just like with other data types, there are a lot of methods you can use on
dictionaries to do something with them. You invoke these methods by calling
them on the dictionary object. For example, this is how you call the .clear()
method on your dictionary:

users = {'mary': 22, 'caroline': 99, 'harry': 20}


users.clear() # OUTPUT: {}

The dict.clear() method empties your dictionary by completely removing


all key-value pairs.

Head over to the documentation on Python dictionaries and explore some of


the dictionary methods. Try to implement some of them on your example
dictionary, e.g.:

dict.get()
dict.pop()
dict.update()

What can you accomplish when using these methods? Remember to keep
the documentation handy and practice reading it. It'll feel complicated and
unfamiliar at the beginning, but learning to be comfortable with reading
documentation is a vital skill for becoming a developer. So keep coming back
to it and don't let yourself get too frustrated if you don't understand
everything right from the start.

Recap

Dictionaries are iterable mappings of key-value pairs. If you directly iterate


over a dict, you're iterating over its keys. It's good to be explicit about what
you want to iterate over by using the appropriate method:

dict.keys() iterates over the keys of your dictionary.


dict.values() iterates over the values of your dictionary.
dict.items() iterates over the both the keys and values of your
dictionary, packaged in a tuple of the shape (key, value).

Like other data types, dictionaries also come with predefined methods that
allow you to perform common actions on your dictionaries. You can learn
more about them in the dictionary documentation.

In the next lesson, you'll recap all the new data types you've gotten to know,
before applying them to your projects.

Additional Resources

Official Python Tutorial: Dictionaries


Python Documentation: Dictionaries
Programiz: Dictionary Methods with examples
Recap Datatypes
In this section of your course, you got to know a whole lot of new data types.
You won't remember everything about all of them right from the start, but
you can always come back to revisit, or look up information about each data
type in Python's documentation on Built-in Types.

In the previous module of this course, you got to know:

int: Integer numbers, such as 12


float: Floating-point numbers, such as 42.0
str: Text data, consisting of collections of characters, such as "hello"

You were already able to do quite a lot by knowing just these data types and
some programming logic! But new data types expand your possibilities of
what you can accomplish with your programs.

In this module, you've learned about some additional important data types:

tuple: Immutable, ordered collections of any type, such as ("age", 33)


list: Mutable, ordered collections, of any type, such as [1, 2, 4, 8]
set: Unordered collections of unique items, such as {"hello",
"world", 42}
dict: Mutable mappings between keys and values, such as {"name":
"feivel", "age": 2}

There are many more data types, and you'll later learn that anything in
Python can be its own data type. What you got to know in this section are
just some common data types that you'll use over and over again.

Info: A data type in Python is nothing special and you'll later learn how to
build your own data types. Think of them as standardized and useful ways to
group-specific functionality, that allows you to tackle certain challenges.
Data types are a helpful concept to categorize types of data into separate
buckets. For example, there's something that's similar between all textual
data. And often, you'll want to be able to treat some types of categories in a
similar manner.

This is what data types were created for. They allow you to lump similar data
together, and perform common actions on that type of data.

Over your journey across the lands of Python, you'll keep encountering new
custom data types that someone has written to fulfill a specific goal. Often
it'll be useful to learn how to work with that data type and use the
abstraction that it provides for you.

On the other hand, by becoming familiar with the built-in data types that
you've gotten to know up to now, you'll be able to handle almost any
programming challenge that the world might throw at you.

Rejoice, give yourself a pat on the back, and get ready to apply some of the
built-in data types you got to know in this section to your projects next.
PROJECT: Adventure Game
In the previous module of this course, you built a text-based adventure game
that allowed you to interact with a few rooms through your command line.

If you haven't built the first version of this project yet, then make sure to
head back to the instructions in the previous module and give it a go! If
you're already done, then great work building it out!

Equipped with a new diamond sword and a couple of additional data types,
you're about to venture back into your text-based RPG to extend its
functionality and make it more immersive.

Game Mechanics Update

The game will continue in the fashion of a classic text-based Dungeons and
Dragons game. But, with the help of the additional data types you've gotten
to know, you'll start giving the player more choices:
Here are the tasks that you'll code up for this project. You'll read them below
in a list form, and as a first step, you can again copy that content over into a
new Python file as pseudocode:

Save the user input options you allow e.g. in a set that you can check
against when your user makes a choice.
Create an inventory for your player, where they can add and remove
items.
Players should be able to collect items they find in rooms and add them
to their inventory.
If they lose a fight against the dragon, then they should lose their
inventory items.
Add more rooms to your game and allow your player to explore.
Some rooms can be empty, others can contain items, and yet others
can contain an opponent.
Implement some logic that decides whether or not your player can beat
the opponent depending on what items they have in their inventory
Use the random module to add a multiplier to your battles, similar to a
dice roll in a real game. This pseudo-random element can have an effect
on whether your player wins or loses when battling an opponent.

Once you've got a working version done, show it again to your friends and
family and let them give it another spin. Feel free to share a link to your game
on the forum in the thread on Command-Line Games.
PROJECT: File Counter
In the previous module of this course, you wrote a script that was able to
move all your screenshots from your messy desktop into a new folder. In this
project, you'll revisit your file mover code and expand on it with the
knowledge of some additional data types, in order to make it more flexible
and powerful.

When you built your file mover, you used the pathlib module that worked
with Path() objects. You can consider Path() objects as yet another data
type, one that is more custom and not considered a built-in type. As you
might remember, you'll have to import it from the standard library in order to
work with it.

In this project, you'll continue to work with pathlib and files, but you'll also
use some of the additional built-in data types that you got to know in this
section.

File Type Counter

Path() objects have an attribute called .suffix that allows you to get the file
extension of each file that you're working with. You used it previously to
decide which files are screenshots:

if filepath.suffix == '.png':
pass # Do something

Now, your desktop has gotten quite messy again, but this time you want to
know what's on there and what keeps cluttering it up!

To get that information, write a script that locates your Desktop, fetches all
the files that are on there, and counts how many files of each different file
type are on your desktop. Use a dictionary to collect this data, and print it to
your console at the end in order to get an overview of what is there.

Info: You can use another package from the standard library called pprint,
which stands for "pretty print", in order to display your output nicely
formatted.

You can now expand on your file mover script and add logic that moves all
file types that have e.g. more than five files on the desktop into their own
separate folder. Will it help to keep your desktop cleaner?

It could be interesting to keep track of what types of files keep


agglomerating on your desktop in surprising numbers, also after you've
cleaned it yet again. You wrote this script that counts them and shows you
the counts as a dictionary, but you'll probably forget these numbers until the
next time you run the script.

You could copy the dictionary and save it in a text document. But that
sounds repetitive and also like something you might be able to automate
using Python. In the next section, you'll learn about File Input/Output so you
can write data to files, and read it back from there.
Introduction to File Input/Output
Working with files can seem very normal from the perspective of a computer
end-user. You double-click a program, you create a new file by selecting a
context menu, and you're ready to start typing your novel:

However, you've already learned that there are also programmatic ways to
handle these familiar tasks. You did something similar when you learned how
to move files with a Python script. Before that, you had probably only done
that using your operating system's graphical user interface.

In this section, you'll keep looking at files from the perspective of a


programmer. You'll think of them as a way to store information, and you'll
use Python to write data to a file, and read it, too.

Why Files Are Useful


In one of the projects in the previous course section, you counted the
different types of files on your desktop. You might have gotten an output
similar to this:

# OUTPUT
{'': 8, '.csv': 2, '.md': 2, '.png': 11}

Your count dictionary shows that you've got the following content cluttering
your desktop:

8 folders (''), they have no file extension


2 CSV files (.csv)
2 Markdown files (.md)
11 screenshots or other images (.png)

Now, that's some juicy information, but how will you keep track of it until the
next time you'll run your script to analyze your desktop contents? Most likely,
you'll have forgotten about it, unless you write it down. Python can keep
track of the data for you.

It can be extremely handy to use your computer's persistent memory to


keep track of data across multiple runs of your scripts. The most widely
known type of persistent memory is a file.

If you think of files from the perspective of a programmer, where they are
just a place to persist some data across multiple executions of your
programs, then you're opening up a whole new world. File extensions are just
some additional information that gives your operating system a hint as to
which program it should use to access the data saved in that file.

Keep that script you just wrote around, and get ready to adapt it in order to
write your desktop analysis data to a new file.
Writing To Files
Applications that need to save data or their current state rely on the ability to
persist data. You can do this with files. By writing to and reading from files,
you are able to save information about an application and use it later.

Continuing with the count of the types of files on your desktop from the
previous section, you'll start by writing information to a file.

Writing In Three Steps

If you map out the process of writing to a file in pseudocode, you might
come up with three distinct steps:

1. Create or open a file


2. Write your data to the file
3. Close the file

You'll mirror these steps using Python code to achieve just what you're
trying.

First, you need to open a file in write mode:

file_out = open("output.txt", "w")

In the code snippet above, you're opening a file called output.txt in write
mode ("w") and assigning that to the variable file_out.

If there's no file called output.txt in the directory you're running your script
from, then it will be created.

Note: If a file called output.txt already exists, all of its content will be
deleted instantly if you open it up in write mode as shown above.
After opening the file, you can write data to it using the .write() method on
your file_out variable:

file_out.write('This is what you are writing to the file')

Finally, you should always close the file after you're done working with it, or
you might run into unexpected effects. For example, you might not be able
to delete the file on some systems, or you might see an older version of the
file content, since changes are committed once the file is closed. You can
close a file using the .close() method on your file_out variable:

file_out.close()

Using these three steps, you can create or open files from within your
Python scripts, and write any content to them.

Tasks

Edit the desktop file counter script that you built in the previous section
to write its output to a file.
Run the script and confirm that you can read the output in your new file.
Take a new screenshot, or add another file to your desktop, then run the
script again.
Which possible issues can you identify?

Recap

You can use Python to write data to a file to persist the information also after
your script has finished running. To do this, you need to:

1. open() a file in write mode ("w")


2. .write() data to the file object you created in the previous step
3. .close() the file object when you're done

In the example code snippet, you wrote a string to a .txt file, but you can
also write Python objects to your files.

When you applied this process to your file counter script, you might have run
into some quirks, such as that the data wasn't nicely formatted, and got
overwritten each time you ran the script.

In the next lesson, you'll learn how you can improve this.
Appending And Formatting
When you edited your file counter script to write to a file, you probably
added code similar to the one shown below:

# Above this would be your file counter code


# This code snippet assumes your saving the count
# to a dictionary called `count`, e.g.:
count = {'': 8, '.csv': 2, '.md': 2, '.png': 11}

# New code that writes to a file


file_out = open("filecounts.txt", "w")
file_out.write(str(count))
file_out.close()

Note that you had to convert the count dictionary into a str() in order to
write it to the file. If you don't do that, then Python will let you know by
showing you a TypeError with clear instructions:

Traceback (most recent call last):


File "<input>", line 1, in <module>
file_out.write(count)
TypeError: write() argument must be str, not dict

If you did do the conversion, then you'll see a new file in your current folder
that shows the expected content:

cat filecounts.txt
{'': 8, '.csv': 2, '.md': 2, '.png': 11}
Great! However, as soon as you run this script again, Python will overwrite all
the existing content in your file. Also, if you want to display a longer
dictionary, it'll become quite hard to read like this.

Appending To A File

You can open file objects in different modes. You've opened it in write mode
("w"), which always starts with a blank slate.

Instead, you can use the append mode ("a") to add new data to an existing
file:

file_out = open("filecounts.txt", "a") # 'append' mode

The rest of your code stays the same. If you now run your script a second
time, you'll see that Python added a new dictionary at the end of the file:

cat hello.txt
{'': 8, '.csv': 2, '.md': 2, '.png': 11}{'': 8, '.csv': 2, '.md': 2, '.png': 11}

By only changing the mode that the file object operates as you were able to
keep adding new information on each run.

However, it looks messy and will be really hard to read after 20 runs if you
leave the script as-is.

Formatting Your Output

So far, you've only stuck string representations of dictionaries one after


another. This isn't a great way to read the logged output of your script, and
it's even worse if you wanted to re-use the information for further
processing.
You're only writing plain text to your file, so a quick way to improve the
output would be to write a newline character at the end of each run. You can
call the .write() method multiple times, as long as the file is not yet closed:

# -- snip --
count = {'': 8, '.csv': 2, '.md': 2, '.png': 11}

file_out = open("filecounts.txt", "a")


file_out.write(str(count))
file_out.write("\n") # Include a line break
file_out.close()

In this example, you add a newline character ("\n"), which represents a line
break, after you've written the string representation of your dictionary. That
way, each run will write to a new line in your output file.

Since you're just working with text representation, you can apply any string
formatting technique to the content you're writing to the file.

Recap

File objects have different modes. If you want to add data at the end of a file
object without overwriting the existing content, then you can use the
append mode ("a") when opening the file object.

Since you're just writing text to your files, you can format the text as you
wish using string formatting techniques.

In the next lesson, you'll use the persistent file memory that you created, and
access the data in there.
Reading From Files
After you've learned how to write information to a file, you'll now read that
information back into your program. Reading from a file has a very similar
structure to writing to a file, and requires similar steps:

1. Open an existing file


2. Read the data from the file
3. Close the file

First, you have to open the file. This time, you'll use the default read mode
("r"):

file_in = open("filecounts.txt", "r")

After creating a file object in read mode, you can use .read() to read the
entire content of your file into memory:

contents = file_in.read()

This line of code saves the content of your file in the variable contents.

Tasks

Print out the value of contents.


What data type does contents have?
Which challenges do you expect if you'd try to work programmatically
with this data?

Try to answer the questions posed above and take a moment to think it
through. Write your thoughts in your notebook, and brainstorm some
possible solutions.

Info: You'll learn about better ways to handle input data in just a bit.

After you're done reading the data from your file, you must again close the
file, just like you did when writing to it:

file_in.close()

Keep in mind that you need to call the .close() method on the file object
that you've opened. In this example, you named the file file_in, so that's
also the name you need to use when closing the file.

Challenging File Contents

If you followed the instructions through the last few lessons, then you'll end
up with a value of content that looks similar to this:

# OUTPUT
"{'': 8, '.csv': 2, '.md': 2, '.png': 11}\n{'': 8, '.csv': 2, '.md': 2, '.png': 11}\n

It's a str value that shows the representation of two dictionaries separated
by newline characters.

Now, what would you do if you wanted to get a total count of all the .png files
that you ever had on your desktop?

If you wanted to tackle this challenge programmatically, then you'd have to


write custom code to parse your string and extract the right kind of
information.

Tasks
Train your string manipulation skills and write a script that can parse this
string input and convert it back into a list of dictionaries that you can use to
access information.

You can do that, but there are better ways that allow for better generalization
and will make your collaborators happier.

Especially if you are planning to use the data your script produces
programmatically, or together with other people, there is a better way to
achieve good structure and reusability of your data.

Using Common File Formats

While any data you'll be writing could be represented as text or binary data,
and you could write custom code to parse the data correctly, you can make
your life much easier by using common file formats for data storage and
data exchange.

You might have heard about three common formats before:

1. .csv
2. .xml
3. .json

All three of these file formats are commonly used to store and retrieve
information programmatically.

Python has modules to handle all of them. In this section, you'll learn to work
with the CSV format because it is extremely common and human-readable.

Later in this course, you'll also get to know the JSON format in more detail,
as you'll learn how to receive data from the Internet, and JSON is a common
standard for data exchange over the Internet.

Recap
You can read data from a file using open() with the default read mode ("r").
To get the full content of a file, you next call .read() on the file object.

However, there are also other Methods of File Objects that you can use to
read from a file object, for example, line-by-line.

To make it easier to retrieve the stored information and work with it, there are
certain conventions around how to format data in a file. Common formats
are:

1. CSV
2. XML
3. JSON

You'll learn how to write a CSV file using Python in the next lesson.
Context Managers And CSV Files
In this lesson, you'll learn how to use Python's csv module to convert your
dictionary data to a common file format, and save it, so that you'll be able to
retrieve it later on.

Before using the csv module, however, you'll learn a common shortcut when
writing to, or reading from, files with Python.

Context Manager

You've already learned how to interact with a file using the open() function
and the .close() method.

When you're working with files, you might often run into some unexpected
errors. Sometimes the contents of the file are not what you expect or the file
doesn't even exist at all. You want to be sure that the file object you opened
will be closed in any case. You can achieve this by using a context manager
when opening the file:

with open("filecounts.txt", "r") as file_in:


print(file_in.read())

Using the above construct with the with keyword, which is called a context
manager, you'll automatically close the file object after the indented part of
your code is done running.

Python knows how to safely close the file once it reaches the end of the
indented code block.

Info: You'll most often see a context manager when you're working with files,
and it's encouraged to use them over the three-step process described in
the previous lessons.

The with context manager is a convenient and safe way to handle your file
interactions. You can use it in the same way when writing to a file.

CSV File Creation

Now you'll use the context manager and the csv module to save your file
counter output to a .csv file, so that it's in a format that'll be accessible to
other developers:

import csv
# -- snip --
count = {"": 8, ".csv": 2, ".md": 2, ".png": 11}

with open("filecounts.csv", "a") as csvfile:


countwriter = csv.writer(csvfile)
data = [count[""], count[".csv"], count[".md"], count[".png"]]
countwriter.writerow(data)

Some parts of this code snippet will be unfamiliar, so you'll take a look at
each line and read about what it does:

First, you're importing the csv module from Python's standard library.
Then you're using a context manager to open up a file in append mode
("a"), and saving the file object to the variable csvfile.
Next, you're creating a CSV writer object with the opened csvfile as its
input, and you save it to the variable csvwriter.
Now you're preparing the row of data that you want to write to the file.
For this, you're accessing each piece of information through a
dictionary lookup and adding them to a list.
file_inally, you're using the csvwriter to write the row of data to your file
using .writerow().

As you can see, the process is similar to when you were writing plain text to a
file before.

Tasks

Run your script and check the output. What does it write to your file?
Run the script a second time and check the output.
Rewrite the functionality of the script without using the csv module.
Create the correct formatting only with proper string formatting.
What are the advantages of using the csv module over plain string
formatting? What do you think is missing from your output file? How
could you add it? Explore the csv module's documentation.

Data Storage Formats

CSV stands for comma-separated values and is a relatively straightforward


way to systematically format your data for storage. It relies on the idea
behind tabular data with rows and columns and is a common export format
for applications such as Microsoft Excel or Google Sheets.

Because CSV only separates values with commas, you can write your own
CSV files even without using Python's csv module. However, the module
introduces some quality of life improvements, such as adding headers and
choosing between different flavors. It also prevents you from accidentally
messing up the correct syntax.

Other formats for storing your data are more difficult to manually reproduce,
which means that there are more opportunities to mess up the correct
syntax. That's why it's a good idea to use the existing modules to help you
with creating the right formatting, as well as with reading it back.

Later in this module, you'll also learn to work with JSON, a common format
used for data exchange over the Internet.

Recap

You can use a context manager through the with keyword to safely open
file objects and rest assured that they'll be closed once your script exits the
indented code block.

To create interchangeable data formats that follow certain syntax standards,


you can use built-in libraries, such as the csv module. It allows you to write
data to a file following a pre-defile_ined syntax that sticks to the
specifications of the data format you're creating.

In the next lesson, you'll learn how to read the data you saved back into your
program so that you can work forward with it.
Reading CSV Data
After running your file counter script twice and saving the output to a CSV
file, you'll end up with a file that might look something like this:

8,2,2,11
8,3,2,14

Your numbers will be different, but if you changed the contents of the
desktop, for example by taking a few more screenshots before running the
script again, you'll see that the numbers have changed.

Info: Your script is only recording very specific file types in your CSV file,
namely the ones that you have explicitly added to the list that gets written to
your CSV file. Adding new file types will be noticed by your file counter
script, but with the current setup, it won't make it into your CSV file.

Every time you'll run your script, it'll add another line to this file.

Tasks

Improve your data by writing the headers to your CSV file during the
first run of your script.
Think about how you could improve the CSV writer so that it records
more file types. This is not a trivial task and you'd have to approach the
organization of your script differently. Map it out in your head and on
paper to train thinking through designing a program.

So you've collected some data, and you can keep collecting it with your file
counter script. Now you'll also want to do something with the collected data!
Time to start a new script and read in the data:
# analyze.py
import csv

with open("filecounts.csv", "r") as csvfile:


reader = csv.DictReader(csvfile, fieldnames=["Folder", "CSV", "MD", "PNG"])
counts = list(reader)

print(counts)

Some of this code is similar to when you were using the csv module to write
the data to your file. However, you can see a few differences that you'll look
at in more detail:

You're operating your file object in read mode ("r")


You're using the csv.DictReader() to read in the data.
Since you didn't add a header row to your CSV data, you need to define
what each piece of data refers to by passing a sequence to the
argument fieldnames. These values will become the keys for the
dictionary that'll get created from each row of your data.
Finally, you need to convert the reader object to a list() in order to use
it as expected.

After running the script, you should see output similar to below:

[
{'Folder': '8', 'CSV': '2', 'MD': '2', 'PNG': '11'},
{'Folder': '8', 'CSV': '3', 'MD': '2', 'PNG': '14'}
]

Your output might be formatted a bit differently, but you can see that you're
now working with a list of dictionaries. You can access each value through
list indexing followed by a dictionary lookup.
Tasks

Give it a try and train your list indexing and dictionary lookup skills!
Write code to access the amount of folders that was collected during
the first run of your file counter script.
Access the number of PNGs during the second run of the script.

After reading in your CSV data and converting it back to a familiar data type
that you know how to work with, you're all set to analyze the changing
contents of your desktop over time.

Recap

You can use a context manager to read data from files in the same way as
you used it before to write data to a file. Python's csv module comes with
some convenient abstractions, such as the csv.DictReader(), that you can
use to create familiar data structures from your CSV data.

In the next lesson, you'll learn how to include Path() objects from the
pathlib module into your File I/O operations.

Additional Resources

Python Documentation: CSV File Reading and Writing


Python Docuemtnation: The with statement
Real Python: Reading and Writing CSV Files in Python
Handling File Names And Paths
In the previous examples, you've referenced the files you're opening and
closing by their names. This works only if the files are in the same folder as
the Python script you're running. When you're referencing files like this,
you're using their relative path:

with open("input.txt", "r") as file_in:


print(file_in.read())

The relative path of a file is the path from the Python script to the file.

An alternative way to reference a file is by using its absolute path. If you


change the previous example to use the absolute path, it could look
something like this, depending on where the file is located on your system:

with open("/Users/yourname/Desktop/input.txt", "r") as file_in:


print(file_in.read())

When you use the absolute path of a file, you can run the Python script
anywhere on your machine and it would file_ind the file input.txt in the
folder /Users/yourname/Desktop and be able to open it up.

Better Path Handling

File paths are a tricky subject. One reason is that it might be hard to keep
track of the exact absolute path of a file. Most of all, however, they get
handled differently by different operating systems. Windows paths look
completely different than UNIX paths, so if you write a script that works with
UNIX style paths, half of your users would probably not be able to use it.
There are, of course, clever ways around this challenge, and the most
straightforward one is using Python's pathlib module. You've worked with it
before in the previous module of this course, and now you'll apply it to File
Input/Output.

from pathlib import Path

data_path = Path("/Users/yourname/Desktop")

with open(data_path.joinpath("input.txt"), "r") as file_in:


print(file_in.read())

You've imported Path from pathlib, and used it to create a Path() object
that points to the folder location of where your data is located. You saved
this information in the variable called data_path.

Info: This is an improvement over using relative or absolute paths, because


this code is operating-system agnostic, which means it'll work the same on
Windows and UNIX machines.

After defile_ining your data_path, you can use it as usual inside your context
manager where you're providing the file name. With .joinpath() you're
adding the file name to the Path() object, which provides open() with the
location of the file you want to open. The rest of the code stays exactly the
same.

Shortcuts To Opening Files

The pathlib module and the Path() objects provide you with a couple of
convenient shortcuts. While you can open and read a file as you did above,
you can do it even faster when using methods on your Path() objects:
from pathlib import Path

filepath = Path("/Users/yourname/Desktop/input.txt")

with filepath.open() as f:
print(f.read())

You can use .open() on a Path() object to open the file in the same way as
you would when using Python's built-in open().

You can also read from and write to files with quick Path() methods:

from pathlib import Path

p = Path("hello.txt")
p.write_text("Hello world!")
p.read_text() # OUTPUT: "Hello world!"

pathlib is a great module that makes working with File Input/Output faster
and more secure.

Recap

You can reference the location of a file in two ways:

1. Relative path: This is the location as seen from the directory that your
Python script executes from.
2. Absolute path: This is the location from the root directory of your
operating system.

You can also use the pathlib module and work with Path() objects instead
of plain strings. This has the advantage that it makes your paths operating
system agnostic, and you can use the many convenience methods that the
module provides to speed up and simplify your File I/O tasks.

Practice reading and writing to files with your lab exercises and think about
something you'd like to build that involves reading from and writing to files.
Take a note in your journal to keep track of your ideas.

In the next section of this module, you'll learn more about functions, a
structure that you've already used before. You'll learn what functions are,
why they are useful, and how you can write your own functions.

Additional Resources

Python Documentation: pathlib - Object-oriented filesystem paths


Introduction To Scopes
In the previous lessons, you've written a function, greet(), and used
docstrings and type hints to better describe it. You've also learned that you
need to return a value if you want to use it outside of your function scope.

In this lesson, you'll look closer at what scopes are and how they apply to
functions. Your greet() function contains three variables:

1. greeting
2. name
3. sentence

greeting and name are two parameters that get filled with the value of the
arguments a user passes when calling your function. sentence gets created
within the body of your function.

In this lesson, you'll start to learn about scope, which influences where you
have access to the values of these variables.

Local Function Scope

Function-internal variables live only inside the function. You can refer to the
function parameters inside your function as much as you want to, but you
won't be able to access them outside of it:

def greet(greeting: str, name: str) -> str:


"""Generates a greeting"""
sentence = f"{greeting}, {name}! How are you?"
return sentence

print(name) # OUTPUT: NameError: name 'name' is not defined


Trying to print the function-internal variable name outside of the function
body will throw a NameError. Python doesn't have anything assigned to the
variable name in the global scope of your script. The same is true for any
other variable in your function, whether you received it as a parameter, such
as greeting and name, or you defined it inside the function, such as
sentence:

def greet(greeting: str, name: str) -> str:


"""Generates a greeting"""
sentence = f"{greeting}, {name}! How are you?"
return sentence

print(sentence) # OUTPUT: NameError: name 'sentence' is not defined

Function-internal variables can't be re-used outside of the function's scope.


To re-use any value generated in the function, you need to squeeze it
through the return statement and expose it to your global scope.

Info: Even though you are returning the value of your function-internal
sentence variable, that doesn't make the variable accessible in your global
scope. All you do is to expose the value of that variable, and you'll need to
assign it to a new variable in the global scope.

In the next lesson, you'll dive deeper into the concept of scopes, and walk
through a step-by-step example to better understand how they work.

Recap

Variables inside a function body aren't accessible outside of your function.


They only exist in the local function scope.
Variable Scopes
In this lesson, you'll dive deeper into the concept of scopes in programming,
and you'll walk through an example step-by-step.

How does Wikipedia define scope:

In computer programming, the scope of a name binding – an association


of a name to an entity, such as a variable – is the region of a computer
program where the binding is valid: where the name can be used to refer
to the entity. Such a region is referred to as a scope block. In other parts
of the program, the name may refer to a different entity (it may have a
different binding), or to nothing at all (it may be unbound).

Ufff! That's a heavy definition and doesn't seem that straightforward. Let's
take another approach and try this again from the beginning.

Start by thinking of a box:


Now think of another box inside that box:
Wait, no! Keep your imagination in check! There should be another box
inside the box. Okay, okay, that's the inner box. So you're now picturing a
cardboard box with another box inside that has some candy in it. Fine, that'll
do.

These boxes are similar to how scopes work. You can fill different scopes
with different variables. So you can have something called a candy inside the
outer box, and you can have something else that is also called candy in the
inner box. Even though they have the same name, they will be two different
candies.

Just like in the example in the previous lesson, you won't be able to access
the candy in the inner box from the outer box. No matter how hard you try:
That covers the very basics of scopes, but the metaphor doesn't entirely
hold up. It might be counterintuitive to know that you can access things
inside the outer box from the one that's nested inside of it. So scopes aren't
just ordinary boxes, and you'll now learn about two different types of scopes:

1. Global scope
2. Local scope

Most of the time up to now you've worked in the global scope of your script.
In the previous lesson, you opened a local function scope and defined
variables that existed only in there.

Global Scope

Each Python session that you start, for example by running a script or an
interpreter session, has a global scope.
Any variable that you define in the global scope is accessible within any of
the inner scopes you might create in that session. That's where the box
metaphor stops making a whole lot of sense.

Even a function within a function within a function (etc.) can still use a
variable that has been defined in the global scope without needing to pass it
as an argument. Nested scopes have access to anything defined in their
outer scopes:

name = "Mycroft"

def print_name_box():
print(name)

def smaller_box():
print(name)

def smallest_box():
print(name)

smallest_box()

smaller_box()

print_name_box()

Run this example code in a script on your own computer or in an online


playground. As you can see, the name variable is accessible in any of the
nested function scopes, even though you only defined it once in the global
scope of your script.

However, this is a one-way street and doesn't work the other way around, as
you've seen in the previous lesson.
In the following graphic, you'll see a visualization of the different scopes
within this code snippet. Each new scope is surrounded by a colored square.
Even though you're opening up some new scopes, the value for name stays
accessible as "Mycroft" in all scopes. This is indicated in the graphic with
the red background behind all scopes:

As you can see, variables defined in the global scope are accessible in all
inner nested scopes of your script, unless they get overwritten.

Local Scope

Variables that are defined within a local scope are available in that local
scope, and any scopes nested within it. A global variable will only exist within
a local scope if there's no variable with the same name in the local scope. If a
local variable has the same name as a global variable the local variable will
always take precedence.

To exemplify this, you'll take another look at your previous example with the
name variable:

name = "Mycroft"

def print_name_box():
print(name)

def smaller_box():
# (Re)assigning a variable within a local scope
# overwrites the same variable from an outer scope
# You also can't use the global variable *before*
# assigning it, if you assign it anywhere in that scope.

# --TASK--: uncomment the print() function below


# and interpret the results when running the script

# print(name)
name = "Sherlock"

def smallest_box():
# Inner scopes always draw from the next-outer layer.
# After `name` got overwritten, the name that will
# be printed is NOT the global-scope name anymore
print(name)

smallest_box()

smaller_box()

print_name_box()

Copy this code into your local IDE and run it to see the output, or use this
online playground. Just like in the previous example, you can see that the
value of name cascades down into the inner scopes.

In the scope of smaller_box(), the name variable gets assigned a new value,
"Sherlock". It keeps that new value further in any deeper-down inner
scopes:

In the graphic above, you can again see all new scopes surrounded by a
colored box. The name variable has the value "Mycroft" in all scopes that
have a red background, and it has the value "Sherlock" in all scopes that
have a blue background.

Tasks

Run both examples in your interpreter.


What are Functions?
In the previous section, you've learned how to handle File I/O in Python. You
used the open() function to create file objects that you could work with. And
that wasn't the first time you've heard about functions in this course. In this
section, you'll finally dive deeper into the concept of making your code do
something with functions.

The Concept Of Functions

Functions are a fundamental concept in programming that touches on two


important aspects:

1. Composition: You use functions to break your code into smaller chunks
that you can arrange and compose together.
2. DRY (Don't Repeat Yourself): You write functions to compartmentalize
and generalize a sequence of instructions so that you can use the same
code for it in multiple places in your script.

Until now, you've mostly written procedural scripts. Python files that execute
instructions from top to bottom. Sometimes, your script would do
something, sometimes it would produce some output and print it to your
command line.

You can think of a function like a script, only smaller. Just like your script file,
your function will contain a set of instructions. When you call your function,
these instructions will be executed, and the function will return some
output. This is similar to how you can run your script, which makes it execute
the instructions, and often give you back some sort of result.

One reason for building functions is also to generalize tasks and help you to
avoid repeating code. Instead, you can write the instructions once and then
use them multiple times. Ready to start writing your own functions?
Function Definition Syntax

Like so many things in programming, there's a specific syntax that you'll


have to follow in order to write a function. Like so many things in Python, this
syntax is quite well-readable and straightforward.

Here's what a basic function definition looks like in Python:

def my_func(parameter):
pass # Replace with code that does something
return # Followed by a value that the function gives back

Such a function definition consists of the following parts:

1. def keyword: tells Python that you're defining a function


2. Function name (e.g. my_func): a variable name used to refer to your
new function object. The name should be descriptive of what the
function does. You'll see examples of that later on.
3. Parameters: Optionally, a function can require some input, called a
parameter. You specify which input is needed in the function definition.
4. Function body: your instructions that do something. In this example,
you see a pass statement as a placeholder for the code you'll write here.
5. return statement: decides what will be the output of your function. A
function can either return some value or None. If you don't specify a
value, like, in the example above, the function will return None.

You'll learn more about these different parts of a function in an upcoming


lesson. But before that, you'll revisit some functions you've used already in
this course, and discuss what they do in practice.

Tasks

Before turning to the next lesson, take a moment and write down a
couple of functions that you've already used throughout this course.
Can you identify which functions required parameters and what these
parameters were?
How could you find out which of the functions you used returned a
value, and which ones returned None?

Additional Resources

Think Python: Functions


Tip: Program Organization
More code and more concepts can add up, and you might start to lose the
overview inside of your own scripts. Some code might not even work the way
you want it to. For example, try running the following code snippet in your
editor:

print(random.randint(0, 100))
hello()

import random

def hello():
print("hello")

Even though this code snippet consists entirely of valid Python code, you're
still running into errors:

# OUTPUT
NameError: name 'random' is not defined

Order matters, because your Python script will execute from top to bottom.
You'll run into two NameErrors, one about random and the other one about
hello because you are attempting to use them before you defined them.

The solution to this is to follow a certain order when writing a script or


program:

1. import statements at the very top


2. followed by function definitions
3. and finally, the code execution
You can re-organize the script shown above in this order to see it execute as
intended:

# Import statements
import random

# Function definitions
def hello():
print("hello")

# Code execution
print(random.randint(0, 100))
hello()

When you follow this basic order, it helps to avoid errors related to execution
order, where you'd for example try to call a function before it's been defined.
This structure also helps you and other coders to quickly orientate within
your code and be able to read it faster.

Recap

Python code executes from top to bottom. When writing your scripts, you
should stick to a basic order:

1. import statements
2. function definitions
3. code execution

This helps to avoid errors and keep your code organized, accessible, and
maintainable.

With this structure in mind, you're ready to dive into building some practical
projects to apply your newly learned knowledge.
Familiar Functions
You've heard the word function many times before, and you probably already
identified a couple of functions you've been using in this course. Here are
some of them:

print() is used to display output to the console. It takes a Python


object as input and writes its string representation to your console. The
print() function returns None.
type() is used to find out the data type of an object. The object is the
argument you pass to it, and the data type is the function's return value.
str(), int(), float(), list(), etc. are functions that allow you to
explicitly convert a value from one data type to another. They take the
value as their input and return the converted value.
input() is used to collect user input through the CLI. The function takes
a string as its input that will be used as a prompt for the user. It then
collects the user input and returns it as a string.
open() allows you to create file objects that you can read from and write
to. It takes a file path and a mode as its input and returns a file object.

You didn't have to define any of these functions, because they're already
part of the Python language. But they aren't special beyond the fact that
they come pre-installed, and you could write your own versions of any of
these functions.

Fruitful Functions

When you think about what most of these functions have in common, you
might notice that you've mostly used them on the right side of assignment
statements:

nstr = "42"
n = int(nstr)

You can do that because you are interested in the return value of the
function.

Instead of allowing the return value to float off into some digital abyss, you
capture it and assign it to a variable, in this case, n.

Most functions that you'll work with, and most functions that you'll write, will
return a value that you want to use somewhere else in your program. In this
course, you'll read about functions that have a return value as fruitful
functions.

Void Functions

Functions that don't return a value you defined, always return None in
Python. These functions are also called void functions.

You might wonder what's the point of a void function. But just because a
function doesn't return anything, that doesn't mean that it can't still have an
effect.

A good example of a void function is the built-in print(). You've used it


many times, and you saw that it always has the effect of displaying some
output to your console.

However, print() is a void function and returns None. The output that is
printed to your console is not its return value, it's what is sometimes called a
side effect.

If you assign the output of a call to print() to a variable, as you did with the
fruitful functions discussed above, and print out the value of your new
variable, you'll get the proof:
result = print("hello void!")
print(result) # None

You'll see that the message gets printed to your console just as you're used
to. When printing the value of the result variable, however, you'll see that
the print() function returned None. It's a void function that does something
in its function body but doesn't explicitly return a value.

Most of the time you'll want to write fruitful functions, and there is even a
programming paradigm called functional programming that aims to only
use functions that have no side effects and always create the same output
when given the same input.

You can write programs in a functional style in Python, but you don't have to.
For now, you'll stick with getting stuff to work without worrying about
programming paradigms.

Recap

You've already encountered and used a few built-in functions throughout


this course, e.g. print() and type().

Functions can be fruitful if they return a specific value, or void if they return
the default None.

Tasks

Print out the return value of each of the functions you've gotten to
know.
What does their output tell you about them?

In the next lesson, you'll take a second look at the different parts that make
up a function definition, before starting to write your own functions.
Calling Functions
You've successfully defined a new function, greet():

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

Now you want to use the code you wrote to accomplish something. In this
lesson, you'll learn how to execute your functions.

Function Calls

Functions help you to reduce repeating code, which makes it easier to write
and maintain your code over the long term. This is true because once you've
defined your function, you can now use it as often as you want to:

print(greet("Hello", "World")) # Hello, World! How are you?


print(greet("Howdy", "partner")) # Howdy, partner! How are you?
print(greet("Zdravo", "prijatelj")) # Zdravo, prijatelj! How are you?
print(greet("Hi", "Martin")) # Hi, Martin! How are you?

To execute the code inside your function, you need to call the function.
You've done this before, so it might not look all that new to you.

To call a function, you write its name followed by opening and closing
parentheses. Optionally, if the function requires arguments, you need to pass
the right amount of arguments in between the parentheses:

def my_func(): # Function without parameters


pass
def my_param_func(param): # Function with parameter
pass

my_func() # Function call without required arguments


my_param_func(42) # Function call with the required argument

What happens when you call a function without passing the right amount of
arguments?

Tasks

What happens when you call greet() without passing any arguments to
it?
What if you pass only one argument?
What happens if you pass numbers instead of strings to greet()?

Whenever you call a function without passing it the required amount of


arguments, Python tells you about it with an error:

greet() # Calling greet without passing any arguments

Traceback (most recent call last):


File "<input>", line 1, in <module>
greet()
TypeError: greet() missing 2 required positional arguments: 'greeting' and 'name'

Keep in mind that with your function definition you're creating a blueprint for
the function calls. You'll then need to stick to this blueprint and call your
function as it was defined.

However, there are situations when the number of arguments you want to
pass to a function might change in different use cases. Python has a solution
for that through a language feature called *args and **kwargs. You'll learn
more about it in the upcoming lesson.

Recap

To execute a function, you need to call it. You do that by writing the
function's name followed by opening and closing parentheses. If your
function requires parameters, you need to pass the right amount of
arguments in between the parentheses.
Parts Of A Function
After you've revisited a couple of the functions you've encountered up to
now, it's time to dive deeper into the structure of a function definition. Here
are once again the parts that make up a function:

1. def keyword
2. Function name
3. Parameters
4. Function body
5. return statement

In this and the following lesson, you'll look into each of these parts in more
detail, while you build out your first custom function from scratch. You can
use the function you'll build to greet people:

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

You'll look into the first three elements of the function definition below, and
you'll learn about the final two on the next page.

The def Keyword

If you look at the first word in the first line of the code snippet, you'll see the
magic that starts it all off:

def

The def keyword is what tells Python that you want to define a new function.
Python expects a specific syntax after it. def needs to be followed by a
whitespace, then a function name, optionally some parameters, a colon, and
finally an indented function body.

The Function Name

The second word in the first line is the name of the function:

def greet

This name is just a variable name and can be any valid Python variable name.
It'll be the name you use to refer to the function object you're creating. Aim
to make your function names descriptive of what the function does.

For example, the function you're building here aims to greet people,
therefore a good descriptive name for it could be greet.

Parameters and Arguments

Following the function name, inside of the parentheses, you'll find the
function parameters:

def greet(greeting, name):

Parameters are optional, but parentheses are required for your function
definition. So if you were to define a function that doesn't take any
arguments, this line would look like below:

def greet():
Before moving on to the next line, take a moment to consider the difference
between parameters and arguments. The two terms are often used
interchangeably, and while they are related to each other, it's worth to be
precise and understand how they really are different.

Here is a slightly adapted definition taken from StackOverflow:

A parameter is a variable in a function definition. When a function is


called, the arguments are the data that you pass into the function's
parameters.

What that means is, that while there's a semantic difference between them,
parameters and arguments are two sides of the same coin.

When you define a function, you are writing parameters, such as greeting
and name in this example. When you call a function you are passing
arguments to the function call, for example, the strings "Hello" and
"Martin":

def greet(greeting, name): # parameters


pass

greet("Hello", "Martin") # arguments

Whatever you pass to the function call as an argument gets assigned to the
corresponding parameter inside the function body.

Info: Order matters when using positional arguments like in this case. You
need to pass the arguments in the right order, or they'll be assigned to the
wrong parameters in your function and you'll get surprising results.

You can also pass arguments together with their parameter names as
keyword arguments, which allows you to pass them in any order and still
have them assigned to the right parameters:

def greet(greeting, name): # Positional arguments in definition


sentence = f"{greeting}, {name}! How are you?"
return sentence

greet(name="Martin", greeting="Hello") # Call it with keywords

Python also supports defining keyword arguments with default values in


your function definition.

Info: The keywords need to match the parameter name that your argument
will be referred to in the function body.

They have the same effect as positional arguments, only that you'll be able
to call your function also without providing all the input. If you omit a
parameter that has a default value, then it'll be used instead:

def greet(greeting="Hi", name="User"): # Adding defaults


sentence = f"{greeting}, {name}! How are you?"
return sentence

print(greet())
print(greet(name="Fievel" greeting="Hello"))

# OUTPUT:
# Hi, User! How are you?
# Hello, Fievel! How are you?

One aspect that makes functions so versatile is that you can pass different
arguments to a function call. They'll be assigned to parameters in the
function body and that's how the execution can work for different inputs.
Recap

A function definition consists of separate parts:

1. def keyword
2. Function name
3. Parameters
4. Function body
5. return statement

In this lesson, you've learned about:

1. the def keyword, which informs Python that you want to define a
function
2. the function name, which is the variable name that you'll refer to when
working with your new function
3. parameters and arguments

There's a conceptual difference between parameters and arguments, where


parameters can be considered the variable placeholders for the arguments
that you're passing during a function call.

You learned that Python supports two types of arguments:

1. positional arguments
2. keyword arguments

You can also mix both types, only keep in mind that they have an inherent
order and you need to first provide all positional arguments before providing
any of the keyword arguments.

In the next lesson, you'll continue to learn more about the different parts that
make up a function definition, specifically the function body and the return
keyword.
Parts Of A Function Part 2
In the previous lesson, you've started to dive deeper into the different parts
that make up a function definition. Specifically, you've learned about:

1. the def keyword


2. the function name variable
3. parameters and arguments

Especially the topic of parameters and arguments is quite a lot to take in,
which is why you'll learn about the next two parts that make up a function
definition in this lesson.

Until now, you've described the function you've defined would only contain
the first three parts, which are all congregated in the first line of code:

def greet(greeting, name):

This doesn't yet make for a useful function, and in fact, would still cause a
SyntaxError if you tried to do anything with it until now.

After finishing the first line of code of a function definition, and ending it with
a colon (:), Python expects an indented code block. You've already
encountered similar syntax before with loops and conditional statements.
Functions use a similar syntax, where indentation has meaning and defines
which lines of code are part of your function. This code is also called the
function body.

The Function Body

Inside the function body is where all your functional code goes:
def greet(greeting, name):
sentence = f"{greeting}, {name}! How are you?"

In this example, your code logic works with the two parameters to construct
a new string that you assign to the variable sentence.

Info: The code needs to be indented for Python to consider it as a part of


your function definition.

The function body is the place to work your code magic. As you can see
above, you can use parameters that you've defined in the first line of your
function definition. You can use, mix, and re-calibrate them, and finally add
your special spices until you come up with the perfect value.

The return Statement

The final value that you've created in your function body is what you want to
return from the function using the return statement:

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

If you want your function to be fruitful, it's important to include a return


statement. Any Python function without an explicit value following the return
statement will always return None.

Below is an example of a void function:

def void_func(message):
print(message)
You can see that you didn't even need to write the return statement at all. If
it's not there, Python will automatically add return None to your code.

Essentially, the code snippet above is the same as writing:

def void_func(message):
print(message)
return None

Avoiding the return statement is equivalent to adding the line return None.

Note: Keep in mind that even though this function does something by
printing to the console, the printed message isn't the return value of the
function.

To create a fruitful function instead, you'll need to explicitly return a value


other than None:

def fruitful_func(message):
return message

This function returns a value that you can assign to a variable and work
forward with. In the example above, the value of message would just be
whatever argument you passed to your function when executing it. There's
not much functionality going on here, but the function is still fruitful since it
has a return value.

Your greet() function now has a return value as well:

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

saying_hi = greet("Hello", "World")

# OUTPUT:
#

Currently, this code snippet doesn't print any output, but you could print()
the return value of your function call, instead of assigning it to a variable:

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

print(greet("Hello", "World"))

# OUTPUT:
# Hello, World! How are you?

As you can see, greet() now returns a value that you can continue to work
with, for example by assigning it to a variable name or printing it out.

The return value is an essential part of your function because it defines what
you'll have access to from all the magic that happened inside your function.
Variables and parameters that you define inside the function body won't be
accessible outside the function.

The return value is the only output that you can continue working with.

Info: If you want to return more than one value from a function, then you
need to wrap your data in a collection. Most of the time, you'll use tuples for
this. For example, the built-in enumerate() function returns a tuple consisting
of an index number and the element's value.
Play around with this in the code playground below to make sure you
understand the concept of void and fruitful functions, and what role the
return statement has in this:

Recap

A function definition consists of separate parts:

1. def keyword
2. Function name
3. Parameters
4. Function body
5. return statement

The function body needs to be indented and contains all the code logic
that your function will compute.

If you want to continue to work with a value that your function computed,
then you need to include a return statement. You'll only have access to the
return value after your function has finished execution.

Info: The reason why you won't have access to any function parameters
outside of the function body is related to the concept of scopes that you'll
learn about later in this section.

You've read about executing functions and function calls in this lesson, on
the next page you'll learn more about what that means and how you can call
your brand new greet() function.
Args And Kwargs
During the last times when you were floating around StackOverflow
researching Python topics in your free time, you might have come across the
words *args and **kwargs. They arguably sound like guttural sounds that a
frog might make, rather than the elegant zssssh of a python. So, it's no
wonder that these special arguments can cause a bit of confusion:

But when it comes down to it, frogs are just animals, and *args and **kwargs
are just another useful feature of the Python language.

The two words *args and **kwargs are conventional abbreviations:

args stands for arguments


kwargs stands for keyword arguments
Both names are replaceable variable names, but for everyone's sake, just
stick with these names to help others and yourself remember what the
language feature is that you're using.

The syntax that actually provides the functionality of this feature are the
asterisks (* and **).

When you define a function, you can prefix a parameter name, e.g. args and
kwargs, with the asterisks. By doing this, you implement a language feature
that allows your function to now take arbitrary amounts of arguments.

But exactly does that mean, and what's the difference between *args and
**kwargs?

The Single Asterisk (*)

If you add *args as a parameter to your function definition, it will package all
passed arguments into a collection. You can then access each item like you
would in a normal list. More commonly, however, you'll just iterate over all
items in args:

def print_args(*args):
for a in args:
print(a)

print_args("barcelona", "tahoe", "ubud", "koh tao")

# OUTPUT:
# barcelona
# tahoe
# ubud
# koh tao
In this code snippet, you're using *args in the first line of your function
definition, just like you would otherwise define a single parameter that your
function takes as its input.

However, by prepending the parameter name with the single asterisk (*), you
invoke the special language feature that allows you to pass as many
arguments to your function call as you want to.

Tasks

Copy the code into your local text editor.


Execute the script passing different amounts of arguments to
print_args().

You might have noticed that the asterisk is only used inside the parentheses
when you define your function. When you use the packaged arguments
inside your function body, you only use the variable name args.

The Double-Asterisk (**)

Adding **kwargs to your function definition has a similar effect. However,


Python packages up your arguments in a mapping instead.

This means that you need to pass the arguments as keyword arguments,
and access them in your function body as you would with a dictionary:

def print_kwargs(**kwargs):
for k, v in kwargs.items():
print(k, v)

print_kwargs(country='ukraine', city='odessa')

# OUTPUT:
# country ukraine
# city odessa
Here you added **kwargs in the first line of your function definition.

By prepending the parameter name with the double-asterisk (**), you invoke
the special language feature that allows you to pass as many keyword
arguments to your function call as you want to.

Use Cases

To think of a practical use case of this feature, and tie it back to your greet()
function, imagine that you're handling a lot of users in your web app. You
want to send them all a short email every month in order to check-in and see
how they like your product.

For this, you'll write a new function, greet_many(), based on greet(), with a
few changes to incorporate the use of *args and allow the function to greet
an arbitrary amount of users:

def greet_many(greeting, *args):


greetings = ""
for name in args:
sentence = f"{greeting}, {name}! How are you?"
greetings += sentence + "\n"
return greetings

Your new greet_many() function takes a required positional argument,


greeting, as well as an arbitrary amount of additional arguments packaged
into the variable args.

Info: Note that if you are mixing defined positional parameters, such as
greeting, with *args, then *args needs to come after any defined
parameters in your function definition.
Because your function will only be able to return one value, you're creating a
new string named greetings that you later concatenate each individual
greeting sentence to, followed by a newline character ("\n") for better
formatting.

When you collect arguments with *args, they get put into a collection, which
is why you're introducing a for loop to access each item in the collection.

With greet_many(), you are now able to greet all your users with the same
greeting message, and it doesn't matter how many users currently are in
your app. *args will handle them whether it's only one or 1000 of them.

In this example, you can also instead explicitly use a list or a tuple to pass
all the names in a single argument that you could have called names. This
approach would be more descriptive in this specific situation and goes to
show that there are always multiple ways to achieve your aim when you're
programming.

Info: You'll revisit the use of *args and **kwargs in another example later in
the course when you'll learn about decorators.

Run the code in your local IDE and make sure that you can get your adapted
greet_many() function to greet different amounts of users before moving on.

Tasks

Refactor greet_many() to use **kwargs instead. What could you pass?


Do an online search and read through some StackOverflow posts about
the topic.
How can you use *args and **kwargs when you're calling a function?

Recap

In this lesson, you learned about using *args and **kwargs to allow your
functions to take arbitrary amounts of arguments:

*args stands for arguments and gathers all additional positional


arguments into a collection.
**kwargs stands for keyword arguments and gathers all additional
keyword arguments into a mapping.

You can also use the * and ** syntax to unpack collections and mappings
when calling a function:

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

user_tuple = ("Hello", "Waheed")


print(greet(*user_tuple)) # OUTPUT: Hello, Waheed! How are you?

Maybe you noticed that it's helpful that you named your function
descriptively as greet(). This helps to know what the function is about when
you need to call it.

However, you currently don't have any easily-accessible information about


how many arguments you need to provide to greet() or what it really does.

Using arbitrary-length arguments such as *args and **kwargs in your


function definition can prevent errors when you want your function to be
open for arbitrary-length inputs. You've looked at how to do this when
writing greet_many().

Most of the time, however, you'll want your functions to be called in a very
specific way, with a specific amount of inputs. greet() is meant to be called
with exactly one greeting and exactly one name, both of which should be of
the str data type. How can you communicate this to other developers?
Docstrings
To better document what your functions do, you should add docstrings to
the function definition. In this lesson, you'll learn what docstrings are, how to
write them, and how to use the information they provide.

Special Comments

Docstrings are special comments that describe functions, classes, and


methods in Python.

Note: You'll learn about classes and methods in the next module of this
course. For now just keep in mind that docstrings apply to them in the same
way as they do to functions.

The rationale for docstrings is to help to know how to work with your
functions. Docstrings should describe at least three aspects of your
function:

1. what it does
2. what arguments it takes
3. what it returns

Describing these important key points of a function makes it more


accessible to other developers.

Write A Docstring

Docstrings have a specific syntax that you need to follow, and additionally
there exist a couple of conventions on how to write good docstrings. The
two essential aspects of a docstring are:

1. Use triple-double quotes (""") to begin and end a docstring.


2. Write it starting at the first line of your function body.
How exactly you format the content of your docstring, and what you include,
is somewhat open for debate, but there are good guidelines that you should
aim to follow.

To walk through creating a docstring for a function, you'll add one to the
greet() function you've written earlier:

def greet(greeting, name):


sentence = f"{greeting}, {name}! How are you?"
return sentence

Following, for example, the style guide on Comments And Docstrings by


Google, you could write a docstring for greet():

def greet(greeting, name):


"""Generates a greeting.

Args:
greeting (str): The greeting to use, e.g. "Hello"
name (str): The name of the person you want to greet

Returns:
str: A personalized greeting message
"""
sentence = f"{greeting}, {name}! How are you?"
return sentence

Now you've turned greet() into a full-fledged example of a well-


documented function with a descriptive name and an extensive docstring.
What's the point?

Read A Docstring
After writing your docstring, you're now able to quickly get information about
how to use your function anytime you need it.

Open up a new interpreter session and copy greet() including its docstring
in there. You now have two options to read your docstring:

1. help(): you can pass the function object as an argument to help()


2. .__doc__: you can access the __doc__ attribute of greet

Try both of them and see what they show you. When calling help(greet) you
should see a full-screen description show up in your terminal that displays
the information from your docstring:

Help on function greet in module __main__:

greet(greeting, name)
Generates a greeting.

Args:
greeting (str): The greeting to use, e.g. "Hello"
name (str): The name of the person you want to greet

Returns:
str: A personalized greeting message

You can exit this screen by pressing the q character on your keyboard.

When you print the .__doc__ attribute of your function, you should see the
same output, but right in your console:

print(greet.__doc__)

As you can see, having defined a descriptive docstring gives any developer
who might use your function a good description of what it is about and how
they can use it. This information is always accessible to them, given that
they've defined or imported your function.

This also works for functions and classes from the standard library, or with
code that other developers wrote. As long as they've added docstrings!

Tasks

Use help() to inspect the docstrings of built-in functions that you've


already worked with.

Calling up these quick explanations can be extremely helpful, therefore you


should always aim to write docstrings for your code. However, typing a full-
length docstring might sound like a lot of work, so here's a quicker way.

Install The VSCode Auto-Docstring Plugin

Like most repetitive tasks in programming, someone built an automation that


makes life easier for you. VSCode has a plugin called Auto-Docstring that
creates most of your docstrings for you. After typing """ in the first line of
your function body, and pressing Enter, you'll get an auto-generated
docstring where you just need to edit some parts of it:

def greet(greeting, name):


"""[summary]

Args:
greeting ([type]): [description]
name ([type]): [description]

Returns:
[type]: [description]
"""
sentence = f"{greeting}, {name}! How are you?"
return sentence

You can jump between the different blanks you need to fill using your Tab
character, which makes creating comprehensive docstrings much faster.

If you're not using VSCode, make sure to check the extensions of your
favorite IDE, it'll most likely have a similar product that you can use.
PyCharm, for example, includes this auto-completion for docstrings by
default.

Recap

Docstrings are special comments that you write right after starting a function
definition. They start and end with triple-double quotes ("""), and there are
various guidelines on how to write good docstrings.

The style you should use depends on what the organization you're working
with uses as their standard, however, a good docstring should describe at
least:

1. what your function does


2. what arguments it takes
3. what it returns

You can follow Google's docstring guidelines and install an extension in your
IDE that helps you to quickly auto-generate the basic structure of your
docstrings.

Adding docstrings to your functions helps to keep your code well


documented and allows developers to call up information about how to use
your functions with help() or .__doc__.

Good documentation is helpful, and even though Python is a loosely-typed


language where you don't need to define what type a variable has, there are
options to add that information even more explicitly than just in your
docstrings. In the next lesson, you'll learn about how to add type hints to
your function definitions.

Additional Resources

PEP-257: Docstring Conventions


Google Style Guide: Comments And Docstrings
VSCode extension: Auto-Docstring
Type Hinting
Thanks to the docstring you wrote in the previous lesson, your function is
now well-documented and you can always look up what types of arguments
it takes, and what it returns. To keep the code easier to read in your learning
material, you'll see an abbreviated version of the docstring here:

def greet(greeting, name):


"""Generates a greeting."""
sentence = f"{greeting}, {name}! How are you?"
return sentence

In your IDE, you can collapse parts of your code, usually by clicking a small
arrow displayed in the margins next to your line numbering. But what if you
ever only wrote a short docstring like the one above?

Python is a dynamically-typed language, which means that you don't need


to define what data type a variable is, or what data types a function can take
as input. This means that strange things can happen with your function!

Dynamic Typing And Static Typing

Dynamic typing, also called "duck typing", evaluates variables and inputs at
runtime of your script. There is no need to declare the type of a variable
when you initialize it. Your variable, e.g. a, could be of any type, and change
types throughout its lifetime:

a = 3
a = "hello"
a = 4.2
a = True
Static typing, on the other hand, gives every variable a specific type when
you create it. In a statically-typed language, you can't change the variable's
type once it's declared in the same way you can in Python.

Both approaches have advantages as well as disadvantages. Static typing


can make programs more secure, avoiding accidental mess-ups that could
come from poorly designed code.

For example, irrespective of whether you wrote an extensive docstring or


not, anyone using your greet() function could input any sort of data type as
arguments, and get surprising outputs:

greet([1, 2, 3], 42) # [1, 2, 3], 42! How are you?


greet(True, False) # True, False! How are you?

A function in a statically-typed language would have strict definitions of


what data type the function can take as inputs, and throw an error if anything
else was passed to it.

As you can see, Python doesn't throw an error. It's able to handle the input,
even though it might not be what you wrote the function for. This can
potentially cause troubles in the applications you're building, but it also
opens up new possibilities and makes it faster and more convenient to
develop with.

In an attempt to get the best of both worlds, Python started to introduce


type hinting, also called type annotations, to the language.

Type Hinting

Type hinting was introduced to improve documentation on which data types


should be used when calling a function.
Info: Python remains dynamically typed, which means that even if you add
type hints to your function, the types aren't enforced.

Primarily, type hints are a way to more concisely and unambiguously


document your function.

To add type hints to your function definition, you add the expected type of a
parameter after a colon (:). You can also define the expected type of your
return value with an arrow (->) at the end of the first line of your function
definition:

def greet(greeting: str, name: str) -> str:


"""Generates a greeting."""
sentence = f"{greeting}, {name}! How are you?"
return sentence

With the type hints added to greet(), it's quicker to see what type of input
the function expects, and what type of data it returns.

But Python stays true to its dynamic typing and won't enforce these type
hints. Even if you added them, and you pass an int to greet(), you still won't
get an error. Type hints are mainly meant to improve the documentation of
your functions.

Enforced Type Hinting

There are external packages, such as mypy, that make it possible to enforce
static typing in Python. mypy uses the type hints and throws errors if you
attempt to pass any data type that contradicts them. By using type hints
together with mypy to check your code, you can benefit from some of the
advantages of static typing in Python.

Info: mypy is an external package, which means you need to install it before
you can use it. If you haven't done this before, read over the next sections
about virtual environments and external packages before completing the
exercise below.

To check your code with mypy, you need to first install the package, and then
run mypy from your CLI and pass it the path to your Python script:

mypy yourscriptname.py

If mypy doesn't identify any issues where your type-hinted function might be
called with the wrong data types as input, it'll show you a success message.
Otherwise, you'll get to meet a new error-message friend:

$ mypy typechecker.py
typechecker.py:7: error: Argument 1 to "greet" has incompatible type "List[int]"
typechecker.py:7: error: Argument 2 to "greet" has incompatible type "int"; expected
typechecker.py:8: error: Argument 1 to "greet" has incompatible type "bool"; expected
typechecker.py:8: error: Argument 2 to "greet" has incompatible type "bool"; expected
Found 4 errors in 1 file (checked 1 source file)

mypy successfully analyzed your code and identified that it included some
use cases that contradict the information encoded through your function's
type hints.

Tasks

Write a script called typechecker.py where you add your type-hinted


greet() function.
Add some example calls to the greet() function that take other data
types than strings as their input.
Run the file normally and confirm that everything works fine
Now run the file with mypy and confirm that you get similar error
messages as the ones shown above.
Fix the errors by changing the inputs to your function calls, then confirm
that checking your script with mypy now passes all tests.

You don't need to add type hints to your functions. It's good to know that
this feature exists, but it doesn't need to be part of your workflow. If you like
the additional descriptiveness that it gives to your functions, then it's a great
habit to get into adding type hints as a part of documenting your functions.

Recap

Python is a dynamically typed language, which gives you freedom but also
introduces potential errors. You can add type hints to your functions to
better document them. You can even use external packages, such as mypy,
to enforce type hints and use Python more like a statically typed language.

To add type hints to your function definition, you add the expected type of a
parameter after a colon (:). You can also define the expected type of your
return value with an arrow (->) at the end of the first line of your function
definition.

Now that you have a well-documented function, both through adding a


descriptive docstring, as well as type hints, you're ready to learn about how
the concept of scopes applies in regards to functions.

Additional Resources

Medium: How to use static type-checking in Python


StackOverflow: How to use type-hints in Python
MyPy Github: mypy
PROJECT: Recreate The Enumerate
Function
You've read a lot about functions and it's about time that you practice writing
them. In this mini-project, your task is to re-create a built-in function that
comes with Python.

You'll get to know the enumerate() function, and then venture off to create
the function from scratch by yourself.

Python's Built-In Enumerate

Like some of the functions you've revisited earlier on in this section,


enumerate() is a built-in function that comes with the Python language. It's
mostly used to keep an additional count when iterating over a collection
using a for loop:

courses = ['Intro', 'Intermediate', 'Advanced', 'Professional']

for index, course in enumerate(courses):


print(f"{index}: {course} Python")

# OUTPUT:
# 0: Intro Python
# 1: Intermediate Python
# 2: Advanced Python
# 3: Professional Python

In the code snippet above you can see enumerate() in action. You're first
defining a list called courses that holds a couple of string values.

Then you create a for loop that is a bit different than it would be if you were
looping directly over the list. Instead of defining only one loop variable,
you're working with two of them, index and course.

You have access to two loop variables because of what enumerate() does to
your courses list. Instead of providing only the current item in your iterator,
enumerate() packages each item together with an incrementing number into
a tuple:

(0, 'Intro')
(1, 'Intermediate')
(2, 'Advanced')
(3, 'Professional')

Your for loop then implicitly uses tuple unpacking to take the tuples
generated by enumerate() apart again and provide them as two separate
variables that you descriptively named index and course.

Using enumerate() to keep track of the counts in your for loops is a


common way to tackle this task in Python.

Alternatives

As usual in programming, there are many different ways to solve a challenge.


Here's another way to keep a count during a for loop based on a different
structure that is common in other programming languages:

courses = ['Intro', 'Intermediate', 'Advanced', 'Professional']

for index in range(len(courses)):


print(f"{index}: {courses[index]} Python")
Here you're using a familiar for loop with only one loop variable, together
with two other built-in functions, range() and len(), to access an index
count of the list items. You're then using list indexing in the loop body to
access the values of the courses list.

As you can see, there's more than one way to solve this challenge, but
Python's enumerate() is a well-liked function in the Pythonsphere. So why
not get to know it more closely?

Your Custom Enumerate

This project will train you in your understanding of data types, writing
functions, and reading documentation.

Your task is to create a function called my_enumerate() that mirrors the


functionality of Python's built-in enumerate().

Don't just copy the range(len(sequence)) example and work off of it.
Instead, go online, train your search engine skills, and research how
enumerate() works internally. Then use this to inform your design decisions
for your custom my_enumerate() function.

Ultimately, you should be able to use my_enumerate() as a drop-in


replacement for enumerate() that can be used in the same way and will
produce the same results:

def my_enumerate():
# You implement this function
pass

courses = ['Intro', 'Intermediate', 'Advanced', 'Professional']

for index, course in my_enumerate(courses):


print(f"{index}: {course} Python")
# OUTPUT:
# 0: Intro Python
# 1: Intermediate Python
# 2: Advanced Python
# 3: Professional Python

Take your time to understand the task at hand. Do your research to


understand how enumerate() works. Write all the pseudocode you need. Ask
questions in your forum. And start building your own pythonic function.
PROJECT: Game Updates
It's time to return to your favorite command-line game and apply the new
concepts that you've learned about.

Specifically, there are two new big concepts that you can incorporate into
your CLI game project to make it better:

1. File Input/Output
2. Functions

Take a moment and brainstorm with your notebook about ways that you can
include these concepts to improve your game.
When you're done, here are some broad suggestions to consider:

State: Write the state of your gameplay to a file at the end of a session.
You could use it to keep track of your player's inventory across multiple
instances of your game.
Functions: Refactor your code to use functions for the different actions
that your player can take. Then use your game loop to call the functions
that the player chooses.
Documentation: Add docstrings and type hints to your functions.
Explain what they do and learn to use good practices for writing
readable and reusable functions right from the start.

Please share your updated game on the forum in the Command-Line Games
thread.
More Than Just Python
Wow! If you've started this course from the beginning and made it all the way
here, you've already come a long way! Congratulations on your progress :)

You've learned about crucial aspects of the Python language, as well as


about programming concepts in general. You can now:

Install Python
Understand what programming is and why it is useful
Read, write, and understand Python code
Use Python as a scripting language
Write Python in the REPL and as a script
Create variables and assign values to them
Work with text and numbers
Identify and use common data types, such as:
int
float
str
tuple
list
set
dict
bool
None
Plan out your task with pseudocode
Document your reasoning and decisions using code comments
Write loop logic to tackle repetitive tasks with:
for loops
while loops
list comprehension
Make comparisons and calculations using operators:
Assignment operator
Arithmetic operators
Membership operator
Relational operators
Logical operators
Identity operator
Make decisions with conditional logic and control flow using:
if, elif, else statements
Looping keywords: break, continue, and return
Collect user input with input()
Format your strings with f-strings
Write and call functions to avoid repetition and generalize your code
logic
Provide input to your functions with arguments, and return values to
reuse them
Use positional and keyword parameters
Document your functions with docstrings and type hinting
Understand scopes in programming
Automate repetitive tasks on your file system
Work with File input/output on your computer

Projects

You've also applied code logic concepts, correct Python syntax, and your
creativity to build out projects:

File Renamer: Move and rename files in a folder on your file system
Automate renaming and moving files
Using the pathlib module to handle file paths
CLI Games: Build a small CLI game
Guess My Number
Hangman
Adventure role-playing game

At this point, you've learned enough about Python and programming so you
are ready to handle any scripting task.

World Building

But there are also other concepts that you learned on your way.
Programming isn't just programming, after all, and in this course, you're
learning more than just programming.

You also learned to:

Adopt a growth mindset


Understand the logistics of this course
Get a working development setup going
Use a professional code editor, such as VS Code or PyCharm
Read error messages and make them your friends
Automate repetitive tasks on your operating system
Understand how the file system on your operating system is structured
Work with absolute and relative paths
Conduct common file system operations
Write Bash code to interact with your operating system through your
command line

None of these topics are exclusively related to the Python programming


language, but they are important steps towards becoming a productive
software developer and having a better understanding of how to
professionally operate your computer. You'll also need these additional skills
to tackle the day-to-day tasks of a programmer.

Next Steps

In the upcoming sections of this course, you'll spend more time on topics
that aren't exclusively related to the Python programming language.
However, these topics are important for your path to becoming a capable
and versatile developer, whether you want to use your skills privately or as a
profession.

At the end of this course module, you'll build a project that uses Python to
interact with data provided over the internet. You'll programmatically
connect and retrieve this data, and include it in programs that you build
locally on your computer.

You'll venture out into the world-wide-web, and the Internet is a messy place
that consists of thousands of different technologies. You'll keep using
Python to accomplish your tasks, but there are some more general
software development topics that you'll learn to be able to do so safely
and productively.

You'll learn how to:


install and use third-party Python packages
create virtual environments to compartmentalize your development
environment
use environment variables to separate sensitive information and
settings from your production code
interact with APIs that offer data over the Internet
create and work with databases to store and retrieve your data

You'll also keep learning more about Python and how you can debug your
Python programs, as well as how to use specific Python tools to intersect
with and tackle some of these challenges.

Recap

You've already learned a lot up to this point, both about Python, and
programming, as well as related concepts that you might not encounter in
pure Python-focused courses.

Programming is more than just programming.

Moving forward, this module will help you learn more Python, but it'll also
start to focus on more general concepts that are part of your tasks as a
developer, as well as highlighting how to intersect them with your Python
skills.

In this section, you'll learn about external Python packages, as well as how
you can install them in virtual environments (venvs), and use these venvs to
automate some of your project-specific settings.
External Packages
In the lesson about type hinting in Python, you've learned about an external
package called mypy. If you tried to run the mypy command without first
installing it, you'll have encountered a new error friend. If you went to check
it out on GitHub and read over their installation instructions, then you've
probably come across the line:

python3 -m pip install mypy

While Python already comes with a lot of code in its standard library, there
are a vast amount of additional packages available that you can install from
external sources. mypy is one such package.

The Standard Library

Python boasts to come with "batteries included", which means, according


to Python's PEP 206:

having a rich and versatile standard library that is immediately available,


without making the user download separate packages.

The standard library contains a large number of extremely useful packages


that are often enough to tackle most tasks you want to accomplish with
Python.

You've used packages from the standard library before when importing
pathlib and random.

However, another reason that Python is so widely used is its rich ecosystem
of packages and modules. It also helps that it's straightforward to install and
use an external codebase.
Third-Party Packages

The Python Package Index (PyPI) is a central repository where all Python
users can upload their own code as a package, and where everyone can go
to download those packages.

PyPI contains the most well-known third-party packages that you might
want to work with. Because so many packages are hosted in this central
place, there's also a common tool that comes with your Python installation
that allows you to quickly install a package from PyPI. This tool is called pip.

Pip Installs Packages

Python ships with a tool to install packages that is called pip. pip stands for
"pip installs packages" and it makes installing modules and packages quite
straightforward:

python3 -m pip install <package name>

For example, in order to install mypy locally on your computer, you only need
to type this one line of code into your terminal:
python3 -m pip install mypy

This command would install mypy system-wide for your python3 installation.

Note: You most likely don't want to do that. Installing external packages
system-wide is a bad practice because it can lead to version clashes down
the line.

Instead of installing your packages system-wide, however, you're better of


installing them in a virtual environment. You'll learn more about what these
are in the upcoming lessons.

Recap

Python comes with a lot of great packages, and you can install even more of
them from a central location called PyPI with the pre-installed package
manager pip.

Avoid installing packages system-wide, so keep reading before you go crazy


and pollute your system with external packages.

Additional Resources

Python Packaging Index Website: PyPI


Wikipedia: pip
Dependencies
Now that you've opened the floodgates to access PyPI through pip, you
might be tempted to race ahead and install some of the exciting external
packages that you might--or might not--have heard of.

But hold your horses!

With a bustling activity as a developer, when the gears are oiled and grinding
feels like a morning jog, it happens quickly that your dependencies go out
of hand.

Dependencies

Eventually, you'll want to build Python projects that use the rich ecosystem
of modules and packages that are out there.
A dependency is code that someone else wrote and that you use in your
own project. The functionality of your own code depends on the code you
pulled in from an external source.

The developers who wrote the package put a lot of effort into writing code
that abstracts away implementation details and makes it possible for you to
build on top of that. They did that so that you won't have to re-invent the
wheels they've already built.

Using dependencies is very common when you build out more complex
programs, and it's extremely helpful for a quicker and more productive
development process.

However, as a professional developer, you're likely to work on more than just


one project at any given time. Each project might require different
dependencies. Or, what makes it even more complicated, your projects
might require the same packages, but different versions of them.

For example, you might work in web development and use Django, a Python
web framework that you can install as an external package through pip.

Info: Django is just an example for a dependency you might have in your
code. Anything that you python3 -m pip install is a dependency.

As a sought-after web developer, you might be maintaining three different


Django projects for three different clients. And it turns out that they're all
using different versions of Django.

Your system-wide Python can have only one version of Django installed at a
time, so you'd quickly run into trouble in such a situation.

Using virtual environments represents a solution to this issue. They allow


you to compartmentalize the dependencies for each project into a specific
environment and keep them separate from all your other projects. You'll
learn more about virtual environments in the next lesson.
Introduction To Virtual Environments
You might have heard about virtual machines (VMs) before. A virtual
machine allows you to run a different operating system on your current
operating system. For example, you could use a VM to run Linux on a Mac.

You can create a VM with programs such as virtualbox. The VM is a separate


environment that is self-sufficient and doesn't need to interact much with
the main OS of your computer.

The Concept

Virtual environments are a similar concept, only that they don't box up a
complete operating system, but instead just the following:

1. Python: An installation of Python


2. Packages: Python modules and packages

Instead of creating a virtual machine, you create a virtual environment


specifically for your Python:

If you keep your Python inside a virtual environment, then you're keeping it
and all the packages it needs for that specific project, safe from other
environments. You also keep the rest of your computer safe from whatever
goes on inside your terrarium, ahem, virtual environment.

The Reality

Your computer needs to create a folder to store your project-specific Python


as well as all the external packages you'll install for the project. On macOS,
the contents of the virtual environment folder look something like this:

.
├── bin
│ ├── Activate.ps1
│ ├── activate
│ ├── activate.csh
│ ├── activate.fish
│ ├── dmypy
│ ├── easy_install
│ ├── easy_install-3.8
│ ├── mypy
│ ├── mypyc
│ ├── pip
│ ├── pip3
│ ├── pip3.8
│ ├── python -> /Users/martin/.pyenv/versions/3.8.5/bin/python
│ ├── python3 -> python
│ ├── stubgen
│ └── stubtest
├── include
├── lib
│ └── python3.8
│ └── site-packages
│ ├── 8c8c5116bc4884237d33__mypyc.cpython-38-darwin.so
│ ├── __pycache__
│ │ ├── easy_install.cpython-38.pyc
│ │ ├── mypy_extensions.cpython-38.pyc
│ │ └── typing_extensions.cpython-38.pyc
│ ├── easy_install.py
│ ├── mypy

--- snip ---

On Windows, the folder structure is similar but not quite the same. For now,
the differences between the folder structures on the different operating
systems don't matter.

What you can see in the tree structure above is just a small part of all the
files and folders that make up such a virtual environment. If you're curious,
you can look at the full contents of an example virtual environment.

Whether you ventured bravely to follow the link or not, you'll see that this
folder structure is huge. That might seem daunting, and it would be if you
had to build it yourself.

But instead, Python comes bundled with a command that creates all of that
for you. You'll get to know it in the next lesson.

Recap

Using virtual environments makes software development safer and more


reproducible, by keeping your Python interpreter together with project-
relevant external packages in a dedicated folder.

Starting to use them early on can be a huge time-saver. In the next lesson,
you'll create your first virtual environment.
Working With Virtual Environments
You can automatically generate the folder structure for a virtual environment
(venv) in one line of code, activate it with a second command, and
deactivate it with yet another one. To get started working with virtual
environments, you only need to know these three commands, and you'll
learn them in this lesson.

Creating a Virtual Environment

To create a new virtual environment that is set up for all your Python
development needs, you'll need to run the following command:

python3 -m venv your_venv_name

This is a Bash command, where you're using the program python3, which is
your system-wide installation of Python 3, to call one of its built-in modules
(-m) called venv. The venv module that comes with Python's standard library
allows you to create virtual environments. As the final part of this command,
you need to provide a name for your new virtual environment.

It's a default to call your virtual environments env or venv but you could name
them anything you want. You'll just need to remember what you called it
because you need its name to activate it in the next step.

Note: Make sure that your virtual environment's path does not include any
whitespace. Otherwise, the executables won't be found and you might end
up accidentally using the system-wide install of pip. See this StackOverflow
answer for more information.

It's strongly suggested to stick with a default name, and this course will use
venv as a name for any virtual environments created. Therefore, to create a
virtual environment with that default name, you'd run the following
command:

python3 -m venv venv

Give it a try, wherever you currently are in your CLI. You won't break
anything.

After you successfully ran the command, navigate to the new folder that was
created. It'll be named venv if you followed the instructions above.

Inspect the folder using your Finder, Explorer, or CLI. Take a look into the
bin/ subfolder on macOS and Linux, or the Scripts\ folder on Windows,
respectively. You might find that every freshly minted virtual environment
comes with executable files for the Python interpreter, as well as pip.

Activating a Virtual Environment

You are now the proud owner of a virtual environment. But beware! You are
not yet inside this safe space that you created! In order to enter your new
venv, you'll have to activate it. You can activate a virtual environment that
you named venv by typing the following command on macOS and Linux:

source venv/bin/activate

The source command runs the activate script of your venv. You might have
seen it inside of your venv's bin/ folder.

On Windows, the command looks a little bit different, because the activation
script is located in a different folder:
venv\Scripts\activate

Just like on UNIX systems, however, you're also executing a script called
activate. On Windows, that script lives in a directory that's aptly named
Scripts\.

The activation script grants you access to the safe kingdom of venv. After
the command has finished executing, you'll see that your command prompt
has slightly changed. As an indicator that you are now inside of the virtual
environment, you'll see the name of your venv in brackets in front of each
new line in your terminal:

Depending on the shell that you are using in your terminal, it might look
different. But the concept is the same. As soon as you can see your virtual
environment's name in brackets next to your command line prompt, it means
that you can finally start breathing again. You have arrived in the safe
kingdom of venv.

Now you're ready to do all the work you need to do on the project that you
created the venv for. From the terminal window where you activated your
venv, you can now install all the packages you need:

python3 -m pip install mypy

The external packages install inside your venv. This means that when you
deactivate your venv, they won't interfere with other packages you installed
system-wide. It also means they'll be waiting for you when you re-activate
your venv the next time you want to work on this project. And if you wanted
to get rid of everything, all you need to do is to delete the venv folder.

Deactivating a Virtual Environment

To return to your normal system's environment you only need to type one
passphrase into the console:

deactivate

The indication that you're in the venv will disappear and your shell will show
your normal prompt again. That's all there is to deactivating a venv. Welcome
back!

Tasks

1. Use your terminal to create a new folder called test.


2. Navigate into the folder with your CLI.
3. Create a venv called venv inside of the test folder.
4. Activate your venv and notice how the command prompt in your CLI
changes.
5. Use pip to install mypy.
6. Open the Python interpreter and import mypy to verify that everything
worked. If you don't get an error message, then the setup was
successful.
7. If you want to dig deeper, then inspect the venv folder and look for
where pip installed the mypy files. This is the location where your
packages are installed to when you run python3 -m pip install
<package> while you have your venv activated.
8. Deactivate your venv and notice how the CLI prompt changes again.

Using a Virtual Environment With an IDE

Many IDEs, such as VS Code and PyCharm, can help you automate working
with venvs. PyCharm, for example, automatically creates and activates a new
virtual environment for you when you create a new project. If you use these
features, you won't need to go through the process of creating and
activating a virtual environment yourself.

Tasks

Research the documentation of your favorite IDE and find out how it can
help you to make working with virtual environments easier.

You'll notice whether or not your venv is activated in your IDE's terminal in
the same way as you do in your normal Bash terminal:
The screenshot above shows a terminal window in VS Code with an
activated virtual environment named venv.

Recap

There are three commands you need to know to start working with venvs:

1. Create: python3 -m venv venv


2. Activate: source venv/bin/activate (macOS and Linux) or
venv\Scripts\activate (Windows)
3. Deactivate: deactivate

You'll want to create a new virtual environment for any project that uses
external third-party packages. You can install them into your venv after
creating and activating it. When you're done working on a project, you
deactivate your venv to return to your normal system-wide environment.
Introduction To Environment
Variables
In this lesson, you'll learn about environment variables in Bash on UNIX
operating systems, but Windows has the same concept with slightly
different commands that you can read about in this external guide on
configuring environment variables in Windows. The concept of environment
variables is not specific to Python. Instead, it's a common standard used
across different areas of software development.

Environment variables is dynamic-named values, which you can access from


anywhere in your current environment. They can help you make running your
scripts more user-friendly and secure, and are shared across all applications
in your current environment.

In UNIX systems, the most famous one of them is $PATH, which specifies file
paths where your system looks for executable files. Windows has a similar
variable called Path.

Use Cases

You can access the value of your environment variables anywhere in your
project without ever spelling out the actual value of that variable. Instead,
you can refer to it through the environment variable.

That way you can work with secrets and passwords throughout your project,
and commit all project-relevant code to GitHub while keeping your sensitive
information safe and to yourself.

Note: In this section, you'll learn about using environment variables to


separate sensitive information from the rest of your code. You'll later use it to
build projects that interact with the web towards the end of this course
module. The concepts you'll learn, however, will be helpful also in many
other cases.

Many larger programs that you'll build will include some setting information
that you don't want to share with the world. Think about API keys for web
service calls, database login credentials, or the ingredients to your secret
sauce in your recipe generator. However, you might still want to be able to
collaborate online with other developers on your code through GitHub.

Info: While Git and GitHub are great, sensitive information should never
make its way to the open-source community.

Environment variables can help you with generalizing the setup of your
applications, as well as with separating out sensitive information so that
you'll have an easier time keeping that information safe.

Horror Scenarios

The web is full of horror stories of accidentally posted API key secrets that
ended up costing the owner a lot of money. If you need some extra
convincing, or just want to stay up late tonight, check out the following
posts:

A Git Horror Story: Repository Integrity With Signed Commits


My $500 Cloud Security Screwup
Dev Blunder shows GitHub is crawling with Keyslurping Bots
My AWS Account was hacked and I have a 50 000 Bill

The quick takeaway is that you should never post your sensitive information
to GitHub.

Info: Bots are quick, and one compromised commit is one too many.

Keep in mind that there are multiple ways to keep your sensitive information
safe. In this course, you'll learn how you can use environment variables to
separate out sensitive information and make it less likely you'll end up with
an accidental horror story.

Recap

You can use environment variables to help with your project setup and
separate out sensitive information. In the upcoming lessons, you'll learn how
to:

Set an environment variable


Add and remove environment variables from your command line
Create virtual environment variables in your Python virtual
environment
Automatically set and unset these virtual environment variables when
you activate or deactivate your virtual environment

Even though the skills you're learning here are not specific to Python,
knowing how to work with environment variables is a standard skill for
software developers that'll come in handy especially when you'll work on a
team. It's also helpful when you build projects and store your code on
remote version control sites, such as GitHub.
Working With Environment Variables
In this first lesson about environment variables, you'll learn how to inspect,
add, and remove them directly from the command line.

Note: These lessons focus on using the Bash command language, which
you'll have available if you're on a UNIX system or use WSL on Windows. If
you're working directly on Windows, then setting environment variables
takes less effort if done through the graphical user interface. You can read all
about it in this detailed guide on configuring environment variables on
Windows.

Open up your terminal and type the Bash command printenv. This will give
you a list of all the current environment variables present in your system:

HOME=/Users/Martin
LOGNAME=Martin
USER=Martin
PATH=/Library/Frameworks/Python.framework/Versions/3.9/bin:/usr/bin:/bin

This example output shows you a couple of environment variables that are
currently defined on your local machine. You'll probably see a different name
and some additional lines in your own output.

You can check the value of each variable with echo $<NAME>. For example, to
check the value of LOGNAME, you can type the following command:

echo $LOGNAME

The output you receive when running the same command will be what your
LOGNAME variable points to. You can confirm this value also by running
printenv and looking for LOGNAME in your output.

With these two commands, you can inspect all the environment variables
that are currently defined in your system. But what if you want to change,
add, or remove one using Bash?

Adding And Removing Environment Variables

Using Bash in your CLI, you can add a new environment variable with the
following command:

export <NAME>=<VALUE>

In this example, you'll have to replace <NAME> with the new environment
variable that you want to add. You also need to replace <VALUE> with the
value you want to assign to that environment variable.

For example, to add a new variable with the name DAY and the value Sunday,
you would spell it out as follows:

export DAY=Sunday

After executing this command, you can see that a new variable has been
added to your environment. Take a look at it using printenv and echo as
described above.

In order to remove an environment variable with Bash, you'll have to call the
following command:

unset <NAME>
Again, you'll have to add the actual name of the variable you want to remove
instead of the placeholder <NAME>:

unset DAY

This Bash command removes the DAY variable you set before.

Try adding and removing some environment variables using these Bash
commands. Remember you can always check what’s happened using
printenv or echo <NAME>.

However, when you are working on a project that requires a specific API key,
you don't want to set your environment variables across your whole system
environment. The value will be project-specific, which is why you should
compartmentalize your environment variables just like you did for your
dependencies by including them into your virtual environments.

Recap

You can interact with system-wide environment variables directly from your
Bash command line:

Inspect your environment variables with printenv or echo $VARNAME


Add a new environment variable with export NAME=VALUE
Remove an existing environment variable with unset NAME

In the next lesson, you'll learn how to combine the concept of environment
variables with what you learned about virtual environments, to create
environment-specific virtual environment variables.

Additional Resources

FreeCodeCamp: How to securely store API Keys


Mike Gerwitz: Git Horror Story
StackExchange: Strategy for keeping secret info out of source control
Ian Currie on Real Python: Your Python Coding Environment on
Windows: Setup Guide
Virtual Environment Variables
When you do any project-specific development, you always want to avoid
setting anything for your whole system. This holds for your third-party
Python packages. The same counts for secrets, which are usually project-
specific.

It's a much better idea to set these types of environment variables inside of
your virtual environment. In this course, these types of variables will be
called virtual environment variables.

Using environment variables inside of a Python virtual environment is easier


than having to first export and then unset each variable every time that you
want to work on a project.

Editing Your Activation Script

Start by creating a virtual environment for your Python project:

python3 -m venv venv

After you successfully created a virtual environment, open the activate


script in your favorite text editor.

Info: You can find this file inside of the venv folder that got created by
running the command shown above. The relative path of this script is
venv/bin/activate on UNIX systems and venv\Scripts\activate on
Windows.

This script runs every time your venv gets activated, which makes it a good
place to let your computer know which environment variables you would like
to have access to, and which ones to get rid of once you exit the virtual
environment.

You'll now edit the activate script and hard-code the values you want in
there. First, you need to make sure that your virtual environment variables
won’t stick around once you deactivated them, so you start by unsetting the
environment variable that you haven't even created yet.

Unsetting Virtual Environment Variables

To unset a virtual environment variable, you add an unset command to the


deactivate command section in your Bash activate script. The code in this
part of the script runs every time you deactivate your virtual environment.

For example, to unset the variable MY_SUPER_SECRET_SECRET, you need to add


the following line of Bash code:

deactivate () {
unset MY_SUPER_SECRET_SECRET
# Lots of other code
}

Adding this line in the deactivate section makes sure that your virtual
environment variables won't leak into your system environment. Instead,
they'll exist only within your virtual environment.

Once you wrote the code to unset your variable, it's time to make sure you
also set it, so it'll exist in the first place.

Setting Virtual Environment Variables

You can set a virtual environment variable in the same way as you practiced
before with your Bash CLI. However, instead of typing the export command
directly in your terminal, you'll add it as a new line of code at the end of the
activate script:

# The rest of the script


export MY_SUPER_SECRET_SECRET="OMG this is so secret I can't even say!"

Info: You'll need to wrap the value of your virtual environment variable inside
of double quotes ("").

Save the Bash script and close it. Now you can activate your virtual
environment:

source venv/bin/activate

Once the virtual environment has been successfully activated, you can now
run the printenv command to inspect the state of your environment
variables in your current environment.

MY_SUPER_SECRET_SECRET should show up, as should the value you assigned


to it.

After you confirmed that activating your virtual environment brings your
virtual environment variable into existence, go ahead and deactivate it. Use
printenv once again. Your secret should be gone.

As you can see, this setup can keep your project-specific secrets safe within
their own comfy virtual environments.

Accessing Virtual Environment Variables

To use one of your virtual environment variables inside of your Python


project, you need to access it. You can do that for example with Python's os
module that comes packaged with the standard library:
import os

secret = os.environ['MY_SUPER_SECRET_SECRET']
print(secret)

You'll be able to run this code from any file in your project, as long as your
virtual environment is activated, and an environment variable called
MY_SUPER_SECRET_SECRET is defined.

If you don't want your script to terminate with an exception when you didn't
define the environment variable, then you can use Python's dict.get()
method instead of doing a direct lookup.

Recap

Setting environment variables inside of your Python virtual environments


allows you to easily access project-specific setting information while
reducing the danger of accidentally committing them to public version
control.

Note: Make sure that you add your virtual environment folder to your
.gitignore file, or you'll end up pushing your secrets to GitHub after all.

Don't worry too much about GitHub and version control if you're not yet
familiar with it. You can head over to the Git and GitHub course to study up
any time you feel like it. For now, it's not essential to understand it in order to
continue with this course.

In these lessons about environment variables you learned how to:

Add and remove Python environment variables in Bash


Set up project-specific environment variables inside of your Python
virtual environments
Access environment variables with Python

In the next section, you'll dive a bit deeper into more advanced Python
concepts before later learning about APIs and databases, to prepare you for
using Python to interact with the world-wide web.
Intro To New Concepts
In the upcoming section, you'll learn about some more advanced topics of
the Python language in small and self-contained lessons.

You might not need to work with these concepts yet, but it's still good to
know about them and spend some time practicing them, too.

The topics you'll learn about are:

Different ways of using the import statement


Using your custom code as a Python module
Working with if __name__ == "__main__:" in Python
Other types of Python comprehensions
Using Generator expressions
Using Lambda expressions
You'll also learn about the difference between expressions and statements at
the end of the section.
Different Ways Of Importing Code
You've imported code that you wrote in a different file into your scripts
before. In Python, this is relatively straight-forward if you're working in a
folder structure like shown below:

codingnomads
├── ingredients.py
└── soup.py

If you're working in soup.py, you can get access to anything you've defined
inside of ingredients.py with a familiar import statement:

# soup.py
from ingredients import carrot

This is generally the recommended way of importing from a module, and in


this case, it gives you access to the carrot variable that you defined in your
ingredients.py file.

Info: You can create the folder structure and files to work alongside this
lesson. To make the import work in soup.py, you'll need to define a variable
called carrot in ingredients.py.

After you've imported the code, you can now use it in your other script:

# soup.py
from ingredients import carrot

print(carrot)
You can use the carrot variable in any way you want to, just as if you had
declared it directly in the soup.py file.

Namespace Preservation

However, there are also other ways of importing modules in Python.


Sometimes it might be helpful to keep it completely clear where some code
that you're using is defined. You can shorten your import statement and
preserve its namespace:

# soup.py
import ingredients

print(ingredients.carrot)

With this type of import, you need to call any variables or functions defined
in ingredients.py through the ingredients. namespace.

Alias Imports

Sometimes you want to preserve the namespace, but you don't want to keep
typing out a long word such as ingredients over and over again. You can
assign an alias to your code module during the import:

# soup.py
import ingredients as i

print(i.carrot)

If you assign an alias to your module name during import, using the as
keyword, then you can access its namespace through the alias you gave it. In
the code snippet above, the alias for ingredients is just the single letter i.

When you'll work with some popular external Python packages, you'll notice
that some of them are by default aliased during import, for example:

import pandas as pd
import numpy as np

These aliases are only conventions that help to preserve the namespace of
the packages, but allow the developers to type less code. If you haven't
encountered these packages during your online research, look them up so
you have an idea of what they are used for.

Recap

For code files that share a folder with each other, you can import your own
custom code in the same way that you can import modules from the
standard library or even external packages.

During import, you can decide whether you want to retain the namespace
and import everything or pick only specific pieces of information that were
defined in the original module. You can also assign an alias to your imports,
to reference them in a more convenient way.

But how can you import your own code from files that aren't right in the
same folder as your current script?
Nested Namespaces
In the previous lesson, you learned that you can import your own code in the
same way that you can import modules from the standard library, as well as
external packages that you've installed with pip.

But what if your code is nested in additional folders:

codingnomads
├── ingredients.py
├── recipes
│ └── soup.py
└── cook.py

How can you access the logic you wrote in soup.py from your cook.py
script?

Deeper Namespaces

In a similar way to how you've used a namespace before with the


ingredients.py script, you can do it with your recipes/soup.py file as well:

# cook.py
import ingredients as i
import recipes.soup as s

c = i.carrot
s.make_soup(c)

In this example, you can see that Python treats the additional folder as an
extra namespace. Depending on how you choose to import your code, you
can access it through the full namespace, recipes.soup, or through an alias,
as shown in the code snippet above.

Note: For the code snippet to work, you need to have both a carrot variable
defined in ingredients.py, as well as a make_soup() function in
recipes/soup.py that takes one argument. You can only import and use
what you've actually defined.

Python allows you to handle namespaces in a way that can seem intuitive
when you're used to working with folder structures.

In the next lesson, you'll run into a little side-effect that you might encounter
when you're importing code from your custom modules.
Unexpected Messages
You've defined a couple of yummy ingredients in your ingredients.py
module:

# ingredients.py
def prepare(ingredient):
return f"cooked {ingredient}"

carrot = "carrot"
salt = "salt"
potato = "potato"

print(prepare(potato))

Because you spent some time writing your code and you were also checking
its functionality, you kept a print() at the bottom of your file:

print(prepare(potato))

So far that all seems fine, and if you execute ingredients.py, it all seems to
work normally.

Side Effects From Importing

You head over to soup.py and import just your carrot once again:

# soup.py
from ingredients import carrot
However, now you're getting some unexpected output in your console:

# OUTPUT:
cooked potato

Huh?! Where did the potato come from, and why is it already cooked?
You've explicitly imported only the carrot so far. Does that mean you can
access the potato variable in soup.py:

# soup.py
from ingredients import carrot

print(carrot)
print(potato)

The output of running this doesn't make the mystery less mysterious:

# OUTPUT:
cooked potato
carrot
Traceback (most recent call last):
File "/Users/martin/codingnomads/cook.py", line 9, in <module>
print(potato)
NameError: name 'potato' is not defined

Your CLI clearly prints the cooked potato, but Python doesn't know anything
about the potato variable and throws its digital hands in the air with a sigh of
NameError. What's going on here?

Info: When you import code from a module, you execute the whole script. If
there's any code execution written in the script, it'll run and potentially
produce some unexpected output.

Python runs the call to your print() function inside of ingredients.py when
you're importing even just the carrot variable in a different script. But that's
not what you want! You just want access to the carrot, and leave the potato
uncooked and in storage over in ingredients.py.

Recap

When you import code from a module, Python executes the whole script.
Any leftover function calls will also execute, and they might produce
unexpected results.

In the next lesson, you'll learn how you can avoid running into this.
Dunder Name
To avoid code execution in your source module during import, you can do
two things:

1. Remove any code execution from the file and make sure it only defines
functions, classes, and variables without doing anything else.
2. Use the __name__ namespace and nest your code execution there.

If you nest your code execution in a specific indented block, you'll still be
able to run the file directly and have it produce output, but avoid unexpected
code execution when importing some values from the module.

Dunder Name

To avoid executing code during imports, but leaving it in the original module
file, you can add the following code to the bottom of your module:

if __name__ == "__main__":
# your code execution goes here

For example, when you want to keep checking for cooked potatoes in
ingredients.py, but you don't want to cook when you're just importing the
carrot in soup.py, you can change the code in ingredients.py like so:

# ingredients.py
def prepare(ingredient):
return f"cooked {ingredient}"

carrot = "carrot"
salt = "salt"
potato = "potato"
if __name__ == "__main__":
print(prepare(potato))

If you add this line of code and indent the code execution underneath it, you
can run ingredients.py as you normally would and receive your cooked
potato:

python ingredients.py # OUTPUT: cooked potato

At the same time, if you now head back to soup.py and run the code, you'll
see that Python won't cook your potato and instead it'll only print back the
carrot that you wanted to:

# soup.py
from ingredients import carrot

print(carrot) # OUTPUT: carrot

So what happened here? When you add the line if __name__ ==


"__main__:", you give the following instructions to Python:

if you run this program directly instead of importing it as a module, do the


following:

Then, in the indented code block in the next line, you're calling the prepare()
function and print its result.

Recap

By adding if __name__ == "__main__:" to the bottom of your script, and


indenting any code execution below it, you achieve the following:

Running the module directly executes the nested code logic


Importing the module in another file skips that code logic

When you're working with different files and larger programs, this code
snippet can come in handy.

Don't worry if this is confusing and no need to break your head with it. If
you're interested to learn more, check out the linked resources below.
Python Comprehensions
You've learned about list comprehensions in an earlier section. In this lesson,
you'll get to know other Python comprehensions, which work with a similar
syntax:

listcomp = [x*2 for x in range(5)]


setcomp = {x*2 for x in range(5)}
dictcomp = {k: v*2 for (k, v) in {"a": 1, "b": 2}.items()}

You'll see that the different comprehensions do what you'd expect them to
do, and mainly present a shortcut syntax to writing for loop logic.

List Comprehensions

Python comprehensions are a concise way of writing some code logic that
you could also express in a for loop. You've already encountered list
comprehensions before:

list_ = [x*2 for x in range(5)]


print(list_) # OUTPUT: [0, 2, 4, 6, 8]
print(type(list_)) # OUTPUT: <class 'list'>

The list comprehension allows you to encode the logic you'd otherwise apply
in a for loop into a single line of code. In the example above, you're
multiplying each number provided by the range() object by 2, and you end
up with a new list object containing the results.

Aside from the most commonly used list comprehensions, Python also
provides comprehension syntax for other data types.
Set Comprehensions

You can use set comprehensions with a slight change to the syntax you're
used to, by using curly braces ({}) instead of the square brackets:

set_ = {x*2 for x in range(5)}


print(set_) # OUTPUT: (0, 2, 4, 6, 8)
print(type(set_)) # OUTPUT: <class 'set'>

This code snippet creates a new set() that contains the results of the
calculation applied to each number provided by range().

Dictionary Comprehensions

Another comprehension you might encounter is the dictionary


comprehension. Its syntax is slightly more involved, just like it's for loops
are. This is necessary because of the different structures of dictionaries:

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


dict_2 = {k: v*2 for (k, v) in dict_1.items()}
print(dict_2) # OUTPUT: {"a": 2, "b": 4}
print(type(dict_2)) # OUTPUT: <class 'dict'>

This code snippet creates a new dict() that contains the results of the
calculation applied to each value of the original dict_1 while keeping the
keys intact.

Recap

Python provides comprehension syntax for different data structures, such as


lists, sets, and dictionaries.
None of these constructs adds new functionality to your workflow. If you
prefer to write for loops, then stick with them. Python comprehensions aim
to give you a way to do some common tasks in a concise way, but remember
that readability is paramount! Always try to keep your code as legible as
possible, and don't let Python comprehension syntax get in your way of
doing that.

In the next lesson, you'll get to know generator objects, which follow a
similar syntax to Python comprehensions, but actually create something
slightly different.

Additional Resources

Python Comprehensions: http://python-3-patterns-idioms-


test.readthedocs.io/en/latest/Comprehensions.html
Generators
In the previous lesson, you got to know the syntax of Python
comprehensions. In generator objects, you might encounter an extremely
similar construct that behaves a bit differently:

generator = (x*2 for x in range(5))


print(generator) # OUTPUT: <generator object <genexpr> at 0x1106845f0>
print(type(generator)) # OUTPUT: <class 'generator'>

The only syntactic difference to a list comprehension is the different types of


parentheses used. Generators wrap the code logic in round parentheses
(()).

However, when you look at the output, you might be surprised. You don't
seem to be able to look into the generator object, and you can see that it
created neither a set() nor a list(), but something called a 'generator'.

Info: You'll dive much deeper into classes and objects in the third module of
this course, where you'll learn about object-oriented programming, and why
you keep encountering classes and objects in Python.

So what is this generator object, what's its use, and how can you work with
it?

Generators

Python generators allow you to work with iterables in a more memory-


effective way than if you were using a list comprehension. When you write a
list comprehension, it actually creates a new list, which is an object that
contains multiple other objects:
list_ = [x*2 for x in range(5)]
print(list_) # OUTPUT: [0, 2, 4, 6, 8]
print(type(list_)) # OUTPUT: <class 'list'>

When Python creates this list object, it needs to assign computer memory in
order to be able to store it somewhere. If you're working with huge lists, this
can potentially become an issue.

Info: You're nowhere near running into memory issues due to list creation
with the code you're writing in this course. However, when you're working
with large datasets, this challenge is real.

A generator on the other hand is only a single generator object. It doesn't


hold any references to other objects that take up memory themselves.
Instead, the generator only yields each item of the iterable once, while it's
needed, and then dumps it into oblivion:
Generators are slower in execution because when a single iterator should
be addressed, the item first needs to be created. However, they are much
quicker to create because there's no huge list object with thousands of
items in it that needs to be built.

In order to actually create and work with the items whose potential existence
the generator contains, you'll have to iterator over the generator object:

gen = (x*2 for x in range(5))


for i in gen:
print(i)

# OUTPUT:
# 0
# 2
# 4
# 6
# 8

Avoiding creating and storing objects that you won't actually need again can
lead to performance improvements by freeing up memory space.

Recap

You can create generator objects with a similar syntax to Python


comprehensions. The difference is that you're only creating one object that
has the potential to access multiple items in an iterable without yet creating
all the objects necessary for doing that. To work with the iterators, you need
to loop over the generator object.

Generators are especially useful when working with large amounts of data.

Additional Resources
Sentdex: List comprehensions vs. generators
Dan Bader: Python Iterators
Python Wiki: Generators
Jeff Knupp: Improve Your Python: 'yield' and Generators Explained
Lambda Expressions
Lambda expressions, also called anonymous functions, allow you to
define small functions in a single line of code. They can make your code
more concise, but they can be a difficult concept to grasp, so there is a
trade-off in keeping your code human-friendly and readable.

Lambdas And Normal Functions

In this lesson, you'll take a look at a lambda expression to learn how it relates
to a normal function:

def square_root(x):
return x**2

In the code snippet above, you defined a function, square_root() that takes
an integer, x, as its argument and returns that number squared.

You can write the same logic in a one-liner lambda expression:

squared = lambda x: x**2

In both cases, you've defined a function object, square_root and squared


respectively, that you can call with a number as an argument to get the same
results:

print(square_root(4) == squared(4)) # OUTPUT: True

As you can see, lambda expressions are just another way to write a function
in Python.

Info: If you're planning to assign a name to your function object, then you
should always opt for defining the function using the def keyword. While it's
possible to give a name to a lambda expression, this is not how they're
intended to be used. They are called "anonymous functions" for a reason!

So if it's just another way to write a function, why bother with lambda
expressions at all?

Anonymous Functions

As mentioned in the note above, you shouldn't use a lambda expression to


name a function object. Lambdas are meant to be anonymous functions that
you use as a one-off input to another function.

You've probably used Python's sorted() function before. This function has
an optional key parameter that allows you to pass a function to apply some
logic to each element before performing the sort. For example, if you wanted
to sort the ingredients list of tuples shown below by their amounts, you can
do this by passing an anonymous function to the key parameter:

ingredients = [("carrots", 2), ("potatoes", 4), ("peppers", 1)]


sorted(ingredients, key=lambda x: x[1])

The sorted() function will apply the lambda expression to each item of the
iterable, in this case, each tuple of the shape ("name", number). Similar to a
loop variable, you can think that x will be each of the tuples contained in the
list. You define that with the first part of the lambda expression, lambda x:.

Then, you're defining what the anonymous function should do with each of
the tuples, x, and in this case, you're telling the code to pick out the second
item of each tuple with x[1].

This means that your lambda expression will pick the second item of each
tuple, which are the numerical amounts, use them to create a new iterable,
and sort your original values based on the sorted order of that new iterable:

# OUTPUT:
# [('peppers', 1), ('carrots', 2), ('potatoes', 4)]

As you can see, this code outputs a new list that is sorted ascending by the
second element in the tuples, which would be hard to achieve without using
the lambda expression.

Functional Programming

This way of programming is part of a paradigm called functional


programming that is different from object-oriented programming that you'll
mostly work on within this course. Python has the capability to work in both
ways, which is one of the reasons why it's such a versatile language.

At the same time, this fact also explains why lambda expressions can
sometimes appear foreign. You're used to working with Python in an object-
oriented pattern, and from that perspective, lambda functions don't quite fit
the mold.
While you won't need to use lambda expressions in your programs, it's still
useful to understand the basics about them, and when it might be helpful to
use one.

Other examples of functions that can take function objects as arguments are
Python's filter(), map(), and reduce() functions. They are often used with
lambda expressions to define the logic that'll be applied to all elements in an
iterable. These functions are also a common way to start thinking about
concepts that are common in functional programming:

squares = list(map(lambda x: x**2, range(10)))


print(squares) # OUTPUT: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Using map() with a lambda expression as its first input, and an iterable
range() object as the second input, you just calculated all square numbers
from 0 to 10 and added them to a new list. Pretty neat and compact!

Note: You need to explicitly convert the output of map() to a list, because
filter(), map() and reduce()return generator objects instead of lists in
Python 3.

If you're thinking that you could have done the same thing more concisely
with a list comprehension, then you're right in thinking so! The list
comprehension can do the same thing and is arguably more readable for
many developers:

squares = [x**2 for x in range(10)]


print(squares) # OUTPUT: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

As you can see, there's often no real advantage to using a lambda


expression in these situations. They are most useful when you need to pass
a function as an argument to another function, as mentioned further up. For
example, you couldn't pass a list comprehension to the key parameter in
sorted(), because the function expects another function object.

Recap

Lambda expressions are anonymous functions that are primarily used to


pass function logic to another function as an argument. The receiving
function can use this logic and for example, apply it to all elements in an
iterable. Lambda expressions are more common in functional programming
and might seem unfamiliar. Most of the time, you can use different logic
instead of a lambda expression, for example by defining a function object or
using a list comprehension instead.

Additional Resources

Real Python: How to Use Python Lambda Functions


Python Conquers The Universe: Lambda tutorial
U-Arizona: List Comprehensions
David Z on StackOverflow: Lambda functions
Expressions And Statements
You've heard about expressions and statements before, maybe without
paying that much attention to them. When you're reading through online
programming tutorials and blog posts, you might keep encountering those
two terms frequently.

In this lesson, you'll learn about the difference between an expression and a
statement.

Expressions

Expressions are a combination of programming logic that evaluates to a


result. For example, adding two numbers together is an expression
because it results in the sum of the two numbers:

2 + 3 # OUTPUT: 5

In the code snippet above, you can see that 2 + 3 has a result, 5. When you
execute the expression 2 + 3, you'll hold a value in your digital hand. This
makes 2 + 3 an expression.

Statements

Statements are a more general term for all sorts of pieces that make up
your code. In programming, statements are the building blocks that
constitute a program.

Programs are built of consecutive statements that are ultimately combined


to produce a result. A statement is made up of components that can be
other statements as well as expressions.
An example of a statement is the variable assignment:

x = 2

In this variable assignment, you're creating a reference between the variable


x and the value 2.

This statement doesn't have a computed result, contrary to an expression


like the one shown further up. All you do is assign a value to a variable.

Expressions And Statements In A Program

In any complex program that you'll write, you'll use both expressions and
statements together. A basic example of a statement that comes in
combination with an expression is:

x = 2 + 3

If you pick this short code snippet apart, you'll see that while the complete
line of code is a statement, it also contains an expression:

Expression: On the right side of the statement, there is an expression, 2


+ 3, that evaluates to the integer 5.
Statement: The statement x = expression saves the value of the
expression 2 + 3 in the variable x.

Your code will be made up of both, but sometimes it's helpful to be able to
make a distinction between the two. When?

Info: Keep training in the habit of researching any questions you might have.
It's extremely important for your continuous learning journey. The skill of
answering your own questions through effective online research will stay
relevant throughout your work as a software developer.

One example of where the distinction between statements and expressions


is important is when passing arguments to functions. You can pass an
expression as an argument to a function, but you can't pass a statement as
an argument.

Recap

As a rule of thumb, you can remember that expressions evaluate to a result


and statements don't.

Additional Resources

StackOverflow: What is the Difference Between an Expression and a


Statement in Python
PROJECT: File Counter Database
Revisit your file counter script. Instead of writing the logs of each run to a
file, you'll set it up to write to a SQL database instead.

Info: If you need a refresher on interacting with a database using Python,


check out the Working with Databases in Python section of the linked
course.

Collect the same information you saved to a CSV file before into a database
instead. You can use a file-based SQLite database or a server-based
database such as PostgreSQL or MySQL.

Additionally, look into Python's datetime module and attempt to track the
times when you ran your script.

Data Analysis

Once your script is set up to save its run data into your database, you'll next
write a script to analyze your collected desktop statistics.

Use your Python skills to calculate:

The total number of files that was on your desktop since you started
tracking it
The total number for each file type
On what day you had the most items on your desktop
The most common file type ever to clutter your desktop

Write your results to a new table in your database and set your analysis
script up so that it keeps updating this table on each execution.

Tasks
Write a script that stores the information returned from your file counter
script in a SQL database.
Read the saved data from your database and analyze it.
Write the results of your analysis to another table in your database.

Your program should move away from using CSV or JSON files, but you
should still be able to track your data over multiple executions.
Limits Of The Mighty Print
Until now, you've used the mighty print() function to inspect the state of
your code at specific points and used it to figure out what's going on.
Printing variables gives you insight into what your variables are like at any
stage of the execution of your script. Using print() is a straightforward and
powerful ally when debugging your code.

More Code, More Problems

However, once you'll start dealing with larger applications, you'll sooner or
later hit the ceiling of possibilities with print(). You might have already been
there while learning about APIs and databases in the previous section.

And this is completely normal! The more code you'll write, and the more
complex it'll get, the more errors ("bugs") will find their way in there.

Because debugging is such a common task in software development, there


are whole libraries built just to make it easier and more effective.

Recap

Complex programs are likely to have multiple software bugs in them. In this
section of your course, you'll take a look at tools that can help debug your
Python programs.

And even if bugs may be annoying sometimes, don't be mean to them! They
are often pretty cute, and there's a lot you can learn from them:
More Visual Debuggers
The text-heavy black-and-white style of the pdb debugger can seem
unintuitive to some people. If you prefer a more visual interface, you can
swap out pdb for another debugger, for example:

pudb: a more visual command-line-based debugger


web-pdb: a visual debugger displayed in your web browser with clickable
UI

In this lesson, you'll look at these two alternatives to pdb and learn how you
can swap your debugger without needing to change your code.

Colorful CLI Debugger With Variable Inspector

The first alternative debugger, pudb, sounds very similar to pdb. However, if
you use it you'll see that it's a whole different visual experience.

pudb is a third-party package, which means that you need to install it with
pip before you'll be able to use it:

python3 -m pip install pudb

Note: Remember to create and activate a virtual environment before


installing any external packages on your local machine.

After installing pudb, you need to edit the relevant environment variable,
PYTHONBREAKPOINT. This will instruct Python to use the newly installed
debugger instead of the default one:

export PYTHONBREAKPOINT=pudb.set_trace
After setting the PYTHONBREAKPOINT environment variable to pudb.set_trace,
you can execute your script in the same way you did before:

python3 your_file_name.py

Python will now launch pudb for you where it launched pdb before:

As you can see, this looks quite different from the strictly text-based pdb that
you encountered in the previous lessons. A notable improvement is the
variable inspector in the top right of your terminal, where you can see all
currently defined variables and their values at the line where pudb is currently
pointing to.

You can use the same commands you learned before to step through your
program and investigate what's going on in there.
Web-Based Debugger With Clickable User Interface

Another option you can install as an external package is web-pdb. This


debugger provides an even more user-friendly visual interface. It'll start a
server on your localhost that you can access in your browser. The UI of web-
pdb even allows you to click buttons instead of typing the pdb commands.

Start by installing the package using pip inside of your virtual environment:

python3 -m pip install web-pdb

Just as you did with pudb before, you'll need to edit the PYTHONBREAKPOINT
environment variable to point to web-pdb instead:

export PYTHONBREAKPOINT=web_pdb.set_trace

Now you can execute your script in the same way you did before:

python3 your_file_name.py

Your console will show you some output that indicates that the web server
has been started:

2031-03-07 19:51:28,894: root - web_console:108 - CRITICAL - Web-PDB: starting web-se

When you see this output, you can open up your browser and go to the
following URL:
http://localhost:5555/

Once the page has loaded, you should see the interface of a modern user-
friendly debugger:

You'll notice that there's also a variable inspector at the top right of the
interface and it generally looks quite similar to pudb. However, you can use
buttons to interact with your interactive debugger session.

Recap

By default, breakpoint() calls Python's standard library debugger pdb. This


debugger comes with the standard library, but sometimes you might want to
use a more visual interface to make your debugging more straightforward.
You only need to change a single environment variable, PYTHONBREAKPOINT, to
use a different debugger. You don't need to change anything in your code.

This can allow you to debug with different tools in different environments.
For example, you could use a debugger with a graphical user interface on
your local development machine, and use a plain text one on your server
without needing to change the code.

Here's an example of how you can change the relevant environment variable:

export PYTHONBREAKPOINT=web_pdb.set_trace

The command shown above sets the environment variable


PYTHONBREAKPOINT to use web_pdb as your debugger, instead of the default
pdb. If you now run your script, a different debugger session will be started.

In the next lesson on debugging, you'll see how you can use a built-in
debugger that comes with most modern IDEs.

Additional Resources

pudb: pudb Documentation


web-pdb: web-pdb Documentation
Integrated Debuggers
You'll spend a lot of your time as a developer debugging code. That's why
more professional text editors and IDEs usually include an integrated
debugger.

From seeing the more visual debuggers, such as pudb and web-pdb, you're
already familiar with the general idea of an interface that these visual
debuggers provide. You can do everything that you did with the pdb-based
debuggers, and often much more.

Info: IDEs with integrated debuggers usually also include a visual way to set
breakpoints, so that you don't need to add any additional lines of code into
your script. In VS Code you can do this by clicking into the margin next to the
line numbers.

As an example of a professional integrated debugger, you'll take a look at the


one that comes with VS Code.

Accessing the Debugger in VS Code

To access VS Code's debugger, you can press the dropdown next to the Play
symbol on the top-right corner of the app, then select Debug Python File:
Alternatively, you can also access it by opening the command palette by
pressing Cmd+Shift+p on macOS, or Ctrl+Shift+p on Linux and Windows,
typing debug, and pressing Enter:

This will execute your script as a debugging session, which means that any
visual breakpoints you set in the margins of your script will take effect. The
built-in debugger will stop execution at the breakpoints and launch the
interactive session.

Interacting with VS Code's Debugger

VS Code's debugger allows you to visually set breakpoints, step through


execution, inspect all current variable values, and more.

However, just like the visual debuggers you've encountered in the previous
lesson, it works in a more user-friendly manner. You have access to buttons
instead of text commands.

You'll also notice the Variables panel, and how it displays the current variable
values. VS Code's debugger also uses color-highlighting to show eventual
changes:

Don't get overwhelmed if there seems to be too much lot going on. Try to
focus on what you're interested in right now and recreate the more basic
functionality of pdb. You can keep learning your built-in debugger slowly
Introduction To Your Task
In this section, you'll combine the skills you have learned so far to continue
to build out your game project with information that you'll gather from the
Internet through an API. You will:

Interface with an API


Incorporate the data in your game project

Each piece of the project will build on concepts that you have covered in
earlier sections of the course. This section will walk you through adding an
API call to a name generator API to your game.

At the end of the section, you'll be tasked to integrate another API of your
own choice into your gameplay.
The Name API
Just like in the APIs and Databases course, you'll use the requests library to
connect to an API and receive the responses.

For this walkthrough, you'll use the Uzby name generator API to create
random names for your players. You'll work with this specific API because
it's relatively straightforward and is very focused on doing just this one thing.

Before you can start working with an API, you'll always need to read through
its documentation to know how to interact with it. The Uzby API
documentation is quite short and fits on one page:
Essentially, you'll make your request to the following URL, providing a
number as a parameter for MINIMUM_LENGTH and MAXIMUM_LENGTH:

https://uzby.com/api.php?min=MINIMUM_LENGTH&max=MAXIMUM_LENGTH
If you wanted to create a random pronounceable name between a length of 4
and 8 characters, you can visit this URL in your web browser:

https://uzby.com/api.php?min=4&max=6

You'll see a very basic result in your web browser. It's just what you asked
the API to get for you:

You can make the same request that you just made with your web browser
with Python instead. You'll revisit how to do that in the next lesson.
Explore The API
Create a new virtual environment for your game project and install the
requests library. Then, create a new Python script to test making the API
request and receive a randomly generated name.

Because of the user-friendly abstractions of the requests library, you don't


need a lot of code to make this API request:

import requests

min_len = 4
max_len = 6
URL = f"https://uzby.com/api.php?min={min_len}&max={max_len}"

response = requests.get(URL)
print(response.text)

After you run this short script, you'll get a name as a string printed out to
your console:

Zaiki

Keep in mind that these names are randomly generated, which is why your
output will be different, even if you use the same numbers for min_len and
max_len.

Tasks

Explore the API some more by making more calls with different values
for min_len and max_len.
Can you change the arguments the API takes to receive an error
message from the API? What does it look like?

In the next lesson, you'll incorporate this API call into your CLI game code to
generate a random name for your player, based on the real name they
provide.
PROJECT: Include More APIs
You've just stepped through the process of including an API call into your
local game script. Way to go!

As you know by now, you'll have to train each skill a lot to solidify your
knowledge. This is why you'll pick another API of your own choice for your
next task and incorporate its functionality into your game as well.

The Internet is full of data and offers a large chunk of it through freely
accessible APIs. You'll find some links to aggregated API resources at the
bottom of this page.

Possible API Ideas

If you're stuck coming up with an idea for an API call that you could include
in your project, then you can get some inspiration from the suggestions
below. Feel free to use something completely different, however! The most
important part is that it's interesting for you to work with and that it adds a
new aspect to your game, even if it's just a very small one like the random
name generator that you implemented earlier in this section.

You could include:

Weather information: Create in-game weather based on the actual


weather of your players, and allow them to find different items
depending on the weather.
Location information: Combine location information with a real-time
weather API. Ask your players where they are, then use the response of
the API calls to influence the gameplay.
Name meanings: Instead of assigning your players a random name,
look up the meaning of their real name and add a fitting modifier to it,
e.g. "..., the Brave".
Word meanings: Allow your players to find items with strange names.
Add the possibility to look up the meaning of these words through an
API.
Recipes: Give players the possibility to look up recipes from the food
items they collected.
Conversations: Display real-life social media conversations that your
players can listen to.
Translations: Give players a choice to read your game instructions in
different languages.

Some of these tasks will be easier to implement than others, and it'll depend
to a large degree on the API that you choose. To get started, pick an API
that:

is free to use
doesn't require authentication
has well-made and well-maintained documentation

You'll make your task easier if you select your API according to these criteria.

Of course, if you feel brave and curious, and you have an idea in mind, then
go ahead and implement it even if the API you want to use doesn't make it
quite as straightforward as the Uzby API you've worked with before.

Note: If you choose to work with an API that requires authentication, make
sure to keep your API key information out of your production code. You can
do this for example by adding it as an environment variable.

The best way to learn new skills and train them is to follow your interest and
keep putting in the time and effort.

Additional Resources

API list: Free APIs


Dev.to: 10 fun APIs to use for your next project
Once you've incorporated your new API functionality into your game code,
make sure to share it on the forum in the Command-line Games thread.
Example Solution
This lesson includes an example implementation of including the Uzby
random names API into your game code.

Info: Keep in mind that there are multiple ways to solve this challenge, and
that writing code is a creative activity where you get to express your
thoughts. If you've solved the challenge in a different way, then that is
perfectly fine!

You could incorporate the API call into your game like so:

import requests

name_length = 0
# Check whether it meets the length requirements for the API call
while not (name_length >= 2 and name_length <= 40):
# Collect the name from your player
original_name = input("Enter your name: ")
name_length = len(original_name)

# Ping the Uzby API to create a new random name for your player,
# using the length of their given name as input
URL = f"https://uzby.com/api.php?min={name_length}&max={name_length}"
response = requests.get(URL)
name = response.text

# Inform the player about their in-game name


print(f"Welcome to the dungeon! In this world, your name won't be {original_name}
print(f"Instead, you'll be know as {name.upper()}!")

# Your game code follows here

When you add this code to your CLI game, it now interacts with the web to
gather data from an API based on the input of your players. Nice work! This
might not be enough for your game to qualify as an online immersive game,
but you're learning to use Python to interact with data on the Internet.

However, what happens now if your players don't have an Internet


connection?

Tasks

Switch off your Internet connection and try running your game script.
What error do you receive?
Can you identify the name of the error in your traceback?
Research the error online in the documentation of requests.
How can you avoid your players running into that error?

Because generating a random name through an API call isn't an essential


feature of your game, it would be better if the game wouldn't crash if
someone plays it without an Internet connection. There are ways to account
for that.

In the next module of this course, you'll learn how you can write code to
catch errors that your programs run into. You'll learn about exception
handling, and you'll get the chance to improve your scripts and make sure
that your game is playable also without an active connection to the Internet.

Before you move on to the next module, however, you'll learn about another
important concept in software development that isn't exclusive to Python. In
the final section of this module, you'll learn the essentials of version control
with Git and GitHub.

Info: If you want to dive deeper, check out the dedicated course on Version
Control with Git and GitHub.

In order to be able to share your game with the world and allow others to
collaborate on your code, you'll need to put it up on the Internet. In the next
section, you'll learn the basics of how you can do that with Git and GitHub,
as well as what it means to put your code under version control.
Wrap Up Module Two
After completing your API project and putting it under version control on
GitHub, you've successfully completed the second module of this Python
course. Congratulations on your progress :)

You've solidified your knowledge about the Python programming


language, as well as about programming concepts in general. You can
now:

Install Python
Understand what programming is and why it is useful
Read, write, and understand Python code
Use Python as a scripting language
Write Python in the REPL and as a script
Create variables and assign values to them
Work with text and numbers
Identify and use common data types, such as:
int
float
str
tuple
list
set
dict
bool
None
Plan out your task with pseudocode
Document your reasoning and decisions using code comments
Write loop logic to tackle repetitive tasks
for loops
while loops
list comprehension
Make comparisons and calculations with operators
Assignment operator
Arithmetic operators
Membership operator
Relational operators
Logical operators
Identity operator
Make decisions with conditional logic and control flow
if, elif, else statements
Looping keywords: break, continue, and return
Collect user input with input()
Format your strings with f-strings
Write and call functions to avoid repetition and generalize your code
logic
Provide input to your functions with arguments, and return values to
reuse them
Use positional and keyword parameters
Document your functions with docstrings and type hinting
Understand scopes in programming
Automate repetitive tasks on your file system using the pathlib module
Work with File input/output on your computer
Install and use third-party Python packages
Use sqlalchemy to interact with any SQL database through Python
Use the requests package to interact with APIs on the Internet

You've also learned about additional concepts and techniques that are
important in your day-to-day work as a software developer, such as:

Growth mindset: you'll have to always keep learning, and that's a good
thing
Error messages: friendly messages from Python or your operating
system, that help you to identify any miscommunications you had with
your computer
Operating systems: they are different from each other, but there are
also many similarities
Development environment: installing and working with the tools you
need to write code productively on your local machine
Virtual environments: compartmentalize your development
environment
Environment variables: separate sensitive information and settings
from your production code
Databases: high-performance software built to persistently store and
retrieve your data
APIs: structured data provided over the Internet that you can
programmatically consume
Debugging: find and fix bugs in your code, and use tooling to make that
process more productive
Version control: with Git and GitHub to save snapshots of your projects
and open your codebase up for collaboration
Refactoring: go back into the code you wrote earlier and improve it

At this point, you know enough Python and you have the additional
necessary software development skills to tackle a wide variety of projects.
Speaking about projects, you've already completed a few.

Projects

You've also applied code logic concepts, correct Python syntax, and your
creativity to build out projects:

File Renamer: Move and rename files in a folder on your file system
CLI Games: Build CLI games
Guess My Number
Hangman
Adventure role-playing game with API interactions

You've worked on your projects in multiple sections of the course, going


back to refactor, rewrite, and enhance the code you wrote earlier. This is
extremely common when you're working on a code project, and an important
aspect of software development to train and get used to.

You've already created fun games and useful automation scripts that you
can be proud of and that you can showcase on your personal GitHub
account.

Next Steps

The next course in this series is Python 301 which shifts focus to learning
Python as an object-oriented programming language. You'll get to know
what exactly it means that:
Everything's an object in Python

You'll finally dig deeper into the concept of objects and learn about their
attributes and methods. You've already used a lot of the concepts you'll
learn about, but now you'll finally better understand why things worked as
they did.

Additionally, you'll also learn about approaches to testing and exception


handling in Python, important concepts that will help you to round off your
skills as a budding software developer.

You might also like