python_introduction/learning_python3.md

108 KiB

What we'll learn

You'll learn three things at the same time so don't get discouraged if it feels a bit much at the start. Everybody's issues will be in these three different domains and at the beginning it can be difficult to differentiate between them. Keep this in mind, everybody has to go through this stage and the click comes at different times for different people but everybody clicks at some point! The three new things you'll learn:

  1. the concepts of programming, most notably Object Orientated Programming (OOP)
  2. the syntax of one particular language, in our case python3
  3. the tools needed to start programming in our language of choice

Within each of these topics there are subtopics but these are not bottomless! Below is a small overview of how I would subdivide them.

Concepts

The subtopics behind the concept of programming can be sliced (in no particular order) as follows:

  • objects or OOP (Object Orientated Programming)
  • variables (which are not boxes in python3)
  • conditional logic
  • functions
  • loops

The concepts behind these topics are the same in most modern languages, it's just how you write them that is different. This how is part of the syntax of the language.

Syntax

In computer science, the syntax of a computer language is the set of rules that defines the combinations of symbols that are considered to be correctly structured statements or expressions in that language. This applies both to programming languages, where the document represents source code, and to markup languages, where the document represents data. The syntax of a language defines its surface form.[1]

The quote above is taken shamelessly from wikipedia.

Tools

Writing code

Scripts are text files, plain and simple. So in order to write a python3 script all we need is a text editor. Nano, vim, notepad++ all do a good job of editing plain text files but some make it easier than others. One of the many features of an IDE is syntax highlighting. It colors things such as keywords which makes our life so much nicer when writing code. We'll come back to these features in a bit.

Running code

In order to run python3 code you need the python3 interpreter. This is because when you execute your script, the interpreter will read and execute each line of the text file line by line.

Most people who want to write and run python3 code, or any language for that matter, will install an Integrated Development Environment to do so. There are no rules as to what has to be included for a program to qualify as an IDE but in my opinion it should include:

  • syntax highlighting
  • autocomplete
  • goto commands such as goto definition, goto declaration, goto references
  • automatic pair opening and closing
  • builtin help navigation

There is a plethora of IDE's available and you can't really make a wrong choice here, but to make the overall learning curve a bit less steep we'll start out with a user friendly IDE, pycharm.

The python3 shell

TODO animated overview of the shell and the world of OOP

Installing pycharm

Depending on the platform you use, you can install Pycharm in multiple ways. The computers in the classroom come with Pycharm installed but if you want to continue working from home you can follow the instructions here.

If you run Windows at home you will probably need to install python as well. Download the latest version from here. On Linux and Mac OSX there will probably be some version of python installed. From a terminal you can check the version that's installed by executing the following commmands.

waldek@metal:~$ python3 --version
Python 3.9.2
waldek@metal:~$ 

Virtual environments

TODO

Your first project

In almost any language you'll find a helloworld program. It serves to illustrate a very basic working script or program to showcase the syntax. In python a helloworld is done as such.

print("Hello World!")

Just for reference below are a few helloworld programs in different languages. First c# then c then c++ and last but not least javascript.

c#

Console.WriteLine("Hello World!");

c

#include <stdio.h>

int main() {
   printf("Hello World!");
   return 0;
}

c++

// Your First C++ Program

#include <iostream>

int main() {
    std::cout << "Hello World!";
    return 0;
}

javascript

alert( 'Hello, world!' );

How to execute

  • within pycharm
  • from the command line

Simple printing

The most basic printing can be done by calling the print function. In python a call is symbolized by the (). In practice this becomes as follows.

print("hello world")
print("my name is Wouter")
print("I'm", 35, "years old")

print is a built-in function. We can prove this in a shell, plus we can read it's docstring.

>>> print
<built-in function print>
>>> print(print.__doc__)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file:  a file-like object (stream); defaults to the current sys.stdout.
sep:   string inserted between values, default a space.
end:   string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
>>> 

🏃 Try it

Try printing different lines and with combinations of different object types such as int, float and str. What happens if you add (+) values to one another? What about subtracting? And multiplication and division?

We can also print the objects referenced by variables. A simple example:

name = "Wouter"
age = "35"

print("Hello, my name is", name, "and I'm", age, "years old.")

While it works perfectly fine it's not super readable. We can improve the readability by using either string replacement or string formatting. My personal preference is string formatting.

🏃 Try it

Have a look at both ways illustrated below and try them out.

String replacement

name = "Wouter"
age = "35"

print(f"Hello, my name is {name} and I'm {age} years old.")

String formatting

name = "Wouter"
age = "35"

print("Hello, my name is {} and I'm {} years old.".format(name, age))

Taking input

The first built-in function we saw is print which can be used to signal messages to the user. But how can we get some information from the user? This is done with the built-in input function. If we open up a python shell we can observe it's behaviour.

>>> input()
hello world
'hello world'
>>> 

It seems to echo back what we type on the empty line. If we take this idea and add it to a script the behaviour changes slightly. The prompt appears but when we hit enter the text is not printed. This is one of the slight nuances between running scripts and using the shell. The shell is more verbose and will explicitly tell you what a function returns, unless it doesn't return anything.

input is, like print, a built-in function. We can prove this in a shell, plus we can read it's docstring just as with print.

>>> input
<built-in function input>
>>> print(input.__doc__)
Read a string from standard input.  The trailing newline is stripped.

The prompt string, if given, is printed to standard output without a
trailing newline before reading input.

If the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError.
On *nix systems, readline is used if available.
>>> 

Some functions are blocking

When we call print the function is executed immediately and the python interpreter continues with the next line. The input function is slightly different. It is called a blocking function. When a blocking function is called the program is waiting for some action. Once the condition is met, the program continues. It's important to be aware of this but don't overthink it. We'll get back to this behaviour later.

Functions can return something

So, functions can return something but how can we use the returned objects? This is where variables come in handy. The input function will always return an object of type str. If we want to use this object later in our code we need to add a post-it to it so we can reference it later. Remember that the object is created by the function call, and we add the reference after the object's creation.

print("What is your name? ")
answer = input()
print("Well hello", answer, "!")

When looking at the code block above did you notice the empty space I added after my question? Can you tell me why I did that?

🏃 Try it

Try playing around with the input function and incorporate the different ways to print with it. Ask multiple questions and combine the answers to print a sensible message on one line. Can you create a simple calculator? If not, can you explain me why?

Functions can take arguments

Some, if not most, functions will take one or more arguments when calling them. This might sound complicated but you've already done this! The print function takes a-message-to-print as an argument, or even multiple ones as you probably noticed when playing around with + and ,.

The input function can take arguments but as we've seen does not require an argument. When looking at the documentation we can discover what the function does, how to call the function and what it returns.

CTRL-q opens the documentation in pycharm

Help on built-in function input in module builtins:

input(prompt=None, /)
    Read a string from standard input.  The trailing newline is stripped.
    
    The prompt string, if given, is printed to standard output without a
    trailing newline before reading input.
    
    If the user hits EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return), raise EOFError.
    On *nix systems, readline is used if available.

We can add one argument inside the input call which serves as a prompt. Now which type should the object we pass to input be? The most logical type would be a str that represents the question to ask the user no? Let's try it out.

answer = input("What is your name?")
print("Well hello {}!".format(answer))

🏃 Try it

Modify the questions you asked before so they have a proper prompt via the input function. Ask multiple questions and combine the answers to print a message on one line. Try it with different print formatting.

Taking input and evaluation

We can do a lot more with the input from users than just print it back out. It can be used to make logical choices based on the return value. This is a lot easier than you think. Imagine you ask me my age. When I respond with 35 you'll think to yourself "that's old!". If I would be younger then 30 you would think I'm young. We can implement this logic in python with an easy to read syntax. First we'll take a python shell to experiment a bit.

>>> 35 > 30
True
>>> 25 > 30
False
>>> 30 > 30
False
>>> 30 >= 30
True
>>> 

The True and False you see are also objects but of the type bool. If you want to read up a bit more on boolean logic I can advise you this page and also this. This boolean logic open the door towards conditional logic.

Conditional logic

Let's convert the quote below to logical statements.

If you are younger than 27 you are still young so if you're older than 27 you're considered old, but if you are 27 on the dot your life might be at risk!

age = 35
if age < 27:
    print("you are still very young, enjoy it!")
elif age > 27:
    print("watch out for those hips oldie...")
else:
    print("you are at a dangerous crossroad in life!")

Do not fight the automatic indentation in your IDE! Pycharm is intelligent enough to know when to indent so if it does not indent by itself, you probably made a syntax error.

Class string methods

Let's take the logic above and implement it in a real program. I would like the program to ask the user for his/her name and age. After both questions are asked I would like the program to show a personalized message to the user. The code below is functional but requires quite a bit of explaining!

name = input("What is you name?")
age = input("What is you age?")

if age.isdigit():
    age = int(age)
else:
    print("{} is not a valid age".format(age))
    exit(1)

if age < 27:
    print("My god {}, you are still very young, enjoy it!".format(name))
elif age > 27:
    print("Wow {}! Watch out for those hips oldie...".format(name))
else:
    print("{}, you are at a dangerous crossroad in life!".format(name.capitalize()))

An object of the type str has multiple methods that can be applied on itself. It might not have been obvious but the string formatting via "hello {}".format("world") we did above is exactly this. We call the .format method on a str object. Strings have a handful of methods we can call. Pycharm lists these methods when you type . after a string object. In the shell we can visualize this as well. For those of you not familiar with shells have a look at tab completion.

