Python Intermediate – Exercises

Yoan Mollard

List of mini-projects

  1. Hanged man practice Python scripting, in PyCharm
  2. BFS in a graph choose the right data collection
  3. Money transfer simulator develop, test and distribute a complete package
  4. Communicate with a REST API practice requests and multiprocessing
  5. Plot ping durations practice subprocess and matplotlib

Enable zoom in PyCharm

If needed, enable zooming with:
Ctrl+wheel in File > Settings:

Mini-project 1: Hanged man

You probably know the hanged man game:

  1. The player is shown a secret word in which letters are hidden by underscores
  2. Each turn, the player proposes a letter to unveil
  3. If the chosen letter is part of the word, all their occurences are revealed

The goal is to reveal the secret word in less turns that there are letters in it.

___IC__S_I_U_IO___LL_M___
   ________
   |       |
  \o/      |
   |       |
  / \      |
_______________________
  1. Write and test a function input_letter() that asks the user to type a letter and returns it. This functions retries in case the user types anything that is not valid (a number, punctuation, several letters, ...).

ℹ️ The function input("Prompt:") returns a a string read from the console

  1. Write a function unveil(letter, original_word, hidden_word) that browses all characters of a hidden word and reveals the requested letter at the right position if it is in the original word.

⚠️ The str type is immutable

ℹ️ hidden word can be either fully hidden by underscores, or only partially hidden

  1. Give 4 examples representing the 4 possible cases for inputs and outputs of the unveil(...) function. Use assert to make sure they pass all four.
  1. Define and initialize the following variables to coherent initial values:
  • words: a list of possible words to be guessed
  • secret_word: a secret word randomly picked among the previous list (use for instance random.choice)
  • displayed_word: the partially hidden word, i.e. the word of same length as the secret word in which every letter is replaced by an underscore
  • remaining_attempts: the number of remaining attempts. For simplicity, initialize it to the number of letters in secret_word. This counter must be decremented every turn.
  1. Add a game loop that:
  • Displays the partially hidden word and the number of remaining attempts
  • Prompts the player to enter a valid letter with input_letter()
  • Replaces matches of this letter from secret_word in displayed_word, if any
  • Checks the game state: exit the program with an appropriate message if the player wins or looses

You game must now be playable!

  1. Optional question: Remember high scores in a JSON file:

Use the built-in 🐍 json module to dump the 3 best scores into a .json file. Load that file at each startup to show high scores.

Mini-project 2. BFS in a graph

BFS browses a tree data structure. It starts at the tree root and visits all nodes at the present depth prior to moving on to the nodes at the next depth level.

From Breadth-first_search, Wikipedia.

📈 Black = visited ; Grey = queued to be visited later
ℹ️ BFS is known as parcours en largeur in French

Here is the general algorithm of the BFS in pseudocode:

Input:  A graph G, 
Input:  A root node where to start BFS
Output: VISITED, the list of all nodes in BFS order
 1  function BFS(G, ROOT) is
 2      let Q be a list
 3      let VISITED be a list 
 4      append ROOT at the end of VISITED
 5      append ROOT at the end of Q
 6      while Q is not empty do
 7          v := pop the first node of Q
 8          for all children (w) of node v in G, do
 9              if w is not in VISITED then
10                  append w at the end of VISITED
11                  append w at the end of Q
12      return VISITED

We have implemented here a naive implementation of the preceeding BFS pseudocode, where a graph is implemented as:

G = { # dict representing the children of all nodes
  5 : [3, 7, 9], # 5 has 3 children: 3 7 and 9 
  3 : [2, 4, 10],
  7 : [8],
  2 : [],    # A node with an empty list is a leaf
  4 : [8, 9],
  8 : [],
  9 : [12, 11],
  10: [13, 10, 8, 9],
  11: [3, 7, 9],
  12: [],
  13: []}

1.: Run the naive BFS and observe the running time.

2.: Implement a new version improved_bfs() that fixes the performance issues that you spotted with the profiler.

3. (Optional): Download Pypy and run the BFS against Pypy instead of CPython. Since Pypy do not support all Python features, you will have to adapt the code with a trial-and-error approach.

Mini-project 3. Money transfer simulator

In this exercise we are going to create a simplified Information System that is able to handle and simulate bank transactions.

In our scenario there are 4 actors: a bank (HSBC), a supermarket (Walmart), and 2 individuals Alice and Bob.

Each actor has his/her own bank account.

Part 1: The basic scenario

  • 1.1. Create a class BankAccount that owns 2 attributes:
    • _owner (of type str): the owner's name
    • _balance (of type int): the balance (do not take care of decimals)
    • the class constructor takes in parameter, in this order, owner and initial_balance

