From cc9697159bd191ab96c6bad7e2797f336c4c8ae2 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 19:36:06 +0000 Subject: [PATCH 1/9] why we use types task --- sprint-5/why_we_use_types.py | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 sprint-5/why_we_use_types.py diff --git a/sprint-5/why_we_use_types.py b/sprint-5/why_we_use_types.py new file mode 100644 index 000000000..3b794f1c4 --- /dev/null +++ b/sprint-5/why_we_use_types.py @@ -0,0 +1,52 @@ +#================================================================================= +# Exercise 1: Predict what double("22") will do. Then run the code and check. +# Did it do what you expected? Why did it return the value it did? +#================================================================================= +def double(value): + return value * 2 + +# Prediction: +# double("22") will return "2222" because when you multiply a string by +# an integer in Python, it concatenates the string that many times. +# So "22" * 2 will result in "22" + "22", which is "2222". + +print("Exercise 1: double('22') =", double("22")) + +# Actual output: double('22') = 2222 + +#================================================================================= +# Identify the bug in double() +#================================================================================= +def double_bug(number): + # Bug: This function is called "double" but multiplies the input by 3 instead of 2. + # This is a logic error, not a type error + return number * 3 + +print("Exercise 2:", double_bug(10)) + +#================================================================================= +# About half(), double(), second() +#================================================================================= +def half(value): + return value / 2 + +def double(value): + return value * 2 + +def second(value): + return value[1] +# Prediction and explanation: + +# half(22) --> 11.0 +# half("hello") --> TypeError, because you cannot divide a string by a number. +# half("22") --> TypeError, because you cannot divide a string by a number. + +# double_correct(22) --> 44 +# double_correct("hello") --> "hellohello", because multiplying a string by an integer concatenates it. +# double_correct("22") --> "2222" + +# second(22) --> TypeError, because you cannot index an integer. +# second(0 x 16) --> TypeError, because you cannot index an integer. +# second("hello") --> "e", because it returns the second character of the string. +# second("22") --> "2", because it returns the second character of the string. + From 732a40b825b03bcf3aefd657ba775d1cf895c955 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 19:48:51 +0000 Subject: [PATCH 2/9] Type checking with mypy --- sprint-5/Type-checking-with-mypy.py | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 sprint-5/Type-checking-with-mypy.py diff --git a/sprint-5/Type-checking-with-mypy.py b/sprint-5/Type-checking-with-mypy.py new file mode 100644 index 000000000..54e5bf67c --- /dev/null +++ b/sprint-5/Type-checking-with-mypy.py @@ -0,0 +1,40 @@ +#============================================================================== +# Do not run the following code. + +# This code contains bugs related to types. They are bugs mypy can catch. + +# Read this 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. +#============================================================================== + +def open_account(balances: dict[str, int], name: str, amount: int) -> None: + balances[name] = amount + +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 + +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: dict[str, int] = { + "Sima": 700, + "Linn": 545, + "Georg": 831, +} + +open_account(balances, "Tobi", 913) +open_account(balances, "Olya", 713) + +total_pence = sum_balances(balances) +total_string = format_pence_as_string(total_pence) + +print(f"The bank accounts total {total_string}") \ No newline at end of file From e1f95450ac1e1e7a872b08232500ffaf2712dab5 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 19:57:29 +0000 Subject: [PATCH 3/9] classes and objects task --- sprint-5/classes_and_objects.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 sprint-5/classes_and_objects.py diff --git a/sprint-5/classes_and_objects.py b/sprint-5/classes_and_objects.py new file mode 100644 index 000000000..2273bf80f --- /dev/null +++ b/sprint-5/classes_and_objects.py @@ -0,0 +1,32 @@ +# Exercise 1 — Save the above code to a file, and run it through mypy. +#Read the error, and make sure you understand what it’s telling you. + +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 error: "Person" has no attribute "address" + +eliza = Person("Eliza", 34, "Arch Linux") +print(eliza.name) +print(eliza.address) # mypy error: "Person" has no attribute "address" + +# Exercise 2 — Add the is_adult code to the file you saved earlier. + +# Run it through mypy - notice that no errors are reported - +# mypy understands that Person has a property named age so is happy with the function. + +# 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_adult(person: Person) -> bool: + return person.age >= 18 + +print(is_adult(imran)) # True + +def get_favorite_food(person: Person) -> str: + return person.favorite_food # mypy error: "Person" has no attribute "favorite_food" + From 405f2c48bf504701643ac054bcdc504cb4acafbc Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 20:06:58 +0000 Subject: [PATCH 4/9] methods task --- sprint-5/methods.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 sprint-5/methods.py diff --git a/sprint-5/methods.py b/sprint-5/methods.py new file mode 100644 index 000000000..f123ed9a7 --- /dev/null +++ b/sprint-5/methods.py @@ -0,0 +1,36 @@ +from datetime import date +# Exercise 1: +# Advantages of methods over free functions: +# 1- Methods keep related data and behavior together + +# 2- Methods make code easier to read (person.is_adult() is clearer than is_adult(person)) + +# 3- Methods reduce the number of parameters (because self already contains the data) + +# 4- Methods help mypy catch more errors (because they belong to a specific class) + +# 5- Methods make the code more object‑oriented and easier to extend + + +#Exercise 2 — Change Person to use date of birth + +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): + today = date.today() + age = today.year - self.date_of_birth.year + + has_had_birthday_this_year = ( + (today.month, today.day) >= (self.date_of_birth.month, self.date_of_birth.day) + ) + if not has_had_birthday_this_year: + age -= 1 + return age >= 18 + +imran = Person("Imran", date(2000, 1, 1), "Ubuntu") +print(imran.is_adult()) \ No newline at end of file From 49fae75b8c4ef293e9dfa619e9686e9ce6d4daff Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 20:16:28 +0000 Subject: [PATCH 5/9] add dataclasses exercise --- sprint-5/dataclasses_exercise.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 sprint-5/dataclasses_exercise.py diff --git a/sprint-5/dataclasses_exercise.py b/sprint-5/dataclasses_exercise.py new file mode 100644 index 000000000..28ba746af --- /dev/null +++ b/sprint-5/dataclasses_exercise.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from datetime import date +# exercise +# 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(frozen=True) +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 + + if(today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day): + age -= 1 + return age >= 18 + +imran = Person("Imran", date(2000, 1, 1), "Ubuntu") +imran2 = Person("Imran", date(2000, 1, 1), "Ubuntu") +print(imran.is_adult()) +print(imran == imran2) # True, because dataclasses automatically generate an __eq__ method that compares the fields of the dataclass. \ No newline at end of file From 625be12bce001cf43b799d14d2992487fb27fb45 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 20:24:54 +0000 Subject: [PATCH 6/9] generics task --- sprint-5/generics_exercise.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 sprint-5/generics_exercise.py diff --git a/sprint-5/generics_exercise.py b/sprint-5/generics_exercise.py new file mode 100644 index 000000000..b227467c0 --- /dev/null +++ b/sprint-5/generics_exercise.py @@ -0,0 +1,24 @@ +#exercise +# 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.) +from dataclasses import dataclass +from typing import List + +@dataclass(frozen=True) +class Person: + name: str + age: int + children: List["Person"] + +fatma = Person(name="Fatma", age=10, children=[]) +aisha = Person(name="Aisha", age=5, children=[]) + +imran = Person(name="Imran", age=40, 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 527becb3606cc138fdd4effffacd9115ebbec347 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 20:37:44 +0000 Subject: [PATCH 7/9] Type guided refactorings task --- sprint-5/Type-guided-refactorings.py | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 sprint-5/Type-guided-refactorings.py diff --git a/sprint-5/Type-guided-refactorings.py b/sprint-5/Type-guided-refactorings.py new file mode 100644 index 000000000..3bb5e7152 --- /dev/null +++ b/sprint-5/Type-guided-refactorings.py @@ -0,0 +1,55 @@ +#exercise +# 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. + +from dataclasses import dataclass +from typing import List + +@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 + + +def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: + possible_laptops: List[Laptop] = [] + for laptop in laptops: + if laptop.operating_system in person.preferred_operating_systems: + possible_laptops.append(laptop) + return possible_laptops + + +people = [ + Person(name="Imran", age=22, preferred_operating_systems=["Ubuntu", "Arch Linux"]), + 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 3a5ecd16d3dbbc70d04c5f5c3cfbf1b21bcbbc7d Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 21:21:44 +0000 Subject: [PATCH 8/9] enum exercise --- sprint-5/enums_exercise.py | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 sprint-5/enums_exercise.py diff --git a/sprint-5/enums_exercise.py b/sprint-5/enums_exercise.py new file mode 100644 index 000000000..beaa87773 --- /dev/null +++ b/sprint-5/enums_exercise.py @@ -0,0 +1,65 @@ +# exercise +# 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. + +from dataclasses import dataclass +from enum import Enum +from typing import List +import sys + +class OperatingSystem(Enum): + MACOS = "macOS" + ARCH = "Arch Linux" + UBUNTU = "Ubuntu" + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + +laptops: List[Laptop] = [ + 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), +] + +def convert_os(user_input: str) -> OperatingSystem: + for os_value in OperatingSystem: + if user_input.lower() == os_value.value.lower(): + return os_value + print(f"Error: '{user_input}' is not a valid operating system.", file=sys.stderr) + sys.exit(1) + +# Get user input +name = input("Enter your name: ").strip() +age_input = input("Enter your age: ").strip() +if not age_input.isdigit(): + print(f"Error: '{age_input}' age must be a number.", file=sys.stderr) + sys.exit(1) +age = int(age_input) + +os_input = input("Enter your preferred operating system (macOS, Arch Linux, Ubuntu): ").strip() +preferred_os = convert_os(os_input) + +# Count laptops with the preferred operating system +matching_laptops = [laptop for laptop in laptops if laptop.operating_system == preferred_os] +print(f"\nHi {name}, there are {len(matching_laptops)} laptops available with {preferred_os.value}.") + +# Check if there are better laptops with a different operating system +counts ={os: 0 for os in OperatingSystem} +for laptop in laptops: + counts[laptop.operating_system] += 1 + +best_os = max(counts, key=counts.get) +if best_os != preferred_os: + print(f"if you’re willing to accept {best_os.value} you’re more likely to get a laptop, as there are {counts[best_os]} available.") \ No newline at end of file From 6d3a0c7b3e64930bccde8213081e6fe8f7e90a62 Mon Sep 17 00:00:00 2001 From: Ebrahim Beiaty Date: Wed, 25 Mar 2026 21:33:08 +0000 Subject: [PATCH 9/9] inheritance task --- sprint-5/inheritance.py | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 sprint-5/inheritance.py diff --git a/sprint-5/inheritance.py b/sprint-5/inheritance.py new file mode 100644 index 000000000..c2862f63f --- /dev/null +++ b/sprint-5/inheritance.py @@ -0,0 +1,54 @@ +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) -> 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") +#first name is Elizaveta, last name is Alekseeva, no previous last names +print(person1.get_name()) +#Elizaveta Alekseeva + +print(person1.get_full_name()) +#Elizaveta Alekseeva + +person1.change_last_name("Tyurina") +#previous last name is now Alekseeva, current last name is Tyurina + +print(person1.get_name()) +#Elizaveta Tyurina + +print(person1.get_full_name()) +#Elizaveta Tyurina (née Alekseeva) + +person2 = Parent("Elizaveta", "Alekseeva") +# first name is Elizaveta, last name is Alekseeva +print(person2.get_name()) +#Elizaveta Alekseeva + +print(person2.get_full_name()) +# AttributeError: 'Parent' object has no attribute 'get_full_name' +person2.change_last_name("Tyurina") +# AttributeError: 'Parent' object has no attribute 'change_last_name' +print(person2.get_name()) +#Elizaveta Alekseeva, because the last name was not changed +print(person2.get_full_name()) +# AttributeError: 'Parent' object has no attribute 'get_full_name'