From 7f56176ce52d34fdaa165033ea69555d04c1674b Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 12:50:53 +0100 Subject: [PATCH 01/14] add .gitignore --- prep-exercises/.gitignore | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 prep-exercises/.gitignore diff --git a/prep-exercises/.gitignore b/prep-exercises/.gitignore new file mode 100644 index 0000000..726801d --- /dev/null +++ b/prep-exercises/.gitignore @@ -0,0 +1,23 @@ +# Virtual Environment +venv/ +env/ +ENV/ + +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# mypy cache +.mypy_cache/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store + From dd0aa3bb1489bc6ee7aaeab5207ec64d18e2bb02 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 02/14] Add why we use types exercise --- prep-exercises/why_we_use_types.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 prep-exercises/why_we_use_types.py diff --git a/prep-exercises/why_we_use_types.py b/prep-exercises/why_we_use_types.py new file mode 100644 index 0000000..dc147c1 --- /dev/null +++ b/prep-exercises/why_we_use_types.py @@ -0,0 +1,29 @@ +def half(value): + return value / 2 + +def double(value): + return value * 2 + +def second(value): + return value[1] + +# Prediction: double("22") will return "2222" because * operator repeats strings +print(double("22")) + +# Testing other cases +print(half(22)) +# print(half("hello")) # This will error +# print(half("22")) # This will error + +print(double(22)) +print(double("hello")) + +# print(second(22)) # This will error +print(second("hello")) +print(second("22")) + +# The bug: this function is called double but multiplies by 3! +def double_bug(number): + return number * 3 + +print(double_bug(10)) # Should return 20 but returns 30 \ No newline at end of file From 350e5952cabfce66681914fa7afa1e5c33ecba1b Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 03/14] Add mypy type checking exercise --- prep-exercises/type_checking_with_mypy.py | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 prep-exercises/type_checking_with_mypy.py diff --git a/prep-exercises/type_checking_with_mypy.py b/prep-exercises/type_checking_with_mypy.py new file mode 100644 index 0000000..149aa06 --- /dev/null +++ b/prep-exercises/type_checking_with_mypy.py @@ -0,0 +1,34 @@ +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, +} + +# Bugs fixed: +# 1. Added balances as first argument +# 2. Converted amounts to pence (int) +# 3. Fixed function name typo +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}") From 3b72b37e0e21b80998142ec0a465274bd1d3b706 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 04/14] Add classes and objects exercise --- prep-exercises/classes_and_objects.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 prep-exercises/classes_and_objects.py diff --git a/prep-exercises/classes_and_objects.py b/prep-exercises/classes_and_objects.py new file mode 100644 index 0000000..4db603d --- /dev/null +++ b/prep-exercises/classes_and_objects.py @@ -0,0 +1,25 @@ +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 + + def is_adult(self) -> bool: + return self.age >= 18 + + +imran = Person("Imran", 22, "Ubuntu") +print(imran.name) +# print(imran.address) # Error: address is not an attribute +print(imran.is_adult()) + +eliza = Person("Eliza", 34, "Arch Linux") +print(eliza.name) +# print(eliza.address) # Error: address is not an attribute + + +# This function will error at runtime if called +def get_address(person: Person) -> str: + return person.address # Error: Person has no attribute 'address' + +# print(get_address(imran)) # Uncommenting this will cause an error From 0ab20fb5deec6e268cc7f384dda1fe346bf44a9a Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 05/14] Add methods exercise --- prep-exercises/methods.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 prep-exercises/methods.py diff --git a/prep-exercises/methods.py b/prep-exercises/methods.py new file mode 100644 index 0000000..52a0921 --- /dev/null +++ b/prep-exercises/methods.py @@ -0,0 +1,22 @@ +from datetime import date + +class Person: + def __init__(self, name: str, DoB: date, preferred_operating_system: str): + self.name = name + self.DoB = DoB + self.preferred_operating_system = preferred_operating_system + + def is_adult(self) -> bool: + current_date = date.today() + age = current_date.year - self.DoB.year - ( + (current_date.month, current_date.day) < (self.DoB.month, self.DoB.day) + ) + return age >= 18 + + + +eliza = Person("Eliza", date(2010, 5, 15), "Arch Linux") +print(f"{eliza.name} is adult: {eliza.is_adult()}") + +sara = Person("Sara", date(1995, 12, 20), "macOS") +print(f"{sara.name} is adult: {sara.is_adult()}") From f422356b893354953de38c0092e638510fbcb696 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 06/14] Add dataclasses exercise --- prep-exercises/dataclasses_exercise.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 prep-exercises/dataclasses_exercise.py diff --git a/prep-exercises/dataclasses_exercise.py b/prep-exercises/dataclasses_exercise.py new file mode 100644 index 0000000..d6cdabe --- /dev/null +++ b/prep-exercises/dataclasses_exercise.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from datetime import date + +@dataclass(frozen=True) +class Person: + name: str + date_of_birth: date + preferred_operating_system: str + + def is_adult(self) -> bool: + current_date = date.today() + age = current_date.year - self.date_of_birth.year - ( + (current_date.month, current_date.day) < (self.date_of_birth.month, self.date_of_birth.day) + ) + return age >= 18 + +# Example usage: +imran = Person("Imran", date(2001, 5, 15), "Ubuntu") +print(imran) +print(imran.is_adult()) + +eliza = Person("Eliza", date(1990, 3, 20), "Arch Linux") +print(eliza) +print(eliza.is_adult()) + From 76e32027f154d513d02d21729044f2932c538868 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 07/14] Add generics exercise --- prep-exercises/generics.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 prep-exercises/generics.py diff --git a/prep-exercises/generics.py b/prep-exercises/generics.py new file mode 100644 index 0000000..cc3b609 --- /dev/null +++ b/prep-exercises/generics.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import List + +@dataclass(frozen=True) +class Person: + name: str + age: int + children: List["Person"] + +sara = Person(name="Sara", age=5, children=[]) +ahmed = Person(name="Ahmed", age=8, children=[]) + +ali = Person(name="Ali", age=28, children=[sara]) +aya = Person(name="Aya", age=32, children=[ahmed]) + +imran = Person(name="Imran", age=55, children=[ali, aya]) + +def print_family_tree(person: Person) -> None: + print(f"{person.name} ({person.age})") + for child in person.children: + print(f" - {child.name} ({child.age})") + for grandchild in child.children: + print(f" - {grandchild.name} ({grandchild.age})") + +def count_family_members(person: Person) -> int: + count = 1 + for child in person.children: + count += count_family_members(child) + return count + +print_family_tree(imran) +print(f"\nTotal family members: {count_family_members(imran)}") From 7c1f71d852d22b3df39aa283992c74f685ab8088 Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 08/14] Add type-guided refactorings exercise --- prep-exercises/type_guided_refactorings.py | 55 ++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 prep-exercises/type_guided_refactorings.py diff --git a/prep-exercises/type_guided_refactorings.py b/prep-exercises/type_guided_refactorings.py new file mode 100644 index 0000000..e1b7561 --- /dev/null +++ b/prep-exercises/type_guided_refactorings.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +from typing import Optional + +# Before refactoring - unclear types +def process_user_data(data): + name = data[0] + age = data[1] + email = data[2] if len(data) > 2 else None + + if age < 18: + return None + + return f"{name} ({age}): {email or 'no email'}" + + +# After refactoring - clear types +@dataclass +class User: + name: str + age: int + email: Optional[str] = None + +def is_adult(user: User) -> bool: + return user.age >= 18 + +def format_user_info(user: User) -> str: + email_str = user.email if user.email else "no email" + return f"{user.name} ({user.age}): {email_str}" + +def process_user(user: User) -> Optional[str]: + if not is_adult(user): + return None + return format_user_info(user) + + +# Testing old version +print(process_user_data(("Alice", 25, "alice@example.com"))) +print(process_user_data(("Bob", 16))) + +# Testing new version +user1 = User("Alice", 25, "alice@example.com") +user2 = User("Bob", 16) + +print(process_user(user1)) +print(process_user(user2)) + + +# Another example +def get_value(key: str, data: dict[str, int]) -> Optional[int]: + return data.get(key) + +sample_data = {"a": 1, "b": 2} +result = get_value("a", sample_data) +if result is not None: + print(f"Found: {result}") From b169d9d2cfd99f14a0a45f95730157e317b1fecd Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:04 +0100 Subject: [PATCH 09/14] Add enums exercise --- prep-exercises/enums_exercise.py | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 prep-exercises/enums_exercise.py diff --git a/prep-exercises/enums_exercise.py b/prep-exercises/enums_exercise.py new file mode 100644 index 0000000..0da90ec --- /dev/null +++ b/prep-exercises/enums_exercise.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from enum import Enum + +class OperatingSystem(Enum): + MACOS = "macOS" + UBUNTU = "Ubuntu" + ARCH = "Arch Linux" + +@dataclass +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + +laptops = [ + Laptop(id=1, manufacturer="Lenovo", model="ThinkPad", screen_size_in_inches=14, operating_system=OperatingSystem.ARCH), + Laptop(id=2, manufacturer="HP", model="Pavilion", screen_size_in_inches=15.6, operating_system=OperatingSystem.UBUNTU), + Laptop(id=3, manufacturer="Asus", model="ZenBook", screen_size_in_inches=13.3, operating_system=OperatingSystem.UBUNTU), + Laptop(id=4, manufacturer="Apple", model="MacBook Air", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), +] + +@dataclass +class Person: + name: str + age: int + preferred_operating_system: OperatingSystem + + +name = input('Please enter your name: ') + +age_input = input('Please enter your age: ') +try: + age = int(age_input) +except ValueError: + print(f"Error: '{age_input}' is not a valid age. Please enter a number.") + exit() + +preferred_os_input = input('Please enter your preferred operating system: ') +try: + preferred_operating_system = OperatingSystem(preferred_os_input) +except ValueError: + available_options = ', '.join([os.value for os in OperatingSystem]) + print(f"Error: '{preferred_os_input}' is not available. Please choose from: {available_options}") + exit() + + +person = Person(name=name, age=age, preferred_operating_system=preferred_operating_system) + + +number_of_available_laptops = sum( + 1 for laptop in laptops if laptop.operating_system == person.preferred_operating_system +) + +def offer_laptop_to_user() -> None: + offer = input('Would you like a laptop with this OS (yes / no) ? ') + if offer.lower() == "y" or offer.lower() == "yes": + print('Great! Please come on monday next week to collect your laptop') + else: + print('No problem. See you later.') + +if number_of_available_laptops == 1: + print(f'There is {number_of_available_laptops} laptop available with your preferred operating system.') + offer_laptop_to_user() +elif number_of_available_laptops > 1: + print(f'There are {number_of_available_laptops} laptops available with your preferred operating system.') + offer_laptop_to_user() +else: + print('There are no laptops available with your preferred operating system.') From 8b0db4cb9e1221e61e7151e0f82b9c08df50ffca Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:05 +0100 Subject: [PATCH 10/14] Add inheritance exercise --- prep-exercises/inheritance.py | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 prep-exercises/inheritance.py diff --git a/prep-exercises/inheritance.py b/prep-exercises/inheritance.py new file mode 100644 index 0000000..c5b0acf --- /dev/null +++ b/prep-exercises/inheritance.py @@ -0,0 +1,40 @@ +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: list[str] = [] + + 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}" + + +print("Creating Child instance:") +person1 = Child("Sarah", "Johnson") +print(f"Name: {person1.get_name()}") +print(f"Full name: {person1.get_full_name()}") + +print("\nChanging last name to Smith:") +person1.change_last_name("Smith") +print(f"Name: {person1.get_name()}") +print(f"Full name: {person1.get_full_name()}") + +print("\nCreating Parent instance:") +person2 = Parent("Emma", "Wilson") +print(f"Name: {person2.get_name()}") + +# person2.change_last_name("Brown") # Error: Parent doesn't have change_last_name method From 5bb7641df2e307b393e30ceb26d0f13fa7ca11bf Mon Sep 17 00:00:00 2001 From: Alaa Date: Sat, 25 Oct 2025 13:53:16 +0100 Subject: [PATCH 11/14] Add requirements.txt --- prep-exercises/requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 prep-exercises/requirements.txt diff --git a/prep-exercises/requirements.txt b/prep-exercises/requirements.txt new file mode 100644 index 0000000..76695b3 --- /dev/null +++ b/prep-exercises/requirements.txt @@ -0,0 +1 @@ +mypy==1.18.2 From 1f45822800ca6204c5b3a805dbe81654bdca8624 Mon Sep 17 00:00:00 2001 From: Alaa Date: Wed, 29 Oct 2025 14:01:31 +0000 Subject: [PATCH 12/14] Fix double_bug function to multiply by 2 instead of 3 --- prep-exercises/why_we_use_types.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/prep-exercises/why_we_use_types.py b/prep-exercises/why_we_use_types.py index dc147c1..df4270d 100644 --- a/prep-exercises/why_we_use_types.py +++ b/prep-exercises/why_we_use_types.py @@ -22,8 +22,7 @@ def second(value): print(second("hello")) print(second("22")) -# The bug: this function is called double but multiplies by 3! def double_bug(number): - return number * 3 + return number * 2 -print(double_bug(10)) # Should return 20 but returns 30 \ No newline at end of file +print(double_bug(10)) # Returns 20 (correctly doubles the number) \ No newline at end of file From 099d93645f316798938d8d90bdd4ee5f0eda1c42 Mon Sep 17 00:00:00 2001 From: Alaa Date: Wed, 29 Oct 2025 14:18:45 +0000 Subject: [PATCH 13/14] Add an address parameter to the Person class __init__ and stored it as self.address --- prep-exercises/classes_and_objects.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/prep-exercises/classes_and_objects.py b/prep-exercises/classes_and_objects.py index 4db603d..ba6dadc 100644 --- a/prep-exercises/classes_and_objects.py +++ b/prep-exercises/classes_and_objects.py @@ -1,25 +1,25 @@ class Person: - def __init__(self, name: str, age: int, preferred_operating_system: str): + def __init__(self, name: str, age: int, preferred_operating_system: str, address: str): self.name = name self.age = age self.preferred_operating_system = preferred_operating_system + self.address = address def is_adult(self) -> bool: return self.age >= 18 -imran = Person("Imran", 22, "Ubuntu") +imran = Person("Imran", 22, "Ubuntu", "123 Main St") print(imran.name) -# print(imran.address) # Error: address is not an attribute +print(imran.address) print(imran.is_adult()) -eliza = Person("Eliza", 34, "Arch Linux") +eliza = Person("Eliza", 34, "Arch Linux", "456 Oak Ave") print(eliza.name) -# print(eliza.address) # Error: address is not an attribute +print(eliza.address) -# This function will error at runtime if called def get_address(person: Person) -> str: - return person.address # Error: Person has no attribute 'address' + return person.address -# print(get_address(imran)) # Uncommenting this will cause an error +print(get_address(imran)) From 380df709e31087ccbd402b364fbcd5b8d2c70e19 Mon Sep 17 00:00:00 2001 From: Alaa Date: Wed, 29 Oct 2025 14:28:17 +0000 Subject: [PATCH 14/14] Replace hard-coded age with dynamic calculation from date_of_birth --- prep-exercises/generics.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/prep-exercises/generics.py b/prep-exercises/generics.py index cc3b609..84aed4a 100644 --- a/prep-exercises/generics.py +++ b/prep-exercises/generics.py @@ -1,26 +1,42 @@ from dataclasses import dataclass from typing import List +from datetime import date @dataclass(frozen=True) class Person: name: str - age: int + date_of_birth: date children: List["Person"] + + def age(self) -> int: + current_date = date.today() + age = current_date.year - self.date_of_birth.year - ( + (current_date.month, current_date.day) < (self.date_of_birth.month, self.date_of_birth.day) + ) + return age -sara = Person(name="Sara", age=5, children=[]) -ahmed = Person(name="Ahmed", age=8, children=[]) +# Calculate dates of birth based on current age +def years_ago(years: int) -> date: + today = date.today() + try: + return date(today.year - years, today.month, today.day) + except ValueError: # Handle leap year edge case (Feb 29) + return date(today.year - years, today.month, today.day - 1) -ali = Person(name="Ali", age=28, children=[sara]) -aya = Person(name="Aya", age=32, children=[ahmed]) +sara = Person(name="Sara", date_of_birth=years_ago(5), children=[]) +ahmed = Person(name="Ahmed", date_of_birth=years_ago(8), children=[]) -imran = Person(name="Imran", age=55, children=[ali, aya]) +ali = Person(name="Ali", date_of_birth=years_ago(28), children=[sara]) +aya = Person(name="Aya", date_of_birth=years_ago(32), children=[ahmed]) + +imran = Person(name="Imran", date_of_birth=years_ago(55), children=[ali, aya]) def print_family_tree(person: Person) -> None: - print(f"{person.name} ({person.age})") + print(f"{person.name} ({person.age()})") for child in person.children: - print(f" - {child.name} ({child.age})") + print(f" - {child.name} ({child.age()})") for grandchild in child.children: - print(f" - {grandchild.name} ({grandchild.age})") + print(f" - {grandchild.name} ({grandchild.age()})") def count_family_members(person: Person) -> int: count = 1