Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules
.venv/
prep
53 changes: 53 additions & 0 deletions exercises-sprint5/bank.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
def open_account(balances: dict, name: str, amount: int) -> None:
balances[name] = amount

def sum_balances(accounts: dict) -> 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}")




# When I ran mypy I got these errors:

# Error 1: bank.py:24: error: Missing positional argument "amount" in call to "open_account" [call-arg]
# The function open_account expects three arguments but only two have been passed. We need to add one more argument.

# Error 2: Argument 1 has incompatible type "str"; expected "dict"
# By adding balances as the first argument, this will be solved as well.

# Error 3: Argument 2 has incompatible type "float"; expected "str"
# 9.13 is in the wrong position, and it's a float not an int.

# Error 4: Missing positional argument "amount" in call to "open_account"
# Same problem as Error 1 missing balances and wrong types.

# Error 5: bank.py:25: error: Argument 1 to "open_account" has incompatible type "str"; expected "dict[Any, Any]"
# Line 25 has two bugs: balances should be passed as the first argument, and the third argument is passed as a string which should be an int.

# Error 6: bank.py:28: error: Name "format_pence_as_str" is not defined [name-defined]
# Typo! Should be format_pence_as_string.
21 changes: 21 additions & 0 deletions exercises-sprint5/double.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
def double(value):
return value * 2


double("22")
print(double("22"))


# Coming from JS, I predicted that Python might behave the same way.
# Under the hood, without throwing an error, Python would concatenate
# the string "22" with itself, and the result would be "2222".

# correction: later on I realised JavaScript and Python behave differently JS coerces so "22" * 2 returns 44 in JS whereas Python repeats the string according to the number, so "22" * 2 returns "2222".


def double(number):
return number * 3

print(double(10))

# As mentioned in prep section either the name should be triple or the code should be *2
180 changes: 180 additions & 0 deletions exercises-sprint5/enums.py

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To improve the user experience, you might want to do some validation on the input and ask for reentry if it is invalid. You might also want to give some options when asking for an OS

Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""
Laptop Library System
This program helps library users find available laptops matching their
preferred operating system. It demonstrates enum usage, type safety,
and input validation.
"""


from dataclasses import dataclass
from enum import Enum
from typing import List
import sys


# --------------------------------------------------------------------
# ENUM DEFINITIONS
# --------------------------------------------------------------------
"""
Represents valid operating systems for laptops.
Using enums prevents string comparison issues (case, typos, spaces).
"""

class OperatingSystem(Enum):
UBUNTU = "Ubuntu"
MACOS = "macOS"
ARCH = "Arch Linux"

# --------------------------------------------------------------------
# DATA CLASSES
# --------------------------------------------------------------------
#Represents a library user with their preferences.

@dataclass(frozen=True)
class Person:
name: str
age: int
preferred_operating_systems: OperatingSystem


#Represents a laptop available in the library.
@dataclass(frozen=True)
class Laptop:
id: int
manufacturer: str
model: str
screen_size_in_inches: float
operating_system: OperatingSystem

# --------------------------------------------------------------------
# BUSINESS LOGIC
# --------------------------------------------------------------------
"""
Filters laptops to find those matching the user's preferred OS.

Args:
laptops: List of available laptops
person: User with OS preference