With your class it must be possible to execute the following scenario (that has no effect so far, but it must not raise any error):

bank = BankAccount("HSBC", 10000)
walmart = BankAccount("Walmart", 5000)
alice = BankAccount("Alice Worz", 500)
bob = BankAccount("Bob Müller", 100)
  • 1.2. Implement a __str__() method in class BankAccount that displays the name of the owner and current balance. Iterate on all accounts to print them.

  • 1.3. Implement these methods :

    • _credit(value) that credits the current account with the value passed in parameter. We will explain the goal of the initial underscore later.
    • transfer_to(recipient, value) that transfers the value passed in parameter to the recipient passed in parameter
  • 1.4. Run the following scenario and check that end balances are right:

    • 1.4.1. Alice buys $100 of goods at Walmart
    • 1.4.2. Bob buys $100 of goods at Walmart
    • 1.4.3. Alice makes a donation of $100 to Bob
    • 1.4.4. Bob buys $200 at Walmart

Part 2: The blocked account

Bob is currently overdrawn. To prevent this, its adviser converts his account into a blocked account: any purchase would be refused if Bob had not enough money.

  • 2.1. Create the new InsufficientBalance exception type inheriting from ValueError. No code is needed into that new class: use pass to skip code.

  • 2.2. Implement a class BlockedBankAccount so that:

    • the BlockedBankAccount inherits from BankAccount. Make sure you do not forget to call parent methods with super() if necessary
    • the transfer_to methods overrides the parent method, with the only difference that it raises InsufficientBalance if the balance is not sufficiently provided to execute the transfer
  • 2.3. Replace Bob's account by a blocked account and check that the previous scenario actually raises an exception

  • 2.4. Protect the portion of code that looks coherent with try..except in order to catch the exception without interrupting the script

  • 2.5. Explain the concept of protected method and the role of the underscore in front of the method name ; and why it is preferable that _credit is protected

Part 3: The account with agios

In real life another kind of account exists: the account whose balance can actually be negative, but it that case the owner must pay agios to his(her) bank.

The proposed rule here is that, when an account is negative after an outgoing money transfer, each day will cost $1 to the owner until the next money credit.

To do so, we need to introduce transaction dates in our simulation.

3.1. Implement a class AgiosBankAccount so that:

  • the AgiosBankAccount inherits from BankAccount. Make sure you do not forget to call parent method with the super() keyword if necessary
  • the constructor of this account takes in parameter the account of the bank so that agios can be credited on their account.

3.2. Implements the transfer_to method overrides the parent method:

  • it takes the transaction_date in parameter, of type datetime
    (also change the parent class and propagate the date paramter to the base classes and the other child class when necessary)
  • it records the time from which the balance becomes negative. You need an additional attribute for this.

3.3. Implement the _credit method that overrides the method from the parent class, with the only difference that it computes the agios to be payed to the bank and transfer the money to the bank. Round agios to integer values.

3.4. Check your implementation with the previous scenario: After Bob has a negative balance, Alice makes him a transfer 5 days later: make sure that $5 of agios are payed by Bob to his bank.

Part 4: The account package

We have just coded a very simple tool simulating transactions between bank accounts in Object-Oriented Programming.

In order to use it with a lot of other scenarii and actors, we are going to structure our code within a Python package.

We will organise our accounts with the following terminology:

  • bank-internal accounts do not create agios and are not blocked, there are BankAccount and only banks can own such account
  • bank-external accounts are for individuals or companies, they can be either blocked or agios accounts.

We would like to be able to import the classes from than manner:

from account.external.agios import AgiosBankAccount
from account.external.blocked import BlockedBankAccount, InsufficientBalance
from account.internal import BankAccount
  • 4.1. Re-organize your code in order to create this hierarchy of empty .py files first as on the figure.
    Create an empty script scenario1.pyfor the scenario.
  • 4.2. Create a logger for each module: agios.py, blocked.py, internal.py. Make sure you log useful debug info in the next questions.

  • 4.3. Move the class declaration of AgiosBankAccount in agios.py

  • 4.4. Move the class declaration of BlockedBankAccount in blocked.py

  • 4.5. Move the class declaration of BankAccount in internal.py

  • 4.6. Move the scenario (i.e. the successive instanciation of all accounts of companies and individuals) in scenario1.py

  • 4.6. Check each module and add missing relative import statements
    Relative imports start with . or ..

  • 4.7. Check each module and add missing absolute import statements such as datetime

⚠️ Import statements in the scenario must not be relative because scenario1.py will be located outside package account.

  • 4.8. Add empty __init__.py files to all directories of the package.

  • 4.9. Execute the scenario and check that it leads to the same result as before this refactoring