>>> name = "wouter"
>>> name.
name.capitalize(    name.format(        name.isidentifier(  name.ljust(         name.rfind(         name.startswith(
name.casefold(      name.format_map(    name.islower(       name.lower(         name.rindex(        name.strip(
name.center(        name.index(         name.isnumeric(     name.lstrip(        name.rjust(         name.swapcase(
name.count(         name.isalnum(       name.isprintable(   name.maketrans(     name.rpartition(    name.title(
name.encode(        name.isalpha(       name.isspace(       name.partition(     name.rsplit(        name.translate(
name.endswith(      name.isascii(       name.istitle(       name.removeprefix(  name.rstrip(        name.upper(
name.expandtabs(    name.isdecimal(     name.isupper(       name.removesuffix(  name.split(         name.zfill(
name.find(          name.isdigit(       name.join(          name.replace(       name.splitlines(    
>>> name.capitalize()
'Wouter'
>>> name.isd
name.isdecimal(  name.isdigit(    
>>> name.isdigit()
False
>>> age = "35"
>>> age.isdigit()
True
>>> 

Remember CTRL-q opens the documentation in Pycharm and don't forget to actually use it!

🏃 Try it

Can you code ma little calculator now? What about one that takes floating point numbers?

Coding challenge - Celsius to Fahrenheit converter

Your first challenge! I would like you to write a program that converts Celsius to Fahrenheit. You should do this in a new python file. I suggest you call it c_to_f.py or something that makes sense to you. The result of this program could be as follows.

➜  ~ git:(master) ✗ python3 ex_celcius_to_fahrenheit.py
What's the temperature?30
30°C equals 86.0°F
Go turn off the heating!
➜  ~ git:(master) ✗ python3 ex_celcius_to_fahrenheit.py
What's the temperature?4
4°C equals 39.2°F
Brrrr, that's cold!
➜  ~ git:(master) ✗ python3 ex_celcius_to_fahrenheit.py
What's the temperature?blabla
I can't understand you...
➜  ~ git:(master) ✗ 

If you want to make the program a bit more complex, try adding the reverse as in Fahrenheit to Celsius. Your first question to the user could then be in which direction do you want to convert?. If you want to add an other feature, I suggest you try to make the converter work for int and for float objects.

Spoiler warning
result = input("What's the temperature?")

if result.isdigit():
    celsius = int(result)
else:
    print("I can't understand you...")
    exit(1)

farenheit = celsius * (9/5) + 32

print("{}°C equals {}°F".format(celsius, farenheit))

if celsius < 15:
    print("Brrrr, that's cold!")
else:
    print("Go turn off the heating!")

Coding challenge - Currency converter

I would like you to write a program that converts EUR to DOLLAR. You should do this in a new python file. I suggest you call it euro_to_dollar.py or something that makes sense to you. The result of this program could be as follows.

➜  python_course_doc git:(master) ✗ python3 test.py                  
How much EUR would you like to convert into DOLLAR? 140
140 EUR is 159.6 DOLLAR
➜  python_course_doc git:(master) ✗ python3 test.py
How much EUR would you like to convert into DOLLAR? blablabla
That's not a number I understand...
➜  python_course_doc git:(master)
Spoiler warning
result = input("How much EUR would you like to convert into DOLLAR? ")

rate = 1.14

if result.isdigit():
    eur = int(result)
    dollar = eur * rate
    print("{} EUR is {} DOLLAR".format(eur, dollar))
else:
    print("That's not a number I understand...")
  • how to convert string to float, Look at the response here: str.isfloat()?

A text based adventure game

We can use conditional logic to create quite elaborate decision processes. Let's build a mini text based adventure game. Granted it's not a triple A game but it will train your if and else skills plus it will highlight some issues we'll overcome in the next section.

adventure game

Consider the diagram above we can imagine a program that functions nicely with the code below. It is however not very readable nor scalable.

answer = input("You're at a cross section. Do you go left or right?")
if answer.startswith("l"):
    answer = input("Down this hall you encounter a bear. Do you fight it?")
    if answer.startswith("y"):
        print("The bear counter attack! He kills you")
        print("game over!")
        exit(0)
    elif answer.startswith("n"):
        print("It's a friendly bear! He transforms into a wizard!")
        answer = input("The wizard asks you if you know the meaning of life?")
        if answer == "42":
            print("He knods approuvingly and upgrades you to wizard status!")
            print("You win!")
            exit(0)
        else:
            print("He shakes his head in disbelief. You fool!")
            print("game over!")
    else:
        print("that's not a valid choice...")
        print("game over!")
        exit(0)
elif answer.startswith("r"):
    answer = input("Down this hall you find some mushrooms. Do you eat them?")
    if answer.startswith("n"):
        print("You starve to dead...")
        print("game over!")
        exit(0)
    elif answer.startswith("y"):
        print("A wizard apprears out of thin air!")
        answer = input("The wizard asks you if you know the meaning of life?")
        if answer == "42":
            print("He knods approuvingly and upgrades you to wizard status!")
            print("You win!")
            exit(0)
        else:
            print("He shakes his head in disbelief. You fool!")
            print("game over!")
    else:
        print("that's not a valid choice...")
        print("game over!")
        exit(0)
    pass
else:
    print("game over!")
    exit(0)

I urge you to read up on some best practices for if statements. We will not improve on this particular example but I do advise you to create a similar style game during one of the workshops once we have learned some new tricks. If you need some inspiration you could have a look here.

Creating your own functions

One of the issues we have in the text based game example is duplicate code. At two spots we execute almost identical code and this is something that should be avoided at all costs! Why write the same thing over and over? Each time you write it again you introduce a new window for errors! You're better off writing it once and use it lots. This is where functions come into play.

Python ships with a lot of built-in functions but we can create our own very easily. The keyword to define a function in python is def. All the code that is indented will be executed when we call the function. Here is a basic abstraction with a correct syntax.

def first_function():
    print("I'm a function")
    print("Hear me roar!")

Do not fight the indentation, it's part of the syntax of python!

If you type only the code above in a new script, and run it, you won't see much. This is because you only created the function. To use it you need to call it. This is done as follows.

def first_function():
    print("I'm a function")
    print("Hear me roar!")

first_function()

Learning how to create functions is a big step in your programming journey. It can seem confusing at first because the code execution appears to jump around. This is however not the case. Your script is still read and executed line by line so you can not call a function before you defined it! For now you should not overthink the structure of you scripts. As long as they work you should be happy. We'll dive into the proper anatomy of a program real soon.

Remember you can't use functions before defining them! Learn to read the error messages. Also, if it does not autocomplete, it probably won't work...

Functions that do something

The first function I showed you above performs a series of actions each time it is called. We can use it to bake cakes for example. Below we create one function to bake the cake, and call it three times to bake three cakes.

def bake_chocolate_cake():
    print("mix the base ingredients")
    print("add the chocolate flavour")
    print("put in the oven")
    print("enjoy!")

bake_chocolate_cake()
bake_chocolate_cake()
bake_chocolate_cake()

Now, you might like a vanilla cake from time to time. Easy, we'll just write a second function for that purpose.

def bake_chocolate_cake():
    print("mix the base ingredients")
    print("add the chocolate flavour")
    print("put in the oven")
    print("enjoy!")

def bake_vanilla_cake():
    print("mix the base ingredients")
    print("add the vanilla flavour")
    print("put in the oven")
    print("enjoy!")

bake_chocolate_cake()
bake_chocolate_cake()
bake_vanilla_cake()
bake_chocolate_cake()
bake_vanilla_cake()

Voila, we can now make as many chocolate and vanilla cakes as we want! But what about bananas? Following our logic we can create a third function to bake a banana cake but you're probably seeing a pattern here. Each bake_FLAVOR_cake function is almost identical, just the flavor changes. We can create one generic bake_cake function and add the custom flavor each time we actually bake a cake. This can be done with arguments.

def bake_cake(flavor):
    print("mix the base ingredients")
    print("add the {} flavor".format(flavor))
    print("put in the oven")
    print("enjoy!")

bake_cake("chocolate")
bake_cake("vanilla")
bake_cake("banana")

Variable scope

Variable scope might sounds complicated but with some examples you'll understand it in no time. Remember variables are like unique post-its? Well, variable scope is like having multiple colors for your post-its. A yellow post-it with name on it is not the same as a red one with name on it so they can both reference different objects. Scope is where the colors change, and this is done automatically for you in python. It's an inherent feature of the language but the concept of scope is not unique to python, you'll find it in most modern languages. Now, some examples.

total = 9000

def function_scope(argument_one, argument_two):
    print("inside the function", argument_one, argument_two)
    total = argument_one + argument_two
    print("inside the function", total)


function_scope(300, 400)
print("outside the function", total)

Here, there is a variable total outside the function references.But, it is a different object from the total inside the function. Python is very nice and will try to fix some common mistakes or oversights by itself. For example.

name = "Wouter"

def function_scope():
    print("inside the function", name)


function_scope()
print("outside the function", name)

But we can not modify the referenced object from inside the function. This will give an UnboundLocalError: local variable 'name' referenced before assignment error

name = "Wouter"

def function_scope():
    print("inside the function", name)
    name = "Alice"


function_scope()
print("outside the function", name)

There is however a handy keyword we can use to explicitly reference variables from the outermost scope. This is done as follows with the global keyword.

name = "Wouter"

def function_scope():
    global name
    print("inside the function", name)
    name = "Alice"


function_scope()
print("outside the function", name)

Functions that return something

While the global keyword can be useful, it's rarely used. This is because function can not only do thing, they can return objects. The input function we've been using is a prime example of this as it always give you a str object back with the content of the user input. We can copy this behaviour as follows.

def square_surface(length, width):
    surface = length * width
    return surface


square = square_surface(20, 40)
print(square)

🏃 Try it

Functions that return an object are essential to any modern programming language. Think of some calculations such as the Celsius to Fahrenheit converter and create the corresponding functions.

Coding challenge - Pretty Print

Can you write me a function that decorates a name or message with a pretty character. You should do this in a new python file. I suggest you call it pretty_frame.py or something that makes sense to you. Kind of like the two examples below.

##########
# Wouter #
##########

#################
# Python rules! #
#################

As an extra challenge you could return a multi line string and print it outside of the function!

Spoiler warning
def pretty_print(msg, decorator="#"):
    line_len = len(msg) + (len(decorator) * 2) + 2
    print(decorator * line_len)
    print("{} {} {}".format(decorator, msg, decorator))
    print(decorator * line_len)

pretty_print("Wouter")
pretty_print("Python rules!")
pretty_print("Alice", "-")

Using the standard library

There is no need to reinvent the wheel each time you build a bike. The same goes for programming. Most, if not all, programming languages come with a standard library which is a collection of additional objects and functions to facilitate common problems. We'll look at some essential ones together but I urge you to read up a bit when you have some free time. Over the course of you programming journey you'll discover that efficient programming is often finding the right libraries and chaining them together to suit your use case.

Imagine we want to include a dice in our text based adventure game. How on earth do we program that? We need some form of randomness in our code. A quick google demonstrates this is quite difficult without the use of libraries. As randomness is both extensively used in programming and it's quite difficult to do properly, you'll find a random library in most languages.

The new keyword you'll learn here is import. It allows you to include extra functionality to your program. (Go look back at the c and c++ helloworld code, what do you think the #include does?) Once imported we can call functions that exist from the library in question. So, our dice becomes as follows.

import random

throw = random.randint(1, 6)

print(throw)

Autocomplete is your friend. You can use it to browse around a library and discover interesting functions or classes you can make use of.

A second widely used library is datetime. It facilitates handling dates, hours, calendars, time differences, etc. A simple program we can write to illustrate it's purpose is a will-it-be-Sunday program. You give date and the program tells you if it's a Sunday or not.

import datetime

sunday = 6

date = input("which date should I look up? (example: 2022 3 23) ")
year, month, day = date.split()

date = datetime.date(int(year), int(month), int(day))

if date.weekday() == sunday:
    print("yes! you can sleep in...")
else:
    print("better set your alarm")

The program above incorporates a lot of different concepts. Read it very slowly and think about what each step is doing. Also think about how you can break this program!

Why on earth is Sunday six? Read the doc!

Coding challenge - Memento Mori calculator

Memento Mori is a bit of a grim concept and if it freaks you out, maybe adjust the exercise to calculate the days until your next birthday. That being said, we all die and we all live in a country with a life expectancy. The Belgian life expectancy for a man is about 80 years so if you know your birthday, which you should, you can calculate your theoretical death day. Plus, you can calculate the percentage of your life that remains. You should do this in a new python file. I suggest you call it memento_mori.py or something that makes sense to you.

Spoiler warning
import datetime

life_expectancy_years = 80.8
life_expectancy_days = life_expectancy_years * 365
delta = datetime.timedelta(days=life_expectancy_days)

date = input("what is your birthday? (example: 1986 10 7) ")
year, month, day = date.split()

birthday = datetime.date(int(year), int(month), int(day))
memento_mori = birthday + delta

days_to_live = memento_mori - datetime.datetime.now().date()
percentage = (days_to_live.days / life_expectancy_days) * 100

print("your theoretical death day is:", memento_mori)
print("that's", days_to_live.days, "days left to live")
print("which means you have {:.2f}% left to live...".format(percentage))

There are quite a few quirky tricks in the code above. Take your time to ask me about them, or use online documentation and keep some notes.

Writing your first library

Up until now have coded our own functions and usage of these functions. We've seen there is no need to always write everything from scratch as we can reuse existing code by importing libraries and calling it's functions. But what are those libraries actually?

Consider the following mini script. It imports datetime and calls the now function to print the date and time. A pretty basic clock.

import datetime


timestamp = datetime.datetime.now()
print("It is: {}".format(timestamp))

When you put your cursor on the now part of the function call, you can press CTRL-b to go to the declaration of said function. You can also right click on the now part and see all possible actions you can perform on this function. This opens up a second tab in your editor window with a datetime.py file in it. In this file your cursor should have jumped to the following code.

    @classmethod
    def now(cls, tz=None):
        "Construct a datetime from time.time() and optional time zone info."
        t = _time.time()
        return cls.fromtimestamp(t, tz)

What does this look like? It looks like python code no? Well, it is! Libraries are often just other python files we load into our program. So if they are just python files we should be able to write our own libraries no?

🏃 Try it

Import some libraries and go peak at some function declarations. A big part of code writing is navigating a codebase written by other people. Properly understanding how to navigate is essential!

How do we write libraries?

Let's go back to our pretty_print function. I have code along these lines.

def pretty_print(msg, decorator="#"):
    line_len = len(msg) + (len(decorator) * 2) + 2
    print(decorator * line_len)
    print("{} {} {}".format(decorator, msg, decorator))
    print(decorator * line_len)


pretty_print("Wouter")
pretty_print("Python rules!")
pretty_print("Alice", "-")

I'll create a new python file and name it helper_functions.py. In this file I rewrite (don't copy paste, you need the practice) my function. As I'm rewriting my function I'll also need some test calls to my function. Done!

Now in a second new python file I'll name my_program.py I'll import the helper_fucntions.py file. Note the syntax drops the .py extension!

import helper_functions


print("I'm a program")
helper_functions.pretty_print("hello world!")

If we now run the my_program.py file we get the following output.

##########
# Wouter #
##########
#################
# Python rules! #
#################
---------
- Alice -
---------
I'm a program
################
# hello world! #
################

OK, it kind of works, the function get's called from within the my_program.py file but the test calls from the helper_functions.py file are messing up my execution. Luckily there is a way to tell python to only run certain code when a file should be seen as program and not a library. Sounds complicated but it's logically very simple.

What is __name__ == "__main__"?

Remember on the first day when I showed you how python is self documenting via print.__doc__? Well, there are more magical attributes than just the __doc__ one! There is one called __name__ which is used for the exact purpose we're trying to achieve. Go back to the helper_functions.py file and comment out the test calls and add the line that print's the __name__ variable.

def pretty_print(msg, decorator="#"):
    line_len = len(msg) + (len(decorator) * 2) + 2
    print(decorator * line_len)
    print("{} {} {}".format(decorator, msg, decorator))
    print(decorator * line_len)


print("I'm a library")
print("my name is: {}".format(__name__))
# pretty_print("Wouter")
# pretty_print("Python rules!")
# pretty_print("Alice", "-")

Do the same in the my_program.py file so it look similar to the code below.

import helper_functions


print("I'm a program")
print("my name is: {}".format(__name__))
helper_functions.pretty_print("hello world!")

If you now run the helper_functions.py file you should get output similar to this.

I'm a library
my name is: __main__

But when you run the my_program.py you should get something like this.

I'm a library
my name is: helper_functions
I'm a program
my name is: __main__
################
# hello world! #
################

This shows that the __name__ variable changes according to how a script is called and this behavior can be used to our advantage! We can evaluate the value behind __name__ and change execution accordingly. Evaluating and changing execution screams conditional logic no? So in the library we add the following.

def pretty_print(msg, decorator="#"):
    line_len = len(msg) + (len(decorator) * 2) + 2
    print(decorator * line_len)
    print("{} {} {}".format(decorator, msg, decorator))
    print(decorator * line_len)


if __name__ == "__main__":
    pretty_print("Wouter")
    pretty_print("Python rules!")
    pretty_print("Alice", "-")

Only when the library is executed as a program, for example when we're testing out out functions, the condition is met to allow execution of our calls. Problem sorted! Executing our main program now gives the expected output.

I'm a program
my name is: __main__
################
# hello world! #
################

Anatomy of a program

While it's still very early in your coding career I really want to insist on good practices from the start as it will help you make sense of it all. A well written script or program is divided into three sections.

  1. We collect external tools from libraries, either from the standard library or of our own making.
  2. We write the specific tools we need for our program to run.
  3. We call a combination of external and specific tools which constitutes our program logic.

A mock-up program that follows these rules could look like this. Notice how it's easy to read? The pass is a new keyword that does nothing but is often used to declare functions a placeholders.

import random
import datetime


def first_function():
    pass


def second_function():
    pass


def third_function():
    pass


def fourth_function():
    pass


if __name__ == "__main__":
    print("today it's: {}".format(datetime.datetime.now()))
    choice = random.randint(0, 100)
    if choice < 50:
        first_function()
    elif choice > 90:
        second_function()
    elif choice == 55:
        third_function()
    else:
        fourth_function()

🏃 Try it

Go online and look for some scripts and programs that interest you. See if you can assess whether the code follows the pattern I outlined. There are a couple of things you should definitely read up on.

While loop

We started our python journey with fully linear code. Next we saw functions which are first defined and called afterwards. Now we'll have a look at loops. In python there are two types of loops, a while and a for loop. We'll start with the while loop which I see as a loop in time. The for loop is a loop in space but we'll get to that one later.

The concept of a while loop is pretty simple. Code within the loop will be executed as long as a condition is met. Consider the code below.

import time

counter = 0
print("before the loop, counter: {}".format(counter))

while counter <= 10:
    print("inside the loop, counter: {}".format(counter))
    counter += 1
    time.sleep(1)

print("after the loop, counter: {}".format(counter))

Or with only variables and a more verbose way of incrementing the numbers.

first_value = 20
second_value = 0
while first_value > second_value:
    print("second value {} is smaller than {}".format(second_value, first_value))
    second_value = second_value + 1

Two extra things might look new to you here. First the import time and time.sleep(1), can you tell me what it does? Next the counter += 1 which is called incrementing. You'll find this feature in most languages. You can think of it's syntax as counter equals itself plus 1. The 1 can be any number you want though!

When learning the while keyword there is a second keyword you should learn. It comes in very handy when constructing infinite loops. Consider the following code.

import time

counter = 0
print("before the loop, counter: {}".format(counter))

while True:
    print("inside the loop, counter: {}".format(counter))
    counter += 1
    time.sleep(1)

print("after the loop, counter: {}".format(counter))

The while True condition is always True so the loop will never exit! This is what we call an infinite loop. The break keyword was added to the language so we can break out of a loop. The logic is as follows.

import time

counter = 0
print("before the loop, counter: {}".format(counter))

while True:
    print("inside the loop, counter: {}".format(counter))
    counter += 1
    if counter >= 10:
        print("I'll break now!")
        break
    time.sleep(1)

print("after the loop, counter: {}".format(counter))

Infinite loops are a cornerstone of modern programming. While they might look scary, don't overthink it, you'll get used to them very quickly.

Logical procedures can be drawn out with flow charts. An example can be seen in the image below.

flowchart while loop

When testing out an infinite loop it's sometimes handy to insert a time.sleep in it to slow down the execution a bit so you can wrap your head around what's happening.

🏃 Try it

Go back to the Celsius to Fahrenheit converter and add a while loop to ensure the user puts in only numbers.

Coding challenge - Guess the number

Now that you know how to repeat code execution we can create our first game! Everybody knows the guess the number game. The computer chooses a random number and the user has to guess which number it is. At each try the computer will till you if the user's number is bigger or smaller than the one the computer has in mind. The flow of the game could be as follows.

I have a number in mind...
What's your guess? 50
my number is bigger
What's your guess? 80
my number is smaller
What's your guess? blabla
that's not a number! try again...
What's your guess? 76
yes, that's right! you win!
bye bye...
Spoiler warning
import random


def ask_for_number():
    result = input("What's your guess? ")
    if result.isdigit():
        number = int(result)
        return number
    else:
        return None


if __name__ == "__main__":
    number_to_guess = random.randint(0, 100)
    print("I have a number in mind...")
    while True:
        user_number = ask_for_number()
        if user_number is None:
            print("that's not a number! try again...")
            continue
        elif number_to_guess == user_number:
            print("yes, that's right! you win!")
            break
        elif number_to_guess > user_number:
            print("my number is bigger")
        elif number_to_guess < user_number:
            print("my number is smaller")
    print("bye bye...")

🏃 Try it

My solution is very basic. Think of some ways to improve on it. Can you limit the number of tries? Can you add a feature to let the user play a second game after he/she wins or loses? Coming up with challenges is on of the most challenging aspect op learning how to program. Your thought process will send you of into unknown territory and will force you to expand you knowledge. We'll get back to this thought process later, but if you feel like an extra challenge go for it!

Below you can see the same game but broken down into functions.

Spoiler warning
import random


def ask_for_number():
    while True:
        human_number = input("what is your guess? ")
        if human_number.isdigit():
            human_number = int(human_number)
            break
        else:
            print("that is not a number! please try again...")
    return human_number


def are_the_numbers_equal(computer_number, human_number):
    if human_number < computer_number:
        print("my number is bigger")
        return False
    elif human_number > computer_number:
        print("my number is smaller")
        return False
    elif human_number == computer_number:
        print("yes! {} is the number I had in mind".format(computer_number))
        return True


def play_a_game():
    computer_number = random.randint(0, 100)
    print("cheat mode: {}".format(computer_number))
    print("I have a number in mind...")
    while True:
        human_number = ask_for_number()
        status = are_the_numbers_equal(computer_number, human_number)
        if status == True:
            break


def main():
    while True:
        play_a_game()
        play_again = input("do you want to play a new game? (Y/N)")
        if play_again.startswith("N"):
            print("bye bye!")
            break


if __name__ == "__main__":
    main()

#Logical Operators

There is three types of logical operators. All operators returns boolean.

Operator Result
And It send True if all conditions are true
Or It send True if one of conditions are true
Not It reverse the boolean result

Let's start example with And operator !

CustomerName = "Jean"
CustomerAgreement = True

DealerName = "Paul"
DealerAgreement = True

if CustomerAgreement and DealerAgreement :
    print(f"Youpi !!! {CustomerName} and {DealerName} are agreed ")
else:
    print(f"Oh no {CustomerName} and {DealerName} are disagreed ;( ")

As you can guess, Jean and Paul are agreeing to the deal. If I had put 'False' in DealerAgreement boolean, the result will be inverse.

Let's show an another example with the Or operator.

def Choice_cold_hot(Temperature):
    if Temperature <= 20 or Temperature >= 40: 
        print("Don't go outside")
    else:
        print("Let's go to the beach")

if __name__ == "__main__":
    Temperature = int(input("What is the temperature"))
    Choice_cold_hot(Temperature)

Look at this code, if the temperature is smaller than 20° or bigger than 40°, you must stay home and don't go to the beach. So, if I put 35° for temperature, it will say that you should go to the beach.

Let's make an exercise. You have to take the previous code and use the And operator.

Spoiler warning
def Choice_cold_hot(Temperature):
    if Temperature >= 20 and Temperature <= 40:
        print("Let's go to the beach")
    else:
        print("Don't go outside")
if __name__ == "__main__":
    Temperature = int(input("What is the temperature"))
    Choice_cold_hot(Temperature)

Now, we have used that operators, we can use the last logical operator. The Not operator sends the reverse of the result.

if __name__ == "__main__":
    Gas = input("Do you want some gas")
    if not Gas.startswith("y"):
        print("Ok, no problem")
    else:
        print("That will be expensive")

In this example, if you tap yes, the result will be reversed.

Lists

The different built-in objects we've seen until now, such as str and int are simple text and numeric types. There are other classes of objects that server different purposes. One of these groups is called sequence types.

A list in python is pretty much exactly what you think it is. It is an object that groups together other objects. Sounds complicated? Have a look at the following.

my_numbers = [1, 2, 44, 60, 70]
print(my_numbers)

Easy right? Compared to other languages lists in python are very flexible. They can contain objects of different types, and their length can be changed at any time. Programmers coming from other languages often find this flexibility of python a bug but you should see it as a feature.

favorite_number = 7
name = "wouter"
date = [1986, 10, 7]
values = [1, date, favorite_number, "hello world", name]

print(values)

The code above is just an illustration of the flexibility of lists.

Creating lists

Creating lists can be done in two ways, either by using the square brackets [] or by calling list. When calling list it takes one argument, which python will iterate over. For example:

>>> first_list = ["hello", "world", "!"]
>>> first_list
['hello', 'world', '!']
>>> second_list = list("hello world !")
>>> second_list
['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', ' ', '!']
>>> 

List methods

As a list is a different type of object, it has different methods you can invoke on it. When using tab complete in the python shell we get the following.

>>> second_list.
second_list.append(   second_list.count(    second_list.insert(   second_list.reverse(  
second_list.clear(    second_list.extend(   second_list.pop(      second_list.sort(     
second_list.copy(     second_list.index(    second_list.remove(   
>>> second_list.

One of the most used methods is append. It is used to add an element to the end of the list. The second most used method is the pop one. Read the shell code below and you'll understand immediately what they do.

>>> first_list
['hello', 'world', '!']
>>> first_list.append("coucou")
>>> first_list
['hello', 'world', '!', 'coucou']
>>> first_list.pop()
'coucou'
>>> first_list
['hello', 'world', '!']
>>> 

🏃 Try it

Look at all the methods you can invoke on a list and try them out. Remember to read the documentation!

Picking elements and slicing lists

We can pick elements from the list of slice the list as we please. A code block speaks more than words.

>>> long_list
['I', 'am', 'a', 'very', 'long', 'list', 'of', 'words', 'that', 'make', 'little', 'actual', 'sense']
>>> long_list[7]
'words'
>>> long_list[7:9]
['words', 'that']
>>> long_list[7:]
['words', 'that', 'make', 'little', 'actual', 'sense']
>>> long_list[:7]
['I', 'am', 'a', 'very', 'long', 'list', 'of']
>>> long_list[-1]
'sense'
>>> long_list[0]
'I'
>>> 

In programming we start counting at 0 because 0 and nothing are not the same thing.

🏃 Try it

Slice and dice away! A handy method of the str class is split which will cut up a string into separate elements. The result will be a list on which you can use list methods.

For loop

I mentioned the for loop, which is a loop in space, when we saw the while loop. The keyword in question is, surprise surprise, for! I see it as a loop in space because it will run for each element in a sequence which in my mind is something of substance. Your logical mileage may vary but as long as you understand the following code block we're good.

import time

friends = ["max", "mike", "alice", "steve", "rosa", "hans"]

print("The door opens and in walk my {} friends!".format(len(friends)))

for friend in friends:
    print("Hello {}!".format(friend.capitalize()))
    time.sleep(1)

print("{} closes the door behind him...".format(friend.capitalize()))

TODO pizza function with multiple arguments

Simpler example
for i in range(5):
    print(i)

Coding challenge - Cheerleader chant

Can you make me a program that outputs this type of cheerleader chant? You can make it prettier by importing your pretty_print function, plus you can add some time.sleep in it to make it more musical.

Give me an m
M
Give me an a
A
Give me an x
X
Gooooooooo, MAX!
Give me an m
M
Give me an i
I
Give me an k
K
Give me an e
E
Gooooooooo, MIKE!
Give me an c
C
Give me an a
A
Give me an m
M
Give me an i
I
Give me an l
L
Give me an l
L
Give me an e
E
Gooooooooo, CAMILLE!
Spoiler warning
friends = ["max", "mike", "camille"]


for friend in friends:
    for letter in friend:
        print("Give me an {}".format(letter))
        print("{}".format(letter.upper()))
    print("Gooooooooo, {}!".format(friend.upper()))

Coding challenge - ROT13

ROT13 is one of the oldest cryptographic cyphers know to mankind. It dates back to the Roman empire and is also known as a Caesar cypher. The algorithm is pretty simple, you just shift a letter 13 places in the alphabet so a becomes n or x becomes k. Have a look at this website to see the cypher in action. Now, can you make a program that encrypts a phrase with ROT13? Something along these lines:

What's your secret? hello world!
encoded secret: uryyb jbeyq!
Spoiler warning
import string


def encode_rot(msg, rot=13):
    msg = msg.lower()
    letters = list(string.ascii_lowercase)
    coded_msg = []
    for letter in msg:
        if letter not in letters:
            coded_msg.append(letter)
        else:
            idx = letters.index(letter) + rot
            coded_letter = letters[idx % len(letters)]
            coded_msg.append(coded_letter)
    coded_msg = "".join(coded_msg)
    return coded_msg
        

def decode_rot(msg, rot=13):
    pass


if __name__ == "__main__":
    clear_message = input("What's your secret? ")
    encoded_message = encode_rot(clear_message)
    print("encoded secret: {}".format(encoded_message))

🏃 Try it

To make things more interesting you can add a decode function. Plus you could add a prompt that asks how big the shift should be (ROT13, ROT16, ...). You can also make the cypher a lot harder to break if you use a word (or phrase as a key). For example, if the key is abc and the message is hello world the first letter will be offset by a ROT0 (will remain h). The second letter, e will be offset by a ROT1 so will become f. The third letter by ROT2 so will become n. Now, the fourth letter will be offset again by a so ROT0 and will become l. And so on...

Spoiler warning
import string


def encrypt_message(msg, rot=13):
    encoded_message = []
    for letter in msg:
        if letter in alphabet:
            letter_index = alphabet.index(letter)
            encoded_letter_index = (letter_index + rot) % 26
            encoded_letter = alphabet[encoded_letter_index]
            encoded_message.append(encoded_letter)
        else:
            encoded_message.append(letter)
    encoded_message = "".join(encoded_message)
    return encoded_message


def encrypt_with_key(msg, key):
    encrypted_message = []
    counter = 0
    for letter in msg:
        key_letter = key[counter % len(key)]
        key_index = alphabet.index(key_letter)
        encrypted_letter = encrypt_message(letter, key_index)
        counter += 1
        encrypted_message.append(encrypted_letter)
    return "".join(encrypted_message)


if __name__ == "__main__":
    alphabet = list(string.ascii_lowercase)
    key = "mehdi"
    print("hello, I'm an encryption program!")
    result = input("what is your secret? ")
    secret = encrypt_with_key(result, key)
    print("your secret message is: {}".format(secret)) 

Coding challenge - Christmas Tree

Can you make me a program that draws various size Christmas trees? Like the code block below.

         #
        ###
       #####
      #######
     #########
    ###########
   #############
  ###############
 #################
        ###
        ###
     
    #
   ###
  #####
 #######
   ###
   ###

Can you add in some balls as below?

         #
        ##°
       #°###
      #°###°#
     ###°#####
    °##°##°#°##
   #°###°°°#####
  #°############°
 #####°###°#°####°
        ###
        ###

What about this super funky gif...

tree

Spoiler warning

Nice try... You should be able to do this on your own!

List comprehension

This is a bit of an advanced topic but I'm putting it here to show you a very unique and powerful feature of python. It's an in-line combination of list, for and conditional logic which allows us to make lists of lists which obey certain conditions. You can learn about it online.

mixed_list = ["one", "2", "three", "4", "5", "six6", "se7en", "8"]

digits = [d for d in mixed_list if d.isdigit()]

print(digits)

The code above can be recreated without list comprehension as well, it will just be a lot longer.

mixed_list = ["one", "2", "three", "4", "5", "six6", "se7en", "8"]

digits = []
for d in mixed_list:
    if d.isdigit():
        digits.append(d)

print(digits)

As the d in the list comprehension is always a digit, we can convert it to an integer on the spot!

mixed_list = ["one", "2", "three", "4", "5", "six6", "se7en", "8"]

digits = [int(d) for d in mixed_list if d.isdigit()]

print(digits)

Handling files

When we import a library python will read and execute the file in question. We can also just read a simple text file and use the data that's in the file. There are two ways of reading a file, one more pythonic and one more linear. I'll outline both and you can use whichever seems more logical to you.

Reading from a file

In-line way

The builtin function open is what's new here. It takes two arguments, one is the path and the other is a mode. The most used modes are read or write, and this as either text or binary. Have a look at the documentation to discover more modes.

fp = open("./examples/data.txt", "r")
data = fp.readlines()
print("file contains: {}".format(data))
fp.close()

Pythonic way

The exact same thing can be done in a more pythonic way as follows. The beauty of the with syntax is that the close function call is implied by the indentation. I personally prefer this way of reading and writing files but you do you!

file_to_open = "./examples/data.txt"

with open(file_to_open, "r") as fp:
    data = fp.readlines()
    print("file contains: {}".format(data))

print("{} has {} lines".format(file_to_open, len(data)))

Writing to a file

Writing to a file can also be done in two ways, a pythonic and less pythonic way. As I prefer the pythonic way I'll only showcase that one but you'll be able to easily adapt the code yourself. The only difference is the mode we use to open the file.

first_name = input("what's your first name? ")
last_name = input("what's your last name? ")
birthday = input("what's your date of birth? ")

data = [first_name, last_name, birthday]

file_to_open = "./examples/test.tmp"

with open(file_to_open, "w") as fp:
    for element in data:
        fp.write("{}\n".format(element))
print("done!")

There is also a way to write a batch of lines to a file in one go. This is done as follows. But, you'll notice there is no newline added after each element.

data = ["wouter", "gordts", "1986"]

file_to_open = "./examples/test.tmp"

with open(file_to_open, "w") as fp:
    fp.writelines(data)

Coding challenge - Login generator

Can you write me a program that creates random, but easy to remember, usernames? Plus, for each username also a not-so-easy-to-remember password? Maybe with a bit of flexibility? For example variable password length?

This is an exercise you can take pretty far if you plan it out properly. I would advise you to write the generator functions as a library. Then import those functions into the program where you implement the user facing logic. By doing so you can reuse your code for multiple interfaces (CLI, TUI, argparse). If you want to you can also save your logins to a file!

An example of the output I expect:

how many login pairs would you like to create?3
you want complex passwords? (y/n)y
how long should the password be?32
username 0: EarnestCrocodile
password 0: :sdGV&[FDYZZ|RXUpZeo`J&t@*Z>^fEW
username 1: AbstractedDragon
password 1: 32hz5&C@<o\OMa9tnET(lk(3wF%d?$Dy
username 2: MeekStallion
password 2: +;^di8a":AD;_b4^w$Fj'RVkI`CoG,LX
Spoiler warning

This spoiler warning is in multiple steps. If you're unsure how to generate the random-yet-easy-to-remember have a look at this file and this file.

Stop here and try it out!

If you're unsure how to tackle the library part of this exercise, have a look at this file. Don't just copy this code, read it and recreate it yourself!

Stop here and try it out!

Once the library is done you can code the interface. For some inspiration for a simple question/response system you can have a look here.

Dictionaries as data containers

Two dimensional lists

TODO - Currency converter with lists.

Dictionaries

Of the built-in types we first say str, int and float. Next we saw sequence types such as list and tupple. Now we'll dive into a mapping type called dict. I advise you to have a look at the reference pages when in doubt.

A dictionary is kind of like a list but it has two objects per element. We call them key and value. There are a couple of rules you need to be aware of though.

  1. In a dict the keys have to be unique.
  2. A dictionary is unordered meaning the first element is not garanteed to remain the first over the lifespan of the dictionary.
  3. The keys used must be hashable.

Let's visualize a legal dictionary! It's declared with curly brackets as follows {}.

my_data = {"key": "value", "name": "wouter", "age": 35}

same_data_different_layout = {
        "key": "value",
        "name": "wouter",
        "age": 35,
        }

Let's have a look at the methods we can invoke on a dict.

>>> my_data = {"key": "value", "name": "wouter", "age": 35,}
>>> my_data.
my_data.clear(       my_data.get(         my_data.pop(         my_data.update(      
my_data.copy(        my_data.items(       my_data.popitem(     my_data.values(      
my_data.fromkeys(    my_data.keys(        my_data.setdefault(  
>>> my_data.keys()
dict_keys(['key', 'name', 'age'])
>>> my_data.values()
dict_values(['value', 'wouter', 35])
>>> my_data.items()
dict_items([('key', 'value'), ('name', 'wouter'), ('age', 35)])
>>> for key, value in my_data.items():
...     print("key is {}".format(key))
...     print("value is {}".format(value))
... 
key is key
value is value
key is name
value is wouter
key is age
value is 35
>>> 

We can reference specific values corresponding to specific keys as follows.

>>> my_data["name"]
'wouter'
>>> my_data["age"]
35
>>> 

We can use dictionaries as data containers and put them in a list to group together similar data elements. The code below should explain it quite nicely.

login_ovh = {"username": "EarnestCrocodile", "password": ":sdGV&[FDYZZ|RXUpZeo`J&t@*Z>^fEW"}
login_mailbox = {"username": "AbstractedDragon", "password": "32hz5&C@<o\OMa9tnET(lk(3wF%d?$Dy"}
login_gitea = {"username": "MeekStallion", "password": "+;^di8af:AD;_b4^w$Fj'RVkI`CoG,LX"}

my_login_list = [login_ovh, login_mailbox, login_gitea]

for login in my_login_list:
    print("login {}".format(my_login_list.index(login)))
    for key in login.keys():
        print("\t{}: {}".format(key, login[key]))

🏃 Try it

Go back to you currency converter program and add a dict with multiple currencies so you can enter one amount to convert and your program gives you how much that is in each currency of the dict.

Coding challenge - Task manager

CSV based task manager

import csv

FILE = "mytasks.csv"

def read_db():
    db = []
    with open(FILE, "r") as fp:
        data = fp.readlines()
        for task in data:
            task = task.strip()
            db.append(task)
    db = [task.strip() for task in data]  # list comprehension
    return db


def write_db(tasks):
    with open(FILE, "w") as fp:
        for task in tasks:
            fp.write("{}\n".format(task))


def read_csv():
    db = []
    with open(FILE, "r") as fp:
        reader = csv.DictReader(fp)
        for item in reader:
            db.append(item)
    return db


def write_csv(tasks):
    with open(FILE, "w") as fp:
        keys = tasks[0].keys()
        writer = csv.DictWriter(fp, keys)
        writer.writeheader()
        for task in tasks:
            writer.writerow(task)


def delete_task(index, task_list):
    if index <= len(task_list):
        task = task_list.pop(index)
        print("deleting: {}".format(task))
    else:
        print("cannot find your task")


def mark_task_done(index, task_list):
    if index <= len(task_list):
        task = task_list[index]
        task["done"] = "0"
        print("marking done: {}".format(task["description"]))


def add_task(task, task_list):
    task_list.append(task)
    print("you now have {} tasks to do...".format(len(task_list)))


def show_tasks(task_list):
    for task in task_list:
        if task["done"] == "0":
            continue
        index = task_list.index(task)
        key = "description"
        print("\t{}:\t{}:\t{}".format(index, key, task[key]))
        key = "priority"
        print("\t\t{}:\t\t{}".format(key, task[key]))
    print("<--")

# interface

def ask_for_action():
    print("what do you want to do?")
    print("1. add a task")
    print("2. delete a task")
    print("3. mark task done")
    print("4. show all tasks")
    print("5. save and quit")
    while True:
        action = input("--> ")
        if action.isdigit():
            action = int(action)
            break
        print("please input a number...")
    return action


def ask_for_index():
    while True:
        index = input("which index do you want? ")
        if index.isdigit():
            index = int(index)
            break
        print("only digits please...")
    return index


def ask_for_priority():
    while True:
        index = input("which priority should the task have? ")
        if index.isdigit():
            index = int(index)
            break
        print("only digits please...")
    return index


def ask_for_description():
    min = 5
    while True:
        description = input("what do you have to do? ")
        if len(description) >= min:
            break
        print("description needs to be longer than {} characters".format(min))
    return description


def main():
    tasks = read_db()
    tasks = read_csv()
    while True:
        action = ask_for_action()
        if action == 1:
            description = ask_for_description()
            priority = ask_for_priority()
            task = {
                "description": description,
                "done": "1",
                "priority": priority,
            }
            add_task(task, tasks)
        elif action == 2:
            show_tasks(tasks)
            index = ask_for_index()
            delete_task(index, tasks)
            show_tasks(tasks)
        elif action == 3:
            show_tasks(tasks)
            index = ask_for_index()
            mark_task_done(index, tasks)
            show_tasks(tasks)
        elif action == 4:
            show_tasks(tasks)
        elif action == 5:
            write_csv(tasks)
            print("bye bye!")
            exit()


if __name__ == "__main__":
    main()

Can you create me a task manager please? I made one that is run from the command line with different arguments to modify it's behaviour. If this looks too challenging I can tell you that the full code is 64 lines long, including empty lines! Those of you who already tried out argparse in the previous challenges will probably understand what's going on here. Those who did not I urge you to have a look at the link above and don't hesitate to ask for help!

➜  python_course_doc git:(master) ✗ python3 test.py --help              
usage: test.py [-h] [--file FILE] {add,delete,show} ...

positional arguments:
  {add,delete,show}
    add                 adds a todo item to you todo list
    delete              deletes an item from your todo list
    show                show all your tasks

optional arguments:
  -h, --help            show this help message and exit
  --file FILE, -f FILE  path to your todo file
➜  python_course_doc git:(master) ✗ python3 test.py show  
ID: 0 --- buy milk
ID: 1 --- clean house
ID: 2 --- test my code
➜  python_course_doc git:(master) ✗ python3 test.py add write some documentation
➜  python_course_doc git:(master) ✗ python3 test.py show                        
ID: 0 --- buy milk
ID: 1 --- clean house
ID: 2 --- test my code
ID: 3 --- write some documentation
➜  python_course_doc git:(master) ✗ python3 test.py delete 2
➜  python_course_doc git:(master) ✗ python3 test.py show    
ID: 0 --- buy milk
ID: 1 --- clean house
ID: 2 --- write some documentation
➜  python_course_doc git:(master)
Spoiler warning
import argparse
import pathlib


def read_taskfile(taskfile_path):
    tasks = []
    if not pathlib.Path(taskfile_path).exists():
        return tasks
    with open(taskfile_path, "r") as fp:
        lines = fp.readlines()
    for line in lines:
        task = line.strip()
        tasks.append(task)
    return tasks


def write_taskfile(taskfile_path, tasks):
    with open(taskfile_path, "w") as fp:
        for task in tasks:
            fp.write("{}\n".format(task))


def show_tasks(tasks):
    counter = 0
    for task in tasks:
        print("ID: {} --- {}".format(counter, task))
        counter += 1


def add_task(tasks, task):
    tasks.append(task)
    return tasks


def delete_task(tasks, task_id):
    tasks.pop(task_id) 
    return tasks


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--file", "-f", default="./todo.tasks", help="path to your todo file")
    subparser = parser.add_subparsers()
    add = subparser.add_parser("add", help="adds a todo item to you todo list")
    add.add_argument("task", nargs="*")
    delete = subparser.add_parser("delete", help="deletes an item from your todo list")
    delete.add_argument("idx", type=int, nargs="*")
    show = subparser.add_parser("show", help="show all your tasks")
    show.add_argument("show", action="store_true")
    args = parser.parse_args()

    taskfile = args.file
    tasks = read_taskfile(taskfile)

    if "task" in args:
        tasks = add_task(tasks, " ".join(args.task))
        write_taskfile(taskfile, tasks)
    elif "idx" in args:
        for idx in args.idx:
            tasks = delete_task(tasks, idx)
        write_taskfile(taskfile, tasks)
    elif args.show:
        show_tasks(tasks)

Text based databases

The todo list example from before is handy but quite limited as a database. As is it only holds one form or information and that is the actual task. What if we want to add urgency or mark tasks complete (instead of deleting)? This can be done by grouping data together. We already saw a dictionaries which are good mapping structures but how can we save them to disk? A hacky way would be to write a python file containing the dict and import it when we need it. But there are better ways.

The table below probably makes you think of excel. We can use excel to create a text based database where the first line, called the header, defines what information is stored in the database. Every row after the header is an entry with the item's values. Sounds more complicated than it is.

description urgency done
go to the shops 9 0
install music computer 4 0
download python documentation 7 1
bottle new beer 9 1

We can convert this table to a Comma Separated Value file, which can easily be read by python.

description,urgency,done
go to the shops,9,0
install music computer,4,0
download python documentation,7,1
bottle new beer,9,1

If we save this to a blank file we can us the built-in open function to read and interpret the data as a list of dict. The code below does exactly that, first without list comprehension, secondly with. A CSV file is such a standard in programming that most languages come with a built-in parser, hence the import csv.

import csv

with open("./data.csv", "r") as fp:
    tasks = csv.DictReader(fp)
    print(tasks)
    for task in tasks:
        print(task)


with open("./data.csv", "r") as fp:
    tasks = [task for task in csv.DictReader(fp)]

print(tasks)

🏃 Try it

Adapt your task manager to read tasks for a CSV file. Implement the urgency to sort your tasks by importance. Don't delete tasks from the file but rather mark them as done.

Now for some useful scripting

With everything we have learned up until now you can start doing some interesting and useful things. Have a look at these exercises. You can try them out at your own pace. I can give some hints or pointers if needed, either in class or individually. Don't hesitate to ask for help!

Creating our own objects

We've been using built-in objects like str, int and float but we can create our own objects as well! You might wonder why you would do this and that's a valid reflection. For beginners it's often a bit unclear when to create your own objects and when not. We'll go over some abstract examples to showcase the syntax and usage, then do a couple of practical exercises.

First some abstract examples

A basic abstraction can be seen below. The main thing to take away from this code block is the new keyword class. Once the new class is defined (remember what pass does?), we can create an instance of this class. We can create as many instances as we want, just as with str!

class Animal(object):
    pass


dog = Animal()
dog.name = "bobby"
dog.legs = 4

print("the {} named {} has {} legs".format(
    dog.__class__.__name__,
    dog.name,
    dog.legs,
    )
)

We can create as many attributes as we want and in the example above we do this for a name and a number of legs. An Animal will always have a name and a number of legs so why don't we foresee their existence? We can do this as follows.

class Animal(object):
    def __init__(self, name, legs):
        self.name = name
        self.legs = legs


dog_1 = Animal("bobby", 4)
dog_2 = Animal("dianne", 4)
dog_3 = Animal("rex", 3)

all_my_dogs = [dog_1, dog_2, dog_3]

for dog in all_my_dogs:
    print("the {} named {} has {} legs".format(
        dog.__class__.__name__,
        dog.name,
        dog.legs,
        )
    )

Now, objects don't just possess attributes, they can also do things! Executing a piece of code should get you thinking about functions. A function belonging to a class is called a method and we can easily create them!

class Animal(object):
    def __init__(self, name, legs):
        self.name = name
        self.legs = legs

    def speaks(self):
        print("barks ~~{}~~!".format(self.name.upper()))


dog_1 = Animal("bobby", 4)
dog_2 = Animal("dianne", 4)
dog_3 = Animal("rex", 3)

all_my_dogs = [dog_1, dog_2, dog_3]

for dog in all_my_dogs:
    print("the {} named {} has {} legs".format(
        dog.__class__.__name__,
        dog.name,
        dog.legs,
        )
    )
    dog.speaks()

And just as with functions defined outside of a class we can add arguments to our own methods.

class Animal(object):
    def __init__(self, name, legs, owner):
        self.name = name
        self.legs = legs
        self.owner = owner

    def speaks(self):
        print("barks ~~{}~~!".format(self.name.upper()))

    def jumps_on(self, person):
        if person == self.owner:
            print("{} licks {}'s face...".format(self.name, person))
        else:
            print("{} growls in {}'s face".format(self.name, person))


dog_1 = Animal("bobby", 4, "dave")
dog_2 = Animal("dianne", 4, "alice")
dog_3 = Animal("rex", 3, "dave")

all_my_dogs = [dog_1, dog_2, dog_3]

for dog in all_my_dogs:
    print("the {} named {} has {} legs".format(
        dog.__class__.__name__,
        dog.name,
        dog.legs,
        )
    )
    dog.speaks()
    dog.jumps_on("alice")
    print("\t---\n")

🏃 Try it

Think of some other world objects you can create a class for and do so. The first one that comes to mind is vehicles but you can do anything that makes sense to you.

Magic methods

import random


SEXES = ["male", "female"]


class Dog(object):
    SOUND = "woof"

    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex

    def __str__(self):
        if self.age > 3:
            bark = self.__class__.SOUND.upper()
        else:
            bark = self.__class__.SOUND.lower()
        return "{} barks {}".format(self.name, bark)

    def __add__(self, other):
        if self.sex == other.sex:
            return None
        name_part_1 = self.name[0:int(len(self.name) / 2)]
        name_part_2 = other.name[0:int(len(self.name) / 2)]
        name = name_part_1 + name_part_2
        child = Dog(name, 0, random.choice(SEXES))
        return child


if __name__ == "__main__":
    dog_1 = Dog("bobby", 4, SEXES[0])
    dog_2 = Dog("molly", 4, SEXES[1])
    dog_3= Dog("rex", 4, SEXES[0])
    puppy = dog_1 + dog_2 + dog_3
    print(puppy)
    
class DataBase(object):
    def __init__(self, data):
        self.data = data 

    def __iter__(self):
        for element in self.data:
            yield element


if __name__ == "__main__":
    db = DataBase(['hello', "world", "test", "one", "two"])
    for item in db:
        print(item)

Class inheritance

Inheritance is one of the four main pillars of Object Orientated Programming. The idea is pretty simple, classes can inherit behaviour from a parent class. For example, Dog and Cat are both Animal so they share common attributes and methods.

class Animal(object):
    def __init__(self, name, legs, owner):
        self.name = name
        self.legs = legs
        self.owner = owner

    def jumps_on(self, person):
        if person == self.owner:
            print("{} licks {}'s face...".format(self.name, person))
        else:
            print("{} growls in {}'s face".format(self.name, person))


class Dog(Animal):
    CALL = "barks"

    def __init__(self, name, legs=4, owner=None):
        Animal.__init__(self, name, legs, owner)

    def speaks(self):
        print("{} ~~{}~~!".format(self.__class__.CALL, self.name.upper()))


class Cat(Animal):
    CALL = "miauws"

    def __init__(self, name, legs=4, owner=None):
        Animal.__init__(self, name, legs, owner)

    def speaks(self):
        print("{} ~~{}~~!".format(self.__class__.CALL, self.name.lower()))


class Parrot(Animal):
    CALL = "creaks"

    def __init__(self, name, legs=2, owner=None):
        Animal.__init__(self, name, legs, owner)

    def speaks(self):
        print("{} ~~{}~~!".format(self.__class__.CALL, self.name.lower()))

    def jumps_on(self, person):
        print("you're silly, I'm a {}, I don't jump...".format(
            self.__class__.__name__)
            )
    

animal_1 = Dog(name="bobby", owner="dave")
animal_2 = Dog(name="dianne", owner="alice")
animal_3 = Cat("garfield", 3, "dave")
animal_4 = Parrot("coco", "alice")
animal_5 = Cat("gertrude", 3, "dave")

all_animals = [animal_1, animal_2, animal_3, animal_4, animal_5]

for animal in all_animals:
    print("the {} named {} has {} legs".format(
        animal.__class__.__name__,
        animal.name,
        animal.legs,
        )
    )
    animal.speaks()
    animal.jumps_on("alice")
    print("\t---\n")

🏃 Try it

Take the additional class you made, such as vehicle and do some inheritance. For vehicles you could create a base class with a brand and number of wheels and seats. Then inherit for a bus, bike, car, cycle, etc.

Coding challenge - Chalet floor

Can you calculate me how much wood I need to order to build the base of my chalet? I have three different plans, you can see below. The idea behind this exercise is to create a class for each base shape. If you implement the right methods you can add, subtract, multiply your shapes as you wish. I created a library and imported it in my python shell so I can do the following.

>>> import shapes
>>> sq1 = shapes.
shapes.Circle(     shapes.Rectangle(  shapes.Shape(      shapes.Square(     shapes.Triangle(   shapes.math
>>> sq1 = shapes.Square(400)
>>> cr1 = shapes.Circle(30)
>>> re1 = shapes.Rectangle(400, 600)
>>> total = sq1 + re1 - cr1
>>> print(total)
I'm a Shape with an area of 397172.57 cm2
>>> print(re1)
I'm a Rectangle with an area of 240000.00 cm2
>>> print(cr1)
I'm a Circle with an area of 2827.43 cm2
>>> print(sq1)
I'm a Square with an area of 160000.00 cm2
>>> 

There are quite a few things in this exercise we have not seen in detail, so don't hesitate to research online and ask questions! This is a quite advanced challenge so you can be very proud of yourself if you understand the solution!

first plan

second plan

third plan

third plan 3D view

Spoiler warning
import math


class Shape(object):
    def __init__(self):
        self.area = 0

    def __str__(self):
        return "I'm a {} with an area of {:.2f} cm2".format(
                self.__class__.__name__,
                self.area,
                )
    
    def __add__(self, other):
        combi_shape = Shape()
        combi_shape.area = self.area + other.area
        return combi_shape

    def __sub__(self, other):
        combi_shape = Shape()
        combi_shape.area = self.area - other.area
        return combi_shape

    def __div__(self, value):
        combi_shape = Shape()
        combi_shape.area = self.area / value
        return combi_shape


class Square(Shape):
    def __init__(self, width):
        Shape.__init__(self)
        self.area = width * width


class Rectangle(Shape):
    def __init__(self, width, height):
        Shape.__init__(self)
        self.area = width * height


class Triangle(Shape):
    def __init__(self, base, height):
        Shape.__init__(self)
        self.area = (self.width * self.height) / 2


class Circle(Shape):
    def __init__(self, radius):
        Shape.__init__(self)
        self.area = math.pi * math.pow(radius, 2)


if __name__ == "__main__":
    t = Square(4)
    r = Rectangle(4, 6)
    c = Circle(1.6)
    print(t)
    print(r)
    print(c)
    combi = t + r - c
    print(combi)

Now some practical improvements

Improve the login generator

First the library...

Oimport string
import random
import json


def load_file(filepath):
    words = []
    with open(filepath, "r") as fp:
        data = fp.readlines()
    for line in data:
        word = line.strip()
        words.append(word)
    return words


class Login(object):
    ADJECTIVES = load_file("adjectives.txt")
    ANIMALS = load_file("subjects.txt")

    def __init__(self, site=None):
        self.site = site
        self.generate_password()
        self.generate_username()

    def __str__(self):
        msg = "site: {} - username: {} with password: {}".format(self.site, self.username, self.password)
        return msg

    def generate_password(self, length=16, complex=False):
        if complex:
            db = list(string.ascii_letters + string.digits + string.punctuation)
        else:
            db = list(string.ascii_letters)
        password = []
        for i in range(0, length):
            char = random.choice(db)
            password.append(char)
        password = "".join(password)
        self.password = password

    def generate_username(self):
        """generates a random username from a list of animals and adjectives"""
        adj = random.choice(self.__class__.ADJECTIVES)
        sbj = random.choice(self.__class__.ANIMALS)
        username = f"{adj.capitalize()}{sbj.capitalize()}"
        self.username = username

    def set_password(self, password):
        self.password = password

    def set_username(self, username):
        self.username = username

    def verify_password(self, password):
        if password == self.password:
            return True
        return False

    def verify_username(self, username):
        if username == self.username:
            return True
        return False

    def verify_login(self, username, password):
        # if password == self.password and username == self.username:
        if self.verify_password(password) and self.verify_username(username):
            return True
        return False


class Database(object):
    def __init__(self, path="db.json"):
        self.path = path
        self.data = []

    def add(self, login):
        if isinstance(login, Login):
            self.data.append(login)

    def get_all(self):
        return self.data

    def get_by_index(self, index):
        if index in range(0, len(self.data)):
            return self.data[index]

    def save_to_disk(self):
        with open(self.path, "w") as fp:
            json.dump(self.data, fp , default=vars)

    def load_from_disk(self):
        with open(self.path, "r") as fp:
            data = json.load(fp)
        for item in data:
            l = Login(item["site"])
            l.set_password(item["password"])
            l.set_username(item["username"])
            self.data.append(l)


if __name__ == "__main__":
    db = Database()
    db.load_from_disk()
    for login in db.data:
        print(login)
        login.generate_password()
    db.save_to_disk()

Now the command line interface with a bad separation of responsibilities.

from login_lib_OOP import Login, Database


class CommandLineInterface(object):
    def __init__(self, db):
        self.db = db

    def ask_for_int(self, msg):
        while True:
            result = input(msg)
            if result.isdigit():
                number = int(result)
                break
            print("only intergers please...")
        return number

    def ask_for_bool(self, msg):
        while True:
            result = input(msg).lower()
            if result.startswith("y"):
                status = True
                break
            elif result.startswith("n"):
                status = False
                break
            print("answer with y(es)/n(o) please...")
        return status

    def ask_for_view(self):
        msg = "do you want to view your logins?"
        status = self.ask_for_bool(msg)

    def ask_for_add_logins(self):
        msg = "how many logins do you want to add?"
        number = self.ask_for_int(msg)
        for i in range(0, number):
            l = Login("undefined")
            self.db.add(l)

    def view_logins(self):
        for login in self.db.data:
            print("\t{}: {}".format(self.db.data.index(login), login))

    def main_menu(self):
        actions = ["show logins", "add logins", "save to disk","load from disk", "quit"]
        for action in actions:
            print("{}: {}".format(actions.index(action), action))
        msg = "what do you want to do?"
        action = self.ask_for_int(msg)
        if action == 0:
            self.view_logins()
        elif action == 1:
            self.ask_for_add_logins()
        elif action == 2:
            self.db.save_to_disk()
        elif action == 3:
            self.db.load_from_disk()
        elif action == 4:
            return False
        return True

    def run(self):
        while True:
            status = self.main_menu()
            if not status:
                break


if __name__ == "__main__":
    db = Database()
    app = CommandLineInterface(db)
    app.run()

Now a minimal MVC version...

from login_lib_OOP import Login, Database


class Controller(object):
    def __init__(self, model):
        self.model = model

    def set_view(self, view):
        self.view = view

    def add_multiple_logins(self, number):
        for i in range(0, number):
            l = Login()
            self.model.add(l)
            self.view.show_message("added {}".format(l))

    def get_all_logins(self):
        data = self.model.get_all()
        self.view.show_message("there are {} logins in your database".format(len(data)))
        for login in data:
            msg = "{}: {}".format(data.index(login), login)
            self.view.show_data(msg)

    def get_one_login(self, index):
        login = self.model.get_all()[index]
        return login

    def load(self):
        self.model.load_from_disk()
        number = len(self.model.get_all())
        self.view.show_message("loaded {} logins from disk".format(number))

    def save(self):
        self.model.save_to_disk()
        number = len(self.model.get_all())
        self.view.show_message("saved {} logins to disk".format(number))


class CommandLineInterface(object):
    def __init__(self, controller):
        self.controller = controller

    def ask_for_int(self, msg):
        while True:
            result = input(msg)
            if result.isdigit():
                number = int(result)
                break
            print("only intergers please...")
        return number

    def ask_for_bool(self, msg):
        while True:
            result = input(msg).lower()
            if result.startswith("y"):
                status = True
                break
            elif result.startswith("n"):
                status = False
                break
            print("answer with y(es)/n(o) please...")
        return status

    def ask_for_login_index(self):
        msg = "which login do you want to modify?"
        number = self.ask_for_int(msg)
        return number

    def show_data(self, msg):
        print("\t{}".format(msg))

    def show_message(self, msg):
        print("MSG: {}".format(msg))

    def show_header(self, msg):
        print(msg)
        print(len(msg) * "-")

    def main_menu(self):
        self.show_header("main menu:")
        actions = ["show logins", "add logins", "save to disk", "load from disk", "modify a login", "quit"]
        for action in actions:
            print("{}: {}".format(actions.index(action), action))
        msg = "what do you want to do?"
        action = self.ask_for_int(msg)
        if action == 0:
            self.controller.get_all_logins()
        elif action == 1:
            msg = "how many logins do you want?"
            number = self.ask_for_int(msg)
            self.controller.add_multiple_logins(number) 
        elif action == 2:
            self.controller.save()
        elif action == 3:
            self.controller.load()
        elif action == 4:
            self.sub_menu_modify()
        elif action == 5:
            return False
        return True

    def sub_menu_modify(self):
        self.show_header("modify entry:")
        msg = "which login do you want to modify?"
        number = self.ask_for_int(msg)
        login = self.controller.get_one_login(number)
        msg = "do you want to change the site name?"
        status = self.ask_for_bool(msg)
        if status:
            login.site = input("for which site is this login?")
            self.show_message("set site to: {}".format(login.site))
        msg = "do you want to renew the username?"
        status = self.ask_for_bool(msg)
        if status:
            login.generate_username()
            self.show_message("set username to: {}".format(login.username))
        msg = "do you want to renew the password?"
        status = self.ask_for_bool(msg)
        if status:
            login.generate_password()
            self.show_message("set password to: {}".format(login.password))
        msg = "save changes?"
        status = self.ask_for_bool(msg)
        if status:
            self.controller.save()

    def run(self):
        while True:
            status = self.main_menu()
            if not status:
                break


if __name__ == "__main__":
    model = Database()
    controller = Controller(model)
    view = CommandLineInterface(controller)
    controller.set_view(view)
    view.run()

Improve the task manager

TODO convert the task manager to a class

Infinite programs

  • insist on the nature of scripts we did up until now

Logic breakdown of a simple game

***********************
* welcome to hangman! *
***********************

guess the hidden word below
---------------------------
word: *****
guess a letter: a
word: a****
guess a letter: l
word: a**l*
guess a letter: p
word: appl*
guess a letter: e
word: apple
*****************
* you found it! *
*****************
do you want to play a new game? (Y/N)

When looking at the game above we can break down the game logic as follows.

  1. A welcome message is printed
  2. A word-to-find is chosen randomly from a database
  3. The word is shown as ***** to hint at the length of the word
  4. A prompt is presented to the player and the can input one letter
  5. If the letter is present in the word-to-guess it is saved and shown from now on
  6. The previous 2 steps are repeated until the full word is found
  7. A winning banner is shown
  8. The player is asked to play a new game or not.

A non object orientated solution

You can try to create the hangman game in multiple stages. Below you can find multi level hints towards a solution.

Spoiler warning
First hint
import random


def get_random_word():
    pass

def ask_for_letter():
    pass

def is_letter_in_word():
    pass

def show_hidden_word():
    pass

def show_end_game(status):
    pass

def game():
    pass
    
def ask_new_game():
    pass

def main():
    pass 


if __name__ == "__main__":
    main()

Second hint
import random


def get_random_word():
    pass

def ask_for_letter():
    pass

def is_letter_in_word(letter, word):
    pass

def show_hidden_word(word, letters_found):
    pass

def show_end_game(status):
    pass

def game():
    pass

def ask_new_game():
    pass

def main():
    while True:
        status = game()
        show_end_game(status)
        status = ask_new_game()
        if not status:
            break
    print("bye bye!")


if __name__ == "__main__":
    main()

Possible solution
import random


def get_random_word():
    words = ["hello", "world", "apple", "banana", "hippopotamus"]
    return random.choice(words)


def ask_for_letter():
    while True:
        letter = input("your letter please: ")
        if len(letter) != 1:
            print("only one letter! no cheating!")
            continue
        if not letter.isalpha():
            print("letters please, no numbers nor spaces...")
            continue
        else:
            break
    return letter.lower()


def is_letter_in_word(letter, word):
    if letter in word:
        return True
    return False


def show_hidden_word(word, letters_found):
    hidden = ""
    for letter in word:
        if letter in letters_found:
            hidden += letter
        else:
            hidden += "*"
    print("word to find: {}".format(hidden))
    if "*" in hidden:
        return False
    return True


def show_end_game(status):
    if status:
        print("good job!")
    else:
        print("better luck next time...")


def game():
    word_to_guess = get_random_word()
    letters_found = []
    difficulty = len(word_to_guess) * 3
    while len(letters_found) <= difficulty:
        left = difficulty - len(letters_found)
        print("you have {} tries left".format(left))
        status = show_hidden_word(word_to_guess, letters_found)
        if status:
            return True
        letter = ask_for_letter()
        letters_found.append(letter)
    return False


def ask_new_game():
    while True:
        result = input("do you want to play a new game?").lower()
        if result.startswith("y"):
            status = True
            break
        elif result.startswith("n"):
            status = False
            break
        else:
            print("please answer y(es) or n(o)")
    return status


def main():
    while True:
        status = game()
        show_end_game(status)
        status = ask_new_game()
        if not status:
            break
    print("bye bye!")


if __name__ == "__main__":
    main()

An object orientated solution

You can try to create the hangman game in multiple stages. Below you can find multi level hints towards a solution.

Spoiler warning
First hint
import random


class Word(object):
    def __init__(self):
        pass

    def __iter__(self):
        pass

    def __len__(self):
        pass

    def is_letter_in_word(self):
        pass


class Game(object):
    def __init__(self):
        pass

    def play_turn(self):
        pass

    def get_hidden_word(self):
        pass

    def is_word_found(self):
        pass


class Controller(object):
    def __init__(self):
        pass

    def set_view(self):
        pass

    def submit_letter(self):
        pass

    def request_hidden_word(self):
        pass

    def request_new_game(self):
        pass


class CommandLineInterface(object):
    def __init__(self):
        pass

    def ask_for_letter(self):
        pass
        
    def ask_for_bool(self):
        pass

    def show_message(self):
        pass

    def run(self):
        pass
        

if __name__ == "__main__":
    pass

Second hint
import random


class Word(object):
    def __init__(self):
        pass

    def __iter__(self):
        pass

    def __len__(self):
        pass

    def is_letter_in_word(self, letter):
        pass


class Game(object):
    def __init__(self):
        pass

    def play_turn(self, letter):
        pass

    def get_hidden_word(self):
        pass

    def is_word_found(self):
        pass


class Controller(object):
    def __init__(self, model):
        pass

    def set_view(self, view):
        pass

    def submit_letter(self, letter):
        pass

    def request_hidden_word(self):
        pass

    def request_new_game(self):
        pass


class CommandLineInterface(object):
    def __init__(self, controller):
        pass

    def ask_for_letter(self):
        pass
        
    def ask_for_bool(self, msg):
        pass

    def show_message(self, msg):
        pass

    def run(self):
        pass
        

if __name__ == "__main__":
    model = Game()
    controller = Controller(model)
    view = CommandLineInterface(controller)
    controller.set_view(view)
    view.run()

Third hint
import random


class Word(object):
    def __init__(self):
        pass

    def __iter__(self):
        pass

    def __len__(self):
        pass

    def is_letter_in_word(self, letter):
        pass


class Game(object):
    def __init__(self):
        pass

    def play_turn(self, letter):
        pass

    def get_hidden_word(self):
        pass

    def is_word_found(self):
        pass


class Controller(object):
    def __init__(self, model):
        pass

    def set_view(self, view):
        pass

    def submit_letter(self, letter):
        pass

    def request_hidden_word(self):
        pass

    def request_new_game(self):
        pass


class CommandLineInterface(object):
    def __init__(self, controller):
        pass

    def ask_for_letter(self):
        pass
        
    def ask_for_bool(self, msg):
        pass

    def show_message(self, msg):
        pass

    def run(self):
        while True:
            self.controller.request_hidden_word()
            letter = self.ask_for_letter()
            status = self.controller.submit_letter(letter)
            if status:
                status = self.ask_for_bool("do you want to play a new game?")
                if status:
                    self.controller.request_new_game()
                else:
                    break
        self.show_message("bye bye!")


if __name__ == "__main__":
    model = Game()
    controller = Controller(model)
    view = CommandLineInterface(controller)
    controller.set_view(view)
    view.run()

Possible solution
import random


DB = ["hello", "world", "apple", "banana", "hippopotamus"]


class Word(object):
    def __init__(self):
        self.data = list(random.choice(DB))

    def __iter__(self):
        for element in self.data:
            yield element

    def __len__(self):
        return len(self.data)

    def is_letter_in_word(self, letter):
        if letter in self.data:
            return True
        else:
            return False


class Game(object):
    def __init__(self):
        self.word = Word()
        self.tries = len(self.word) * 3
        self.letters_found = []

    def play_turn(self, letter):
        if self.tries == 0:
            return False
        self.letters_found.append(letter)
        if self.word.is_letter_in_word(letter):
            if self.is_word_found():
                return True
        self.tries -= 1
        return None

    def get_hidden_word(self):
        hidden = ""
        for letter in self.word:
            if letter in self.letters_found:
                hidden += letter
            else:
                hidden += "*"
        return hidden

    def is_word_found(self):
        word = self.get_hidden_word()
        if "*" in word:
            return False
        else:
            return True


class Controller(object):
    def __init__(self, model):
        if isinstance(model, Game):
            self.model = model

    def set_view(self, view):
        if isinstance(view, CommandLineInterface):
            self.view = view

    def submit_letter(self, letter):
        status = self.model.play_turn(letter)
        if status:
            self.view.show_message("congragulations! you found it!")
            return True
        elif status == False:
            self.view.show_message("better luck next time...")
            return True
        else:
            self.view.show_message("you have {} tries left".format(self.model.tries))
    
    def request_hidden_word(self):
        self.view.show_message(self.model.get_hidden_word())

    def request_new_game(self):
        self.model = Game()


class CommandLineInterface(object):
    def __init__(self, controller):
        self.controller = controller

    def ask_for_letter(self):
        while True:
            letter = input("your letter please: ")
            if len(letter) != 1:
                print("only one letter! no cheating!")
                continue
            if not letter.isalpha():
                print("letters only please, no numbers nor spaces...")
                continue
            else:
                break
        return letter.lower()

    def ask_for_bool(self, msg):
        while True:
            result = input(msg).lower()
            if result.startswith("y"):
                status = True
                break
            elif result.startswith("n"):
                status = False
                break
            else:
                print("please answer y(es) or n(o)")
        return status

    def show_message(self, msg):
        print(msg)

    def run(self):
        while True:
            self.controller.request_hidden_word()
            letter = self.ask_for_letter()
            status = self.controller.submit_letter(letter)
            if status:
                status = self.ask_for_bool("do you want to play a new game?")
                if status:
                    self.controller.request_new_game()
                else:
                    break
        self.show_message("bye bye!")


if __name__ == "__main__":
    model = Game()
    controller = Controller(model)
    view = CommandLineInterface(controller)
    controller.set_view(view)
    view.run()

Trivial pursuit multiple choice game

We can fetch questions for an online api to make a game. I downloaded a mini set of questions to help you get started. Have a look at the code below to understand how to develop your game logic. Once you successfully build a game you can try and integrate the requests library to get fresh questions each time you play the game.

import json
import html

with open("./assets/quiz_data.json", "r") as fp:
    data = json.load(fp)

for key, value in data.items():
    print(key)

for question in data["results"]:
    for key, value in question.items():
        print(key)

for question in data["results"]:
    print("{}".format(html.unescape(question["question"])))
    choices = list()
    choices.append(question["correct_answer"])
    choices.extend(question["incorrect_answers"])
    for choice in enumerate(choices):
        print(html.unescape("\t {} {}".format(*choice)))

You can get some inspiration from a small project I did for an other course. The code can be found here.

Introduction to the requests library

Threading

TODO add a countdown timer to the multiple choice game