Returns:
List of laptops matching the user's preferred OS
"""
def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]:
possible_laptops = []
for laptop in laptops:
if laptop.operating_system == person.preferred_operating_systems:
possible_laptops.append(laptop)
return possible_laptops


# --------------------------------------------------------------------
# MAIN PROGRAM - USER INPUT AND PROCESSING
# --------------------------------------------------------------------
# Get user input as strings first (raw input)
name = input("Enter your name: ")
age_str = input("Enter your age: ")
print("Available operating systems: Ubuntu, macOS, Arch Linux")
os_str = input("Enter your preferred OS (Ubuntu, macOS, or Arch Linux): ")

# --------------------------------------------------------------------
# INPUT VALIDATION AND CONVERSION
# --------------------------------------------------------------------

# Convert age from string to integer with error handling
# Output to stderr as per requirements, exit with non-zero code
while True:
age_str = input("Enter your age: ")
try:
age = int(age_str)
break
except ValueError:
print(f"Error: {age_str} is not a valid age. Please enter a number.")

# Convert OS string to enum with error handling
# Convert OS string to enum with error handling
os = None
while os is None:
try:
os = OperatingSystem(os_str)
except ValueError:
print(f"Error: '{os_str}' is not a valid OS. Valid options are: Ubuntu, macOS, Arch Linux", file=sys.stderr)
os_str = input("Enter preferred OS: ")


# Create Person object from validated input
# Now we know age is a valid int and os is a valid OperatingSystem
person = Person(name=name, age=age, preferred_operating_systems=os)

# --------------------------------------------------------------------
# DATA - AVAILABLE LAPTOPS
# --------------------------------------------------------------------
# Pre-defined list of laptops in the library (as per exercise requirements)
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),
]


# --------------------------------------------------------------------
# PROCESSING - FIND MATCHING LAPTOPS
# --------------------------------------------------------------------
# Find laptops matching user's preferred OS
possible_laptops = find_possible_laptops(laptops, person)

# Requirement: Tell user how many laptops have their OS
print(f"we have {len(possible_laptops)} laptops with {os.value}")


# --------------------------------------------------------------------
# COUNTING LAPTOPS PER OPERATING SYSTEM
# --------------------------------------------------------------------
# Count laptops for each OS to find which has most
arch_count = 0
ubuntu_count = 0
macos_count = 0
for laptop in laptops:
if laptop.operating_system == OperatingSystem.ARCH:
arch_count += 1
elif laptop.operating_system == OperatingSystem.UBUNTU:
ubuntu_count += 1
else:
macos_count += 1




# --------------------------------------------------------------------
# FINDING THE OPERATING SYSTEM WITH MOST LAPTOPS
# --------------------------------------------------------------------
# Store counts in dictionary for easy max calculation
os_counts = {
OperatingSystem.ARCH: arch_count,
OperatingSystem.UBUNTU: ubuntu_count,
OperatingSystem.MACOS: macos_count
}

# Find OS with maximum laptops
max_os = max(os_counts, key=os_counts.get) # Gets the OS with highest count
max_count = os_counts[max_os] # The actual count


# --------------------------------------------------------------------
# COMPARISON AND SUGGESTION LOGIC
# --------------------------------------------------------------------
# Get user's OS choice and count
os_user= person.preferred_operating_systems
user_count = os_counts[os_user]



# Requirement: Suggest alternative if another OS has MORE laptops
# Check: 1) Different OS, AND 2) Has strictly more laptops (> not >=)
if os_user != max_os and max_count > user_count:
print(f" if you're willing to accept {max_os.value} " +
f"you'd have {max_count} laptops available " +
f"(vs {user_count} for {os_user.value})")



34 changes: 34 additions & 0 deletions exercises-sprint5/generics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#✍️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

@dataclass(frozen=True)
class Person:
name: str
children: list["Person"]
age: int

fatma = Person(name="Fatma", age=7, children=[])
aisha = Person(name="Aisha", age=10, children=[])

imran = Person(name="Imran",age=50, children=[fatma, aisha])

def print_family_tree(person: Person, level: int = 0) -> None:
indent = " " * level
print(f"{indent}{person.name} ({person.age})")
for child in person.children:
print_family_tree(child, level + 1)


print_family_tree(imran)


# When I first ran mypy with `children: list`, it found no errors.
# This is because mypy didn't know what type of items were in the list.
# After changing to `children: List["Person"]` (using generics),
# mypy could identify that each child is a Person object.
# Now it caught the bug: Person has no "age" attribute.
# I fixed this by adding `age: int` to the Person class.
62 changes: 62 additions & 0 deletions exercises-sprint5/inheritance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
""" ✍️exercise
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).
"""

# PREDICTION: This defines a base class called Parent
class Parent:
# PREDICTION: Constructor that sets first_name and last_name attributes
def __init__(self, first_name: str, last_name: str):
self.first_name = first_name
self.last_name = last_name

# PREDICTION: Method that returns full name as "first last"
def get_name(self) -> str:
return f"{self.first_name} {self.last_name}"


# PREDICTION: Child class inherits everything from Parent class
class Child(Parent):
# PREDICTION: Constructor calls parent's constructor then adds new attribute
def __init__(self, first_name: str, last_name: str):
super().__init__(first_name, last_name) # PREDICTION: Calls Parent.__init__
self.previous_last_names = [] # PREDICTION: Creates empty list for this instance

# PREDICTION: Method to change last name and track previous names
def change_last_name(self, last_name) -> None:
self.previous_last_names.append(self.last_name)
self.last_name = last_name

# PREDICTION: Method that returns full name with maiden name note if changed
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}"


# PREDICTION: Creates Child instance with names "Elizaveta" "Alekseeva"
person1 = Child("Elizaveta", "Alekseeva")
# PREDICTION: Prints "Elizaveta Alekseeva" (calls inherited get_name() from Parent)
print(person1.get_name())
# PREDICTION: Prints "Elizaveta Alekseeva" (no suffix since no name change yet)
print(person1.get_full_name())
# PREDICTION: Changes last name to "Tyurina", adds "Alekseeva" to previous_last_names
person1.change_last_name("Tyurina")
# PREDICTION: Prints "Elizaveta Tyurina" (updated last name)
print(person1.get_name())
# PREDICTION: Prints "Elizaveta Tyurina (née Alekseeva)" (shows maiden name)
print(person1.get_full_name())


# PREDICTION: Creates Parent instance (NOT Child) with same names
person2 = Parent("Elizaveta", "Alekseeva")
# PREDICTION: Prints "Elizaveta Alekseeva" (Parent has get_name() method)
print(person2.get_name())
# PREDICTION: ERROR! Parent class doesn't have get_full_name() method
print(person2.get_full_name())
# PREDICTION: ERROR! Parent class doesn't have change_last_name() method
person2.change_last_name("Tyurina")
# PREDICTION: Won't reach this line due to previous error
print(person2.get_name())
# PREDICTION: Won't reach this line due to previous error
print(person2.get_full_name())
Loading