From d21625bdd7b64cced98219a32bdcebdda66d7a04 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:25:32 +0000 Subject: [PATCH 01/19] Complete exercises for Module Tools/Sprint 5/Prep - step 1 'Why we use types' --- prep-exercises/a_why_use_types.py | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 prep-exercises/a_why_use_types.py diff --git a/prep-exercises/a_why_use_types.py b/prep-exercises/a_why_use_types.py new file mode 100644 index 00000000..db2e96f4 --- /dev/null +++ b/prep-exercises/a_why_use_types.py @@ -0,0 +1,42 @@ +# ------------------------ +# Exercise 1 +# ------------------------ +# Q. What does double("22") return? Did it do what you expected? Why did it return the value it did? +# A. I expected the result to be 44 through coercion, like JavaScript. +# It returned the string repeated i.e. 2222 because Python is a strongly typed language and doesn't use coercion + +def double(input): + return input * 2 + +print(double("22")) # returns "2222" + + + +# ------------------------ +# Exercise 2 +# ------------------------ +# Q. Read the code and write down what the bug is. How would you fix it? +# A. In this function it returns the value of 30, which is the input * 3 which is logically correct, however you would +# expect the function to be called "triple" or you would expect the logic to be number * 2, so i would amend +# the function to either of these solutions + +# original function (buggy): +def double(number): + return number * 3 + +print(double(10)) # returns "30" + + +# solution 1 - fix the logic: +def double(number): + return number * 2 + +print(double(10)) # returns "20" + + + +# solution 2 - rename the function: +def triple(number): + return number * 3 + +print(triple(10)) # returns "30" \ No newline at end of file From ff67f81f038271b4168e8048124942d6370ce4ad Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:27:50 +0000 Subject: [PATCH 02/19] Complete exercises for Module Tools/Sprint 5/Prep - step 2 'Type Checking with mypy' --- prep-exercises/b_type_checking.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 prep-exercises/b_type_checking.py diff --git a/prep-exercises/b_type_checking.py b/prep-exercises/b_type_checking.py new file mode 100644 index 00000000..19850044 --- /dev/null +++ b/prep-exercises/b_type_checking.py @@ -0,0 +1,48 @@ +# ------------------------ +# Exercise 1 +# ------------------------ +# Read the code to understand what it’s trying to do. +# Add type annotations to the method parameters and return types of this code. +# Run the code through mypy, and fix all of the bugs that show up. +# When you’re confident all of the type annotations are correct, and the bugs are fixed, run the code and check it works. + + + +# added type annotations to balances of dictionary[string, integer] to represent the structure needed, string to name and integer to amount, +# with the return type of none (as there is nothing to return) +def open_account(balances: dict[str, int], name: str, amount: int) -> None: + balances[name] = amount + +# added type annotation of dictionary to the accounts argument because it reflects the balances argument, +# and a return type of integer. +def sum_balances(accounts: dict[str, int]) -> int: + total = 0 + for name, pence in accounts.items(): + print(f"{name} had balance {pence}") + total += pence + return total + +# added integer type annotation and a return type of string for the output +def format_pence_as_string(total_pence: int) -> str: + if total_pence < 100: + return f"{total_pence}p" + pounds = int(total_pence / 100) + pence = total_pence % 100 + return f"£{pounds}.{pence:02d}" + +balances = { + "Sima": 700, + "Linn": 545, + "Georg": 831, +} + +# added the balances dictionary as first argument (as required above) +# and altered the amounts types from float 9.13 and string "7.13" to integers +open_account(balances, "Tobi", 913) +open_account(balances, "Olya", 713) + +total_pence = sum_balances(balances) +# corrected the function name (originally was "format_pence_as_str") +total_string = format_pence_as_string(total_pence) + +print(f"The bank accounts total {total_string}") \ No newline at end of file From 5e0ebd9132271a75a41d3602660f3f36e36a91df Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:29:29 +0000 Subject: [PATCH 03/19] Complete exercises forthe 'Comprehensive guide to mypy' article in Module Tools/Sprint 5/Prep - step 2 'Type Checking with mypy' --- prep-exercises/article_test.py | 158 +++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 prep-exercises/article_test.py diff --git a/prep-exercises/article_test.py b/prep-exercises/article_test.py new file mode 100644 index 00000000..57d4db49 --- /dev/null +++ b/prep-exercises/article_test.py @@ -0,0 +1,158 @@ +# def double(n: int) -> int: +# return n * 2 + +# num = double(21) +# print(num) + + + +from typing import List, Dict, Counter, Set, Union, Any +from math import sqrt + + +def average(nums: List[int]) -> float: + total = sum(nums) + count = len(nums) + return total / count + +print(average([1, 2, 3, 4])) # 2.5 + + +# from typing import Dict +def get_total_marks(scorecard: Dict[str, int]) -> int: + marks = list(scorecard.values()) # marks : List[int] + return sum(marks) + +scores = {'english': 84, 'maths': 92, 'history': 75} +print(get_total_marks(scores)) # 251 + + +# from typing import Counter, List +def count_occurences(data: List[float]) -> Counter[float]: + occurences = Counter(data) + return occurences + +nums = [2.5, 1.0, 7, 1, 6, 2.5, 1.0] +print(count_occurences(nums)) # Counter({1.0: 3, 2.5: 2, 7: 1, 6: 1}) + + +# from typing import List +def unique_count(nums: List[int]) -> int: + """counts the number of unique items in the list""" + uniques = set() # How does mypy know what type this is? + for num in nums: + uniques.add(num) + + return len(uniques) + +print(unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])) # 4 + + +# from typing import List, Set +def unique_count(nums: List[int]) -> int: + """counts the number of unique items in the list""" + uniques: Set[int] = set() # Manually added type information + for num in nums: + uniques.add(num) + + return len(uniques) + +print(unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])) # 4 + + +# from typing import List, Set +def unique_count(nums: List[int]) -> int: + """counts the number of unique items in the list""" + uniques: Set[int] = set() + for num in nums: + uniques.add(num) + + return len(uniques) + +counts = unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1]) + +reveal_type(counts) # The special magic reveal_type method - +# you should only use reveal_type to debug your code, and remove it when you're done debugging. + + +def print_favorite_color(person): + fav_color = person.get('favorite-color') + if fav_color is None: + print("You don't have a favorite color. 😿") + else: + print(f"Your favorite color is {fav_color}! 😸.") + +me = {'name': 'Tushar', 'favorite-color': 'Purple'} +print_favorite_color(me) + + +def print_favorite_color(person: Dict[str, str]) -> None: # added types to function definition + fav_color = person.get('favorite-color') + reveal_type(fav_color) # added this line here + if fav_color is None: + print("You don't have a favorite color. 😿") + else: + print(f"Your favorite color is {fav_color}! 😸.") + +me = {'name': 'Tushar', 'favorite-color': 'Purple'} +print_favorite_color(me) + + +def print_item(item): + if isinstance(item, list): + for data in item: + print(data) + else: + print(item) + +print_item('Hi!') +print_item(['This is a test', 'of polymorphism']) + + +def print_item(item: Union[str, List[str]]) -> None: + reveal_type(item) + + if isinstance(item, list): + for data in item: + reveal_type(item) + print(data) + else: + reveal_type(item) + print(item) + +print_item('Hi!') +print_item(['This is a test', 'of polymorphism']) + + +#a type-annotated Python implementation of the builtin function abs: +def my_abs(num: Union[int, float, complex]) -> float: + if isinstance(num, complex): + # absolute value of a complex number is sqrt(i^2 + j^2) + return sqrt(num.real ** 2 + num.imag ** 2) + + else: + return num if num > 0 else -num + +print(my_abs(-5.6)) # 5.6 +print(my_abs(42)) # 42 +print(my_abs(0)) # 0 +print(my_abs(6-8j)) # 10.0 + + +# if you ever try to run reveal_type inside an untyped function, this is what happens: +def average(nums): + total = sum(nums) + count = len(nums) + + ans = total / count + reveal_type(ans) # revealed type says it is 'Any' - 'Any' turns off type checking - so, avoid it if poss! + + +def post_data_to_api(data: Any) -> None: + requests.post('https://example.com/post', json=data) + +data = '{"num": 42, "info": null}' +parsed_data = json.loads(data) +reveal_type(parsed_data) # Revealed type is 'Any' + +post_data_to_api(data) \ No newline at end of file From 093032b171d123b83ab6af1778bbc369d392bd65 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:35:09 +0000 Subject: [PATCH 04/19] Complete exercises for Module Tools/Sprint 5/Prep - step 3 'Classes and objects' --- prep-exercises/c_classes_and_objects.py | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 prep-exercises/c_classes_and_objects.py diff --git a/prep-exercises/c_classes_and_objects.py b/prep-exercises/c_classes_and_objects.py new file mode 100644 index 00000000..58fc8a8b --- /dev/null +++ b/prep-exercises/c_classes_and_objects.py @@ -0,0 +1,36 @@ +class Person: + def __init__(self, name: str, age: int, preferred_operating_system: str): + self.name = name + self.age = age + self.preferred_operating_system = preferred_operating_system + + + +imran = Person("Imran", 22, "Ubuntu") +print(imran.name) +# print(imran.address) # mypy: Person has no attribute address + + +eliza = Person("Eliza", 34, "Arch Linux") +print(eliza.name) +# print(eliza.address) # mypy: Person has no attribute address + + +def is_adult(person: Person) -> bool: + return person.age >= 18 + + +print(is_adult(imran)) + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Write a new function in the file that accepts a Person as a parameter and tries to access +# a property that doesn’t exist. Run it through mypy and check that it does report an error. + +def is_located_in(person: Person) -> bool: + return person.is_located_in == "Manchester" + +print(is_located_in(eliza)) # prints: AttributeError: 'Person' object has no attribute 'is_located_in' + From fd85ecae57e803f803ed8c384f2e5fd58167313b Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:36:16 +0000 Subject: [PATCH 05/19] Complete exercises for Module Tools/Sprint 5/Prep - step 4 'Methods' --- prep-exercises/d_methods.py | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 prep-exercises/d_methods.py diff --git a/prep-exercises/d_methods.py b/prep-exercises/d_methods.py new file mode 100644 index 00000000..b2dc0a8d --- /dev/null +++ b/prep-exercises/d_methods.py @@ -0,0 +1,47 @@ +from datetime import date + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Q. Advantages of methods over free functions +# A. Encapsulation - Grouping the data (the attributes) and the behaviour (the methods) together +# means that the code is more organised and easier to understand. It also allows you to hide +# the internal details of how the class works to other parts of the program, so changes +# to the class can be made without affecting external code. Plus, it prevents +# accidental changes to the class's data from outside the class. + +# Ease of documentation - When you use methods, all the actions (functions) related to a specific +# thing (like a Person or a String) are grouped inside the class for that thing. This makes it +# easier to find and understand what that thing can do because everything is in one place. + +# Inheritance and reusability - methods can be inherited and overridden in subclasses, making them +# reusable and offering customization. + +# Access to state - Methods can directly access and modify the data (attributes) of an object using +# self, which free functions cannot do as they are not tied to any specific object. + +# Polymorphism - Methods support this by allowing you to define methods with the same name in +# different classes, you call the same methods on different objects but each with behaviour, +# that is specific to that class. + + +# ------------------------ +# Exercise 2 +# ------------------------ +# Change the Person class to take a date of birth (using the standard library’s datetime.date class) +# and store it in a field instead of age. Update the is_adult method to act the same as before. + +class Person: + def __init__(self, name: str, date_of_birth: date, preferred_operating_system: str): + self.name = name + self.date_of_birth = date_of_birth + self.preferred_operating_system = preferred_operating_system + + def is_adult(self) -> bool: + today = date.today() + age = today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) + return age >= 18 + +imran = Person("Imran", date(2002, 1, 12), "Ubuntu") +print(imran.is_adult()) \ No newline at end of file From 1f2718780e4c96c3463b2add2ddfc1457eab8607 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:48:19 +0000 Subject: [PATCH 06/19] Complete exercises for Module Tools/Sprint 5/Prep - step 5 'Dataclasses' --- prep-exercises/e_dataclasses.py | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 prep-exercises/e_dataclasses.py diff --git a/prep-exercises/e_dataclasses.py b/prep-exercises/e_dataclasses.py new file mode 100644 index 00000000..a5700086 --- /dev/null +++ b/prep-exercises/e_dataclasses.py @@ -0,0 +1,47 @@ +# class Person: +# def __init__(self, name: str, age: int, preferred_operating_system: str): +# self.name = name +# self.age = age +# self.preferred_operating_system = preferred_operating_system + +# imran = Person("Imran", 22, "Ubuntu") +# imran2 = Person("Imran", 22, "Ubuntu") + +# print(imran == imran2) # Prints False +# print(imran) # <__main__.Person object at 0x74b1986e2990> + +from dataclasses import dataclass +from datetime import date + +# @dataclass(frozen=True) +# class Person: +# name: str +# age: int +# preferred_operating_system: str + +# imran = Person("Imran", 22, "Ubuntu") # We can call this constructor - @dataclass generated it for us. +# print(imran) # Prints Person(name='Imran', age=22, preferred_operating_system='Ubuntu') + +# imran2 = Person("Imran", 22, "Ubuntu") +# print(imran == imran2) # Prints True + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Q.Write a Person class using @datatype which uses a datetime.date for date of birth, rather than an int for age. +# Re-add the is_adult method to it. + +@dataclass +class Person: + name: str + date_of_birth: date + preferred_operating_system: str + + def is_adult(self) -> bool: + today = date.today() + age = today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) + return age >= 18 + +imran = Person("Imran", date(2002, 1, 12), "Ubuntu") +print(imran.is_adult()) \ No newline at end of file From 7d8d652571f6e5b8100a75f719592595df35e27f Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:48:53 +0000 Subject: [PATCH 07/19] Complete exercises for Module Tools/Sprint 5/Prep - step 6 'Generics' --- prep-exercises/f_generics.py | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 prep-exercises/f_generics.py diff --git a/prep-exercises/f_generics.py b/prep-exercises/f_generics.py new file mode 100644 index 00000000..ab7f9594 --- /dev/null +++ b/prep-exercises/f_generics.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import List, Optional + +# @dataclass(frozen=True) +# class Person: +# name: str +# children: List["Person"] + +# fatma = Person(name="Fatma", children=[]) +# aisha = Person(name="Aisha", children=[]) + +# imran = Person(name="Imran", children=[fatma, aisha]) + +# def print_family_tree(person: Person) -> None: +# print(person.name) +# for child in person.children: +# print(f"- {child.name} ({child.age})") + +# print_family_tree(imran) + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Fix the above code so that it works. You must not change the print on line 17 +# - we do want to print the children’s ages. (Feel free to invent the ages of Imran’s children.) + +@dataclass(frozen=True) +class Person: + name: str + children: List["Person"] + age: Optional[int] = None # add the optional import so that age is optional with a default of None + + +fatma = Person(name="Fatma", age=13, children=[]) # order of arguments doesn't matter when named +aisha = Person("Aisha", [], 8) #however it does if you don't strictly name them + +imran = Person(name="Imran", children=[fatma, aisha]) + +def print_family_tree(person: Person) -> None: + print(person.name) + for child in person.children: + print(f"- {child.name} ({child.age})") + +print_family_tree(imran) \ No newline at end of file From ec5c6cee7dd036b624fe6ddb6ee283c90a4cc902 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:50:16 +0000 Subject: [PATCH 08/19] Complete exercises for Module Tools/Sprint 5/Prep - step 7 'Type-guided refactorings' --- prep-exercises/g_type_guided_refactorings.py | 93 ++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 prep-exercises/g_type_guided_refactorings.py diff --git a/prep-exercises/g_type_guided_refactorings.py b/prep-exercises/g_type_guided_refactorings.py new file mode 100644 index 00000000..895040b1 --- /dev/null +++ b/prep-exercises/g_type_guided_refactorings.py @@ -0,0 +1,93 @@ +from dataclasses import dataclass +from typing import List + +# @dataclass(frozen=True) +# class Person: +# name: str +# age: int +# preferred_operating_system: str + + +# @dataclass(frozen=True) +# class Laptop: +# id: int +# manufacturer: str +# model: str +# screen_size_in_inches: float +# operating_system: str + + +# def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: +# possible_laptops = [] +# for laptop in laptops: +# if laptop.operating_system == person.preferred_operating_system: +# possible_laptops.append(laptop) +# return possible_laptops + + +# people = [ +# Person(name="Imran", age=22, preferred_operating_system="Ubuntu"), +# Person(name="Eliza", age=34, preferred_operating_system="Arch Linux"), +# ] + +# laptops = [ +# Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system="Arch Linux"), +# Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="Ubuntu"), +# Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="ubuntu"), +# Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system="macOS"), +# ] + +# for person in people: +# possible_laptops = find_possible_laptops(laptops, person) +# print(f"Possible laptops for {person.name}: {possible_laptops}") + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Try changing the type annotation of Person.preferred_operating_system from str to List[str]. +# Run mypy on the code. +# It tells us different places that our code is now wrong, because we’re passing values of the wrong type. +# We probably also want to rename our field - lists are plural. Rename the field to preferred_operating_systems. +# Run mypy again. +# Fix all of the places that mypy tells you need changing. Make sure the program works as you’d expect. + +@dataclass(frozen=True) +class Person: + name: str + age: int + preferred_operating_systems: List[str] + + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: str + +# avoid variable shadowing my changing the internal variable names so that they don't confuse the reader +def find_possible_laptops(available_laptops: List[Laptop], current_person: Person) -> List[Laptop]: + matching_laptops = [] + for laptop in available_laptops: + if laptop.operating_system in current_person.preferred_operating_systems: + matching_laptops.append(laptop) + return matching_laptops + + +people = [ + Person(name="Imran", age=22, preferred_operating_systems=["Ubuntu"]), + Person(name="Eliza", age=34, preferred_operating_systems=["Arch Linux"]), +] + +laptops = [ + Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system="Arch Linux"), + Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="Ubuntu"), + Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="ubuntu"), + Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system="macOS"), +] + +for person in people: + possible_laptops = find_possible_laptops(laptops, person) + print(f"Possible laptops for {person.name}: {possible_laptops}") \ No newline at end of file From cd028dc2dc59d616f2001725b2d5ce1d72f0b3a3 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 13:55:11 +0000 Subject: [PATCH 09/19] Complete exercises for Module Tools/Sprint 5/Prep - step 8 'enums' --- prep-exercises/h_enums.py | 124 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 prep-exercises/h_enums.py diff --git a/prep-exercises/h_enums.py b/prep-exercises/h_enums.py new file mode 100644 index 00000000..7f49692e --- /dev/null +++ b/prep-exercises/h_enums.py @@ -0,0 +1,124 @@ +import sys +from dataclasses import dataclass +from enum import Enum +from typing import List +from collections import Counter + +class OperatingSystem(Enum): + MACOS = "macOS" + ARCH = "Arch Linux" + UBUNTU = "Ubuntu" + +@dataclass(frozen=True) +class Person: + name: str + age: int + preferred_operating_systems: OperatingSystem + + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + +def find_possible_laptops(available_laptops: List[Laptop], current_person: Person) -> List[Laptop]: + return [ + laptop for laptop in available_laptops + if laptop.operating_system == current_person.preferred_operating_systems + ] + +laptops = [ + Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system=OperatingSystem.ARCH), + Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), + Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), + Laptop(id=4, manufacturer="Apple", model="MacBook", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), + Laptop(id=5, manufacturer="Apple", model="MacBook Air", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), + Laptop(id=6, manufacturer="Lenovo", model="ThinkPad", screen_size_in_inches=14, operating_system=OperatingSystem.ARCH), + Laptop(id=7, manufacturer="Asus", model="ZenBook", screen_size_in_inches=13, operating_system=OperatingSystem.UBUNTU), + Laptop(id=8, manufacturer="HP", model="Spectre", screen_size_in_inches=14, operating_system=OperatingSystem.MACOS), + Laptop(id=9, manufacturer="Apple", model="MacBook Pro", screen_size_in_inches=16, operating_system=OperatingSystem.MACOS), +] + +# ------------------------ +# Exercise 1 +# ------------------------ +# Write a program which: +# - Already has a list of Laptops that a library has to lend out. +# - Accepts user input to create a new Person - it should use the input function to read a person’s name, age, +# and preferred operating system. +# - Tells the user how many laptops the library has that have that operating system. +# - If there is an operating system that has more laptops available, tells the user that if they’re willing +# to accept that operating system they’re more likely to get a laptop. +# You should convert the age and preferred operating system input from the user into more constrained types as quickly +# as possible, and should output errors to stderr and terminate the program with a non-zero exit code if the +# user input bad values. + +def user_prompt() -> Person: + try: + name = str(input("Please enter your first name: ")) + if not name.isalpha(): + raise ValueError("Name must contain only alphabetic characters.") + + age = int(input("Please enter your age: ")) + if age <= 0: + raise ValueError("Age must be positive.") + + # define valid OS options + valid_os = [os.value for os in OperatingSystem] + + # dynamically display the os options + preferred_os = input(f"Please enter correct operating system name, either {', '.join(valid_os)}: ").strip() + + # validate the OS + if preferred_os not in valid_os: + raise ValueError("Invalid operating system.") + + # convert to enum + preferred_os_enum = OperatingSystem(preferred_os) + + return Person(name=name, age=age, preferred_operating_systems=preferred_os_enum) + + # throw an error and exit for invalid age and os input + except ValueError as error: + print(f"Invalid input: {error}", file=sys.stderr) + sys.exit(1) + + + +def main() -> None: + + user_details = user_prompt() + + matching_laptops = find_possible_laptops(laptops, user_details) + + # get the counts of the laptops for an OS e.g. os_counts = {OperatingSystem.MACOS: 4,OperatingSystem.ARCH: 2,OperatingSystem.UBUNTU: 3} + os_counts = Counter(laptop.operating_system for laptop in laptops) + + print(f"{user_details.name}, there are {len(matching_laptops)} laptops available with your preferred operating system ({user_details.preferred_operating_systems.value}).") + + if matching_laptops: + print("Matching laptops:") + for laptop in matching_laptops: + print(f"- {laptop.manufacturer} {laptop.model} ({laptop.screen_size_in_inches}\" screen)") + else: + print("No laptops available with your preferred operating system.") + + # get the count of laptops for users preferred OS + user_os_count = os_counts[user_details.preferred_operating_systems] + + # find the OSes with more laptops available + alternative_os = [(os, count) for os, count in os_counts.items() if count > user_os_count] + + if alternative_os: + print("However, there are more laptops available with the following operating systems:") + for os, count in alternative_os: + print(f"- {os.value}: {count} laptops") + print("If you consider one of these options, you will have a greater chance of getting a laptop.") + else: + print("Your preferred operating system has the most laptops available.") + +if __name__ == "__main__": + main() From 56e7745bca06d57fe1fb8844c70d735f7a0d5118 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 16:32:16 +0000 Subject: [PATCH 10/19] Complete exercises for Module Tools/Sprint 5/Prep - step 9 'Inheritance' --- prep-exercises/i_inheritance.py | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 prep-exercises/i_inheritance.py diff --git a/prep-exercises/i_inheritance.py b/prep-exercises/i_inheritance.py new file mode 100644 index 00000000..a92c8a91 --- /dev/null +++ b/prep-exercises/i_inheritance.py @@ -0,0 +1,74 @@ +class Parent: + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + + def get_name(self) -> str: + return f"{self.first_name} {self.last_name}" + + +class Child(Parent): + def __init__(self, first_name: str, last_name: str): + super().__init__(first_name, last_name) + self.previous_last_names = [] + + def change_last_name(self, last_name: str) -> None: + self.previous_last_names.append(self.last_name) + self.last_name = last_name + + def get_full_name(self) -> str: + suffix = "" + if len(self.previous_last_names) > 0: + suffix = f" (née {self.previous_last_names[0]})" + return f"{self.first_name} {self.last_name}{suffix}" + +person1 = Child("Elizaveta", "Alekseeva") +print(person1.get_name()) +print(person1.get_full_name()) +person1.change_last_name("Tyurina") +print(person1.get_name()) +print(person1.get_full_name()) + +person2 = Parent("Elizaveta", "Alekseeva") +print(person2.get_name()) +print(person2.get_full_name()) +person2.change_last_name("Tyurina") +print(person2.get_name()) +print(person2.get_full_name()) + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Play computer with this code. Predict what you expect each line will do. Then run the code and check your predictions. ( +# If any lines cause errors, you may need to comment them out to check later lines). + +# Line 1: Create the class Parent +# Line 2: Define the constructor method (__init__) that takes self, a first_name value with a type of str, and last_name value with a type of str +# Line 3: Creates the first_name field of the object +# Line 4: Creates the last_name field of the object +# Line 6: Define a get_name method that returns a string +# Line 7: Returns a formatted string of the object's first_name and last_name +# Line 10: Create the subclass Child (inherits from the Parent class) +# Line 11: Define the Child class constructor method and pass in the same arguments of a first_name with a type of str, and last_name with a type of str +# Line 12: Call super() to run the Parent constructor method and set up first_name and last_name +# Line 13: Creates a previous_last_names field (which is unique to the Child and not seen in the Parent class) and sets it to an empty list +# Line 15: Define a change_last_name method that takes a new last_name of type str and doesn't return anything as annotated by the type None +# Line 16: Uses the append method to append the current last_name value to the previous_last_names list +# Line 17: Update the object's last_name field with the new last_name value that was passed in +# Line 19: Define a get_full_name method that returns a string type +# Line 20: Creates a local variable called suffix and sets it to an empty string value +# Line 21: Checks if previous_last_names list has any items (names) by checking the length (checks if it is not empty (i.e. greater than 0)) +# Line 22: If it does set the suffix variable to a formatted string that has the word 'née' first then the original last_name value of the previous last name (at the first position in the previous_last_name list) +# Line 23: Returns and formatted string of the Child object's first_name and last_name and adds the suffix at the end + +# For person1: I predict that we will see "Elizaveta Alekseeva" as the first print output for get.name() +# For get_full_name I predict we will see just "Elizaveta Alekseeva" as there has been no other last names passed to the Child as yet +# Once change_last_name has been passed a new name, I expect to see "Elizaveta Tyurina" +# For get_full_name I predict we will see "Elizaveta Tyurina (née Alekseeva)" + +# For person2: I predict that we will see "Elizaveta Alekseeva" for the get.name() +# I imagine there will be an error for print(person2.get_full_name()) because the Parent does not have a get_full_name method +# I predict it will fail for person2.change_last_name("Tyurina") for the same reason as the object is a parent object that has no change_last_name method +# By commenting out the previous 2 lines of code ,the next get.name() will repeat the first print output +# The final get_full_name output will fail again as before because this method doesn't exist in the Parent class. From 6dc57fdb8220940c608e96b85c021e4573836954 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 16:53:45 +0000 Subject: [PATCH 11/19] Fix: shadwing issue in function parpameters --- prep-exercises/b_type_checking.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/prep-exercises/b_type_checking.py b/prep-exercises/b_type_checking.py index 19850044..0e41bca7 100644 --- a/prep-exercises/b_type_checking.py +++ b/prep-exercises/b_type_checking.py @@ -22,15 +22,18 @@ def sum_balances(accounts: dict[str, int]) -> int: total += pence return total -# added integer type annotation and a return type of string for the output -def format_pence_as_string(total_pence: int) -> str: - if total_pence < 100: - return f"{total_pence}p" - pounds = int(total_pence / 100) - pence = total_pence % 100 +# added integer type annotation and a return type of string for the output and renamed the function +# parameter to avoid shadowing issue with variable name in the outer scope +def format_pence_as_string(total_pence_amount: int) -> str: + if total_pence_amount < 100: + return f"{total_pence_amount}p" + pounds = int(total_pence_amount / 100) + pence = total_pence_amount % 100 return f"£{pounds}.{pence:02d}" -balances = { +# renamed variable from 'balances' to 'account_balances' to avoid shadowing the function parameter +# 'balances' in open_account function. +account_balances = { "Sima": 700, "Linn": 545, "Georg": 831, @@ -38,10 +41,10 @@ def format_pence_as_string(total_pence: int) -> str: # added the balances dictionary as first argument (as required above) # and altered the amounts types from float 9.13 and string "7.13" to integers -open_account(balances, "Tobi", 913) -open_account(balances, "Olya", 713) +open_account(account_balances, "Tobi", 913) +open_account(account_balances, "Olya", 713) -total_pence = sum_balances(balances) +total_pence = sum_balances(account_balances) # corrected the function name (originally was "format_pence_as_str") total_string = format_pence_as_string(total_pence) From e367c1d0bb7730299fb71ec300a9a8940c2b3512 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 17:16:05 +0000 Subject: [PATCH 12/19] Fix: enforce minimum age requirement in user prompt and improve output formatting --- prep-exercises/h_enums.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/prep-exercises/h_enums.py b/prep-exercises/h_enums.py index 7f49692e..98113cb4 100644 --- a/prep-exercises/h_enums.py +++ b/prep-exercises/h_enums.py @@ -63,13 +63,15 @@ def user_prompt() -> Person: raise ValueError("Name must contain only alphabetic characters.") age = int(input("Please enter your age: ")) - if age <= 0: - raise ValueError("Age must be positive.") + minimum_age = 18 + if age < minimum_age: + raise ValueError("Age must be 18 or over.") + # define valid OS options valid_os = [os.value for os in OperatingSystem] - # dynamically display the os options + # dynamically display the os options (no need for str type here as input always returns a string) preferred_os = input(f"Please enter correct operating system name, either {', '.join(valid_os)}: ").strip() # validate the OS @@ -97,7 +99,7 @@ def main() -> None: # get the counts of the laptops for an OS e.g. os_counts = {OperatingSystem.MACOS: 4,OperatingSystem.ARCH: 2,OperatingSystem.UBUNTU: 3} os_counts = Counter(laptop.operating_system for laptop in laptops) - print(f"{user_details.name}, there are {len(matching_laptops)} laptops available with your preferred operating system ({user_details.preferred_operating_systems.value}).") + print(f"\n{user_details.name}, there are {len(matching_laptops)} laptops available with your preferred operating system ({user_details.preferred_operating_systems.value}).\n") if matching_laptops: print("Matching laptops:") @@ -113,10 +115,10 @@ def main() -> None: alternative_os = [(os, count) for os, count in os_counts.items() if count > user_os_count] if alternative_os: - print("However, there are more laptops available with the following operating systems:") + print("\nHowever, there are more laptops available with the following operating systems:") for os, count in alternative_os: print(f"- {os.value}: {count} laptops") - print("If you consider one of these options, you will have a greater chance of getting a laptop.") + print("\nIf you consider one of these options, you will have a greater chance of getting a laptop.") else: print("Your preferred operating system has the most laptops available.") From 491e97100216fd1320a952a14b09fde6e2554870 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 17:20:01 +0000 Subject: [PATCH 13/19] Fix: specify type for previous_last_names in Child class constructor --- prep-exercises/i_inheritance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prep-exercises/i_inheritance.py b/prep-exercises/i_inheritance.py index a92c8a91..6d32e830 100644 --- a/prep-exercises/i_inheritance.py +++ b/prep-exercises/i_inheritance.py @@ -10,7 +10,7 @@ def get_name(self) -> str: class Child(Parent): def __init__(self, first_name: str, last_name: str): super().__init__(first_name, last_name) - self.previous_last_names = [] + self.previous_last_names: list[str] = [] def change_last_name(self, last_name: str) -> None: self.previous_last_names.append(self.last_name) From 8aed46e987e47617114dfc674a5d6b1109e2d9d9 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 17:37:48 +0000 Subject: [PATCH 14/19] Fix: update .gitignore to include .venv and .mypy_cache; add requirements.txt for dependencies --- .gitignore | 2 ++ prep-exercises/requirements.txt | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 prep-exercises/requirements.txt diff --git a/.gitignore b/.gitignore index 3c3629e6..1da8d55a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +.venv +.mypy_cache \ No newline at end of file diff --git a/prep-exercises/requirements.txt b/prep-exercises/requirements.txt new file mode 100644 index 00000000..0d467ed9 --- /dev/null +++ b/prep-exercises/requirements.txt @@ -0,0 +1,5 @@ +librt==0.6.3 +mypy==1.19.0 +mypy_extensions==1.1.0 +pathspec==0.12.1 +typing_extensions==4.15.0 From 38406323ff6b4b378e44be0c2b781343fad82b26 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 17:42:00 +0000 Subject: [PATCH 15/19] Add requirements.txt to .gitignore --- prep-exercises/requirements.txt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 prep-exercises/requirements.txt diff --git a/prep-exercises/requirements.txt b/prep-exercises/requirements.txt deleted file mode 100644 index 0d467ed9..00000000 --- a/prep-exercises/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -librt==0.6.3 -mypy==1.19.0 -mypy_extensions==1.1.0 -pathspec==0.12.1 -typing_extensions==4.15.0 From 630ba6f3dd99d5f204ad490d6211e89fe77b582f Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 4 Dec 2025 17:44:57 +0000 Subject: [PATCH 16/19] Fix: update .gitignore to include .mypy_cache and requirements.txt --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1da8d55a..35fe5acc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .venv -.mypy_cache \ No newline at end of file +.mypy_cache +requirements.txt \ No newline at end of file From 1e064693cbb00d17f58abec2d18e08b7e05d87b2 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Sat, 6 Dec 2025 17:24:47 +0000 Subject: [PATCH 17/19] Fix: improve user input handling and type hinting for operating system counts --- prep-exercises/h_enums.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/prep-exercises/h_enums.py b/prep-exercises/h_enums.py index 98113cb4..4c1bb698 100644 --- a/prep-exercises/h_enums.py +++ b/prep-exercises/h_enums.py @@ -1,7 +1,7 @@ import sys from dataclasses import dataclass from enum import Enum -from typing import List +from typing import List, Tuple from collections import Counter class OperatingSystem(Enum): @@ -58,7 +58,7 @@ def find_possible_laptops(available_laptops: List[Laptop], current_person: Perso def user_prompt() -> Person: try: - name = str(input("Please enter your first name: ")) + name = (input("Please enter your first name: ")).strip() if not name.isalpha(): raise ValueError("Name must contain only alphabetic characters.") @@ -97,7 +97,7 @@ def main() -> None: matching_laptops = find_possible_laptops(laptops, user_details) # get the counts of the laptops for an OS e.g. os_counts = {OperatingSystem.MACOS: 4,OperatingSystem.ARCH: 2,OperatingSystem.UBUNTU: 3} - os_counts = Counter(laptop.operating_system for laptop in laptops) + os_counts: Counter[OperatingSystem] = Counter(laptop.operating_system for laptop in laptops) print(f"\n{user_details.name}, there are {len(matching_laptops)} laptops available with your preferred operating system ({user_details.preferred_operating_systems.value}).\n") @@ -111,9 +111,9 @@ def main() -> None: # get the count of laptops for users preferred OS user_os_count = os_counts[user_details.preferred_operating_systems] - # find the OSes with more laptops available - alternative_os = [(os, count) for os, count in os_counts.items() if count > user_os_count] - + # sort alternative OS so most available OS is listed first + alternative_os = sorted([(os, count) for os, count in os_counts.items() if count > user_os_count], key=lambda x: x[1], reverse=True) + if alternative_os: print("\nHowever, there are more laptops available with the following operating systems:") for os, count in alternative_os: From 8063ab740d39ff13abaa7d3b72725a8941615eb3 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Sat, 6 Dec 2025 17:26:14 +0000 Subject: [PATCH 18/19] Fix: remove unused Tuple import from h_enums.py --- prep-exercises/h_enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prep-exercises/h_enums.py b/prep-exercises/h_enums.py index 4c1bb698..d6184c7f 100644 --- a/prep-exercises/h_enums.py +++ b/prep-exercises/h_enums.py @@ -1,7 +1,7 @@ import sys from dataclasses import dataclass from enum import Enum -from typing import List, Tuple +from typing import List from collections import Counter class OperatingSystem(Enum): From 5cc2330c6840172fc5fbf1eba4a948876a9cbf53 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Fri, 2 Jan 2026 20:23:00 +0000 Subject: [PATCH 19/19] Fix: enhance user input validation for name using regular expressions. Embed minimum age variable into error message for accuracy --- prep-exercises/h_enums.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/prep-exercises/h_enums.py b/prep-exercises/h_enums.py index d6184c7f..e63e7bed 100644 --- a/prep-exercises/h_enums.py +++ b/prep-exercises/h_enums.py @@ -1,3 +1,4 @@ +import re import sys from dataclasses import dataclass from enum import Enum @@ -59,13 +60,14 @@ def find_possible_laptops(available_laptops: List[Laptop], current_person: Perso def user_prompt() -> Person: try: name = (input("Please enter your first name: ")).strip() - if not name.isalpha(): - raise ValueError("Name must contain only alphabetic characters.") + # use 're' import (regular expression) for regex validation for allowed name characters + if not re.fullmatch(r"[A-Za-z\- ]+", name): + raise ValueError("Name must contain only alphabetic characters, hyphens, or spaces.") age = int(input("Please enter your age: ")) minimum_age = 18 if age < minimum_age: - raise ValueError("Age must be 18 or over.") + raise ValueError(f"Age must be {minimum_age} or over.") # define valid OS options