From 1c36396fdf6cb9d3b824c0970139855bcd40b75f Mon Sep 17 00:00:00 2001 From: Fatma Date: Thu, 23 Oct 2025 12:28:25 +0100 Subject: [PATCH 01/11] Add exercise on why we use types with predictions and results --- why_we_use_types.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 why_we_use_types.py diff --git a/why_we_use_types.py b/why_we_use_types.py new file mode 100644 index 0000000..89dd56c --- /dev/null +++ b/why_we_use_types.py @@ -0,0 +1,32 @@ +def half(value): + return value / 2 + +def double(value): + return value * 2 + +def second(value): + return value[1] + + +print(half(22)) # My prediction was correct. It returned 11 +# print(half("hello")) # My prediction was correct. It gave an error since it is a string +# print(half("22")) # # I thought maybe Python would see it as a number and return 11 + + +print(double(22)) # My prediction was correct. It returned 44 +print(double("hello")) # I expected an error +print(double("22")) # I thought maybe Python would treat it as a number and return 44 + +# print(second(22)) # # My prediction was correct. It gave an error since it is a number +# print(second(0x16)) # # My prediction was correct. It gave an error +print(second("hello")) # My prediction was correct. It returned 'e' +print(second("22")) # My prediction was correct. It returned '2' + +def double1(number): + return number * 3 + +print(double1(10)) +# When you check the function name, it doesn’t fit what we have to expect. +# If you use this func you would expect it to give you double like 20, not 30. +# It might cause a problem for your code. + From 3ea0bc56c5f6437a04b64ddfa502fbad30f6d6b8 Mon Sep 17 00:00:00 2001 From: Fatma Date: Thu, 23 Oct 2025 12:28:51 +0100 Subject: [PATCH 02/11] Add mypy type checking exercise with type annotations and fixes --- type_checking_with_mypy.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 type_checking_with_mypy.py diff --git a/type_checking_with_mypy.py b/type_checking_with_mypy.py new file mode 100644 index 0000000..76e29a4 --- /dev/null +++ b/type_checking_with_mypy.py @@ -0,0 +1,30 @@ +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 = { + "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 5897bbeb86f71903d5af85673a4c723825ed2746 Mon Sep 17 00:00:00 2001 From: Fatma Date: Sat, 25 Oct 2025 12:16:04 +0100 Subject: [PATCH 03/11] Add classes and objects exercise with type checking examples --- classes_and_objects.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 classes_and_objects.py diff --git a/classes_and_objects.py b/classes_and_objects.py new file mode 100644 index 0000000..501049a --- /dev/null +++ b/classes_and_objects.py @@ -0,0 +1,27 @@ +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") +eliza = Person("Eliza", 34, "Arch Linux") + +print(imran.name) +# print(imran.address) # mypy error: Person has no attribute "address" + +print(eliza.name) +# print(eliza.address) # mypy error again + +def is_adult(person: Person) -> bool: + return person.age >= 18 + +print(is_adult(imran)) # no mypy error + +def print_address(person: Person) -> None: + print(person.address) # mypy will catch this too + +# I learned that a class defines what attributes each object will have. +# Mypy can check if I try to access an attribute that doesn't exist. +# It helps to avoid mistakes like typing person.addres instead of person.address. From 19d3bed60f4d0737c9ca271f6b20045deb09fe0a Mon Sep 17 00:00:00 2001 From: Fatma Date: Sat, 25 Oct 2025 12:56:22 +0100 Subject: [PATCH 04/11] Add methods exercises: advantages of methods and Person class using date_of_birth --- methods_advantages.py | 19 +++++++++++++++++++ methods_with_date_of_birth.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 methods_advantages.py create mode 100644 methods_with_date_of_birth.py diff --git a/methods_advantages.py b/methods_advantages.py new file mode 100644 index 0000000..9a043b9 --- /dev/null +++ b/methods_advantages.py @@ -0,0 +1,19 @@ +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.is_adult()) # True + +# Advantages of using methods instead of free functions: +# 1. Easier documentation - all related behaviors are attached to the class. +# 2. Encapsulation - data and logic are kept together in one place. +# 3. Easier maintenance - when class details change, methods can be updated easily. +# 4. Clearer code - you can write person.is_adult() instead of is_adult(person). \ No newline at end of file diff --git a/methods_with_date_of_birth.py b/methods_with_date_of_birth.py new file mode 100644 index 0000000..613491f --- /dev/null +++ b/methods_with_date_of_birth.py @@ -0,0 +1,21 @@ +from datetime import date + +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, 5, 15), "Ubuntu") +print(imran.is_adult()) # True + +# I learned that methods can easily adapt to changes inside a class. +# Now the class stores a date of birth instead of age, but the method still works correctly. From e9e8d65488552b23aa65ed022d8f14ecb7d7092a Mon Sep 17 00:00:00 2001 From: Fatma Date: Sat, 25 Oct 2025 15:19:53 +0100 Subject: [PATCH 05/11] Add dataclass example for Person with date_of_birth and is_adult method --- dataclasses_.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 dataclasses_.py diff --git a/dataclasses_.py b/dataclasses_.py new file mode 100644 index 0000000..fefc9ac --- /dev/null +++ b/dataclasses_.py @@ -0,0 +1,23 @@ +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: + 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, 5, 15), "Ubuntu") +imran2 = Person("Imran", date(2002, 5, 15), "Ubuntu") + +print(imran) # Person(name='Imran', date_of_birth=datetime.date(2002, 5, 15), preferred_operating_system='Ubuntu') +print(imran == imran2) # True +print(imran.is_adult()) # True From ce71241d6ea8f6b6675636155a2e280d454f85fb Mon Sep 17 00:00:00 2001 From: Fatma Date: Sat, 25 Oct 2025 16:08:35 +0100 Subject: [PATCH 06/11] Add generics exercise using List[Person] for type-safe children attribute --- generics.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 generics.py diff --git a/generics.py b/generics.py new file mode 100644 index 0000000..98ca555 --- /dev/null +++ b/generics.py @@ -0,0 +1,19 @@ +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=7, children=[]) +aisha = Person(name="Aisha", age=5, children=[]) +imran = Person(name="Imran", age=35, 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) From c287d6bd8f52347e79424931c10c99a50e6cc8cd Mon Sep 17 00:00:00 2001 From: Fatma Date: Sat, 25 Oct 2025 20:32:03 +0100 Subject: [PATCH 07/11] Use type checking to guide refactoring: change OS field to a list of preferences --- type_guided_refactorings.py | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 type_guided_refactorings.py diff --git a/type_guided_refactorings.py b/type_guided_refactorings.py new file mode 100644 index 0000000..16703b8 --- /dev/null +++ b/type_guided_refactorings.py @@ -0,0 +1,47 @@ +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 = [] + 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}") + + +# I learned how type checking helps when refactoring code. +# Mypy shows exactly which parts of the code need to change when we rename or change a field type. +# This makes large codebases easier to update safely. \ No newline at end of file From 65ea661f4fa31c08971d7ce95d80074dc0c5f95b Mon Sep 17 00:00:00 2001 From: Fatma Date: Sat, 25 Oct 2025 21:30:15 +0100 Subject: [PATCH 08/11] Add enum-based program for laptop OS preferences and user input validation --- enums.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 enums.py diff --git a/enums.py b/enums.py new file mode 100644 index 0000000..ffb81a4 --- /dev/null +++ b/enums.py @@ -0,0 +1,56 @@ +import sys +from enum import Enum +from dataclasses import dataclass + +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 = [ + 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="Apple", model="MacBook", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), + Laptop(id=4, manufacturer="Apple", model="MacBook Air", screen_size_in_inches=15, operating_system=OperatingSystem.MACOS), +] + +# user input +try: + name = input("Enter your name: ") + age = int(input("Enter your age: ")) + os_input = input("Enter your preferred operating system (macOS / Arch Linux / Ubuntu): ") + + # Convert string to enum + preferred_os = OperatingSystem(os_input) +except ValueError: + print("Invalid input. Please check your age or operating system name.", file=sys.stderr) + sys.exit(1) + +# count available laptops +matching_laptops = [l for l in laptops if l.operating_system == preferred_os] +print(f"{name}, there are {len(matching_laptops)} laptops available with {preferred_os.value}.") + +# suggest alternative OS if another has more laptops +os_counts = { + os: sum(1 for l in laptops if l.operating_system == os) + for os in OperatingSystem +} + +most_common_os = max(os_counts, key=lambda os: os_counts[os]) + +if most_common_os != preferred_os: + print(f"If you can use {most_common_os.value}, you have a higher chance of getting a laptop.") + + +# I learned how to use Enums to make my code safer and avoid typos. +# I also learned how to handle invalid user input safely using try and except. +# Using lambda helps mypy understand the type more clearly. +# Without it, mypy didn't know what os_counts.get returns. From 51af61df7132ec28176bdee642b9647ca06d50b5 Mon Sep 17 00:00:00 2001 From: Fatma Date: Sun, 26 Oct 2025 01:02:31 +0100 Subject: [PATCH 09/11] Add inheritance example with Parent and Child classes demonstrating super() and method overriding --- inheritance.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 inheritance.py diff --git a/inheritance.py b/inheritance.py new file mode 100644 index 0000000..05a4712 --- /dev/null +++ b/inheritance.py @@ -0,0 +1,48 @@ +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): + # Call the parent class constructor + 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()) # from Parent class +print(person1.get_full_name()) # from Child class + +person1.change_last_name("Tyurina") +print(person1.get_name()) +print(person1.get_full_name()) + +person2 = Parent("Elizaveta", "Alekseeva") +print(person2.get_name()) + +# The next lines will cause errors, because Parent doesn't have these methods +# print(person2.get_full_name()) +# person2.change_last_name("Tyurina") + +print(person2.get_name()) +# print(person2.get_full_name()) + + +# I learned that Parent and Child classes can have different methods — Parent can’t access methods only defined in Child +# I understood that the child class can add new methods or override existing ones + From 6820eb37ff08e97077b5ca53b24ccb854cdb6313 Mon Sep 17 00:00:00 2001 From: Fatma Date: Sun, 9 Nov 2025 21:02:56 +0000 Subject: [PATCH 10/11] chore: add explanation comment --- .gitignore | 10 ++++++++++ why_we_use_types.py | 1 + 2 files changed, 11 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c72191 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python +__pycache__/ +.mypy_cache/ +.venv/ + +# Node +node_modules/ + +# Mac +.DS_Store \ No newline at end of file diff --git a/why_we_use_types.py b/why_we_use_types.py index 89dd56c..fe01a68 100644 --- a/why_we_use_types.py +++ b/why_we_use_types.py @@ -27,6 +27,7 @@ def double1(number): print(double1(10)) # When you check the function name, it doesn’t fit what we have to expect. +# A better name would be 'triple' or 'multiply_by_three'. # If you use this func you would expect it to give you double like 20, not 30. # It might cause a problem for your code. From d542bc4062699ba333c49bb6a71f337a8fdc8816 Mon Sep 17 00:00:00 2001 From: Fatma Date: Sun, 9 Nov 2025 21:23:33 +0000 Subject: [PATCH 11/11] feat: calculate age automatically based on birth year --- generics.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/generics.py b/generics.py index 98ca555..908eafb 100644 --- a/generics.py +++ b/generics.py @@ -1,18 +1,24 @@ from dataclasses import dataclass from typing import List +from datetime import date @dataclass(frozen=True) class Person: name: str - age: int + birth_year: int children: List["Person"] -fatma = Person(name="Fatma", age=7, children=[]) -aisha = Person(name="Aisha", age=5, children=[]) -imran = Person(name="Imran", age=35, children=[fatma, aisha]) + @property + def age(self) -> int: + current_year = date.today().year + return current_year - self.birth_year + +fatma = Person(name="Fatma", birth_year=2018, children=[]) +aisha = Person(name="Aisha", birth_year=2020, children=[]) +imran = Person(name="Imran", birth_year=1990, children=[fatma, aisha]) def print_family_tree(person: Person) -> None: - print(person.name) + print(f"{person.name} ({person.age})") for child in person.children: print(f"- {child.name} ({child.age})")