Part 5: Test your package with pytest

  • 5.1. Install pytest with pip
  • 5.2. Create independant test files tests/<module>.py for each module of your package
  • 5.3. Add an entry in sys.path pointing to the parent folder of your package so that pytest is able to locate and import your account package (*)
  • 5.4. With the documentation of pytest, implement unit tests for your classes and run the tests with pytest

*This workaround is not ideal since this path is different on each system, and the situation will be fixed once the package will be made installable in Part 6.

Part 6: Automate package building and testing with tox

6.1. Make your package installable

Refer to the doc about package creation

Create a metadata file pyproject.toml and update its metadata (package name, author, license, description...)

Delete the sys.path workaround in test files since the package is now installable

6.2. Install, configure and run tox

Refer to the tox basic example. Create a basic tox.ini so that your package is built and tested against Python 3.12 and 3.11.

Install and run tox in your project. Make sure all tests pass in both environments.

Re-organise your project structure as proposed in the figure. In Pycharm File > Settings > Project > Project Structure, identify src as a source folder so that the linter can identify your source files.

Part 7: Distribute your package on TestPyPi (Optional)

  • 7.1. Refer to the doc about package creation to create a minimal pyproject.toml
  • 7.2. Name your package accounts-<MYNAME> and substitute your name
  • 7.3. Install build, wheel and twine
  • 7.4. Refer to the doc to build sdist and bdist_wheel distributions
  • 7.5. Upload both distributions to TestPyPI using login __token__. For the password, ask for the token or create your own TestPyPI account and new token.
  • 7.6. Make sure you can install your package from the TestPyPI index via pip:
    pip install accounts-MYNAME --index-url https://test.pypi.org/simple/

Mini-project 4. Communicate with a REST API

https://jsonplaceholder.typicode.com/ exposes a REST API to list/publish posts on a fake blog.

Each blog post has a title + a description + optional photo album.

Part I: Get 10 existing blog posts

Install requests and emit a GET request to get document /posts to server https://jsonplaceholder.typicode.com

Retrieve all posts and print the title of the first 10 posts.

Part II: Publish a new blog post

2.1. On the same endoint as before, emit a POST request with a payload to publish a new blog post.

You need to send a JSON payload that includes:

  • title: Python training
  • body: I am training to the requests module with Python
  • userId: A user ID for the author (eg., user id 1)

2.2. Check in the response that the publish was successful

ℹ️ This API is public on the Internet, so for safety purposes, even successful requests do not actually modify the database.

Part III: Delete your blog post

3.1. With a GET, identify which blog ID has title optio dolor molestias sit.

3.2. Send a DELETE request to remove the post with the found id. Check the server response.

Part IV: 4 nested GET requests to download all photos

Get the list of albums for the user with the username 'Delphine' and then download all photos for the first album in the list:

  • Get the list of users and search for the one named Delphine
  • Get the list of albums for the user 'Delphine'
  • Get the list of all photo URLs of all Delphine's album

Part V: Get all comments in a CSV

5.1. Get all posts with /posts
5.2. For each found post ID, get its comments with /posts/{id}/comments
5.3. Use a DictWriter to write them into comments.csv
5.4. Measure the overall execution time

Part VI: Get all comments in a CSV via multiprocessing

Start from the code of the previous part.

6.1. Add a multiprocessing Manager, a Pool of 4 workers, and a multiprocessing list to be shared into workers
6.2. Divide the list of posts into 4 chunks (the worker posts)
6.3. Isolate the job into get(worker_posts: list, comments: mp.list)
6.4. Starmap worker_posts, comments to get()
6.5. Measure the overall execution time

ℹ️ On Windows, move your code into a if __name__ == '__main__ block. THis is required since Windows does not fork processes but only the main function.

Mini-project 5. Plot ping durations

ping is a network tool sending ICMP requests to hosts. An ICMP request is sent every second and displays its round-trip duration in milliseconds.

The project aims at developing a Python tool that plots durations of ping requests.

  1. Use argparse to accept arguments host (string) and iterations (int)
  2. Use subprocess to call ping on the host passed in argument
    On Windows, pass iterations to -n
    On Linux, pass iterations to -c.
  3. Capture the stdout stream of the subprocess into Python variables
  4. Exclude header/footer lines and error lines based on their format (split strings on delimiters)
  5. Convert each ping duration in millisec into a float and save them in a list
  6. Terminate the ping subprocess after the specified number of iterations
  7. Use matplotlib to plot all ping durations, a dashed mean plot (style="--"), and the standard deviation (fill_between) computed with numpy.

ℹ️ Windows console commands are CP1252 or CP437-encoded. UTF-8 for Linux.

and add argument `-t`.