Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
175 changes: 175 additions & 0 deletions exercises-sprint5/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""
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: ")
os_str = input("Enter preferred OS: ")

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

# Convert age from string to integer with error handling
# Output to stderr as per requirements, exit with non-zero code
try:
age = int(age_str)
except ValueError:

print(f"Error: {age_str} is not a valid age", file=sys.stderr)
sys.exit(1)

# Convert OS string to enum with error handling
try:
os = OperatingSystem(os_str)
except ValueError:
print(f"Error: '{os_str}' is not a valid OS. Choose: Ubuntu, macOS, Arch Linux", file=sys.stderr)
sys.exit(1)


# 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})")



32 changes: 32 additions & 0 deletions exercises-sprint5/generics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#✍️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) -> None:
print(person.name)
for child in person.children:
print(f"- {child.name} ({child.age})")

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())
38 changes: 38 additions & 0 deletions exercises-sprint5/person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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 = date.today().year
birth_year = self.date_of_birth.year
age = today_date - birth_year


return age >= 18

imran = Person("Imran", date(2000, 6, 20), "Ubuntu")
print(imran.name)


eliza = Person("Eliza", date(1987, 12, 10), "Arch Linux")
print(eliza.name)



# when I ran mypy I got this errors:
# person.py:9: error: "Person" has no attribute "address" [attr-defined]
# person.py:13: error: "Person" has no attribute "address" [attr-defined]
# So I will remove .address line as mypy caught rightly that Person class does not have address property. It shows the benefit of using classes as earlier without defining a class mypy could not catch the same bug.


#def get_address(person: Person) -> str:
#return person.address

#print(is_adult(imran))
# When I ran mypy with is_adult function I got no error as age is a property of Person class

# however when I added get_address function and ran mypy again I got this error: person.py:27: error: "Person" has no attribute "address" [attr-defined]Found 1 error in 1 file (checked 1 source file) which was expected as Person class has no address attribute
Loading