Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d21625b
Complete exercises for Module Tools/Sprint 5/Prep - step 1 'Why we us…
Geraldine-Edwards Dec 4, 2025
ff67f81
Complete exercises for Module Tools/Sprint 5/Prep - step 2 'Type Chec…
Geraldine-Edwards Dec 4, 2025
5e0ebd9
Complete exercises forthe 'Comprehensive guide to mypy' article in M…
Geraldine-Edwards Dec 4, 2025
093032b
Complete exercises for Module Tools/Sprint 5/Prep - step 3 'Classes a…
Geraldine-Edwards Dec 4, 2025
fd85eca
Complete exercises for Module Tools/Sprint 5/Prep - step 4 'Methods'
Geraldine-Edwards Dec 4, 2025
1f27187
Complete exercises for Module Tools/Sprint 5/Prep - step 5 'Dataclasses'
Geraldine-Edwards Dec 4, 2025
7d8d652
Complete exercises for Module Tools/Sprint 5/Prep - step 6 'Generics'
Geraldine-Edwards Dec 4, 2025
ec5c6ce
Complete exercises for Module Tools/Sprint 5/Prep - step 7 'Type-guid…
Geraldine-Edwards Dec 4, 2025
cd028dc
Complete exercises for Module Tools/Sprint 5/Prep - step 8 'enums'
Geraldine-Edwards Dec 4, 2025
56e7745
Complete exercises for Module Tools/Sprint 5/Prep - step 9 'Inheritance'
Geraldine-Edwards Dec 4, 2025
6dc57fd
Fix: shadwing issue in function parpameters
Geraldine-Edwards Dec 4, 2025
e367c1d
Fix: enforce minimum age requirement in user prompt and improve outpu…
Geraldine-Edwards Dec 4, 2025
491e971
Fix: specify type for previous_last_names in Child class constructor
Geraldine-Edwards Dec 4, 2025
8aed46e
Fix: update .gitignore to include .venv and .mypy_cache; add requirem…
Geraldine-Edwards Dec 4, 2025
3840632
Add requirements.txt to .gitignore
Geraldine-Edwards Dec 4, 2025
630ba6f
Fix: update .gitignore to include .mypy_cache and requirements.txt
Geraldine-Edwards Dec 4, 2025
1e06469
Fix: improve user input handling and type hinting for operating syste…
Geraldine-Edwards Dec 6, 2025
8063ab7
Fix: remove unused Tuple import from h_enums.py
Geraldine-Edwards Dec 6, 2025
5cc2330
Fix: enhance user input validation for name using regular expressions…
Geraldine-Edwards Jan 2, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
node_modules
.venv
.mypy_cache
requirements.txt
42 changes: 42 additions & 0 deletions prep-exercises/a_why_use_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# ------------------------
# Exercise 1
# ------------------------
# Q. What does double("22") return? Did it do what you expected? Why did it return the value it did?
# A. I expected the result to be 44 through coercion, like JavaScript.
# It returned the string repeated i.e. 2222 because Python is a strongly typed language and doesn't use coercion

Choose a reason for hiding this comment

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

No, Python isn't a strongly typed language, it's "duck typed". However it is as you point out more sensible in how it behaves than javascript.

Copy link
Author

Choose a reason for hiding this comment

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

@OracPrime Thanks for the extra context here David :)
Every resource I went to said it was strongly typed! :D
I have done a bit more reading on the differences between dynamic, static, strong, and duck typing. For me, Python is strongly typed because it doesn’t automatically mix incompatible types like "22" + 2. Also it is dynamically typed, not statically typed, so variable types are checked at runtime and can change. I agree with you about it being duck typed though because even though it enforces type rules at runtime, it still lets objects work if the operation makes sense. :) (No wonder Python is popular!)

Choose a reason for hiding this comment

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

Static typing, duck typing, dynamic typing, .... and the madness which is javascript!


def double(input):
return input * 2

print(double("22")) # returns "2222"



# ------------------------
# Exercise 2
# ------------------------
# Q. Read the code and write down what the bug is. How would you fix it?
# A. In this function it returns the value of 30, which is the input * 3 which is logically correct, however you would
# expect the function to be called "triple" or you would expect the logic to be number * 2, so i would amend
# the function to either of these solutions

# original function (buggy):
def double(number):
return number * 3

print(double(10)) # returns "30"


# solution 1 - fix the logic:
def double(number):
return number * 2

print(double(10)) # returns "20"



# solution 2 - rename the function:
def triple(number):
return number * 3

print(triple(10)) # returns "30"
158 changes: 158 additions & 0 deletions prep-exercises/article_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# def double(n: int) -> int:
# return n * 2

# num = double(21)
# print(num)



from typing import List, Dict, Counter, Set, Union, Any
from math import sqrt


def average(nums: List[int]) -> float:
total = sum(nums)
count = len(nums)
return total / count

print(average([1, 2, 3, 4])) # 2.5


# from typing import Dict
def get_total_marks(scorecard: Dict[str, int]) -> int:
marks = list(scorecard.values()) # marks : List[int]
return sum(marks)

scores = {'english': 84, 'maths': 92, 'history': 75}
print(get_total_marks(scores)) # 251


# from typing import Counter, List
def count_occurences(data: List[float]) -> Counter[float]:
occurences = Counter(data)
return occurences

nums = [2.5, 1.0, 7, 1, 6, 2.5, 1.0]
print(count_occurences(nums)) # Counter({1.0: 3, 2.5: 2, 7: 1, 6: 1})


# from typing import List
def unique_count(nums: List[int]) -> int:
"""counts the number of unique items in the list"""
uniques = set() # How does mypy know what type this is?
for num in nums:
uniques.add(num)

return len(uniques)

print(unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])) # 4


# from typing import List, Set
def unique_count(nums: List[int]) -> int:
"""counts the number of unique items in the list"""
uniques: Set[int] = set() # Manually added type information
for num in nums:
uniques.add(num)

return len(uniques)

print(unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])) # 4


# from typing import List, Set
def unique_count(nums: List[int]) -> int:

Choose a reason for hiding this comment

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

Not sure why you've got 3 versions of this function here?

Copy link
Author

Choose a reason for hiding this comment

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

@OracPrime Haha, yes it is a bit confusing at first glance but this was some exercises from an article (https://dev.to/tusharsadhwani/the-comprehensive-guide-to-mypy-561m) that we had to complete.

"""counts the number of unique items in the list"""
uniques: Set[int] = set()
for num in nums:
uniques.add(num)

return len(uniques)

counts = unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])

reveal_type(counts) # The special magic reveal_type method -
# you should only use reveal_type to debug your code, and remove it when you're done debugging.

Choose a reason for hiding this comment

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

And if it's "remove it when you're done debugging" it should probably never be checked in to github except commented out

Copy link
Author

Choose a reason for hiding this comment

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

As before, everything in this file is from an article and we just had to work through the exercises to deepen understanding - its not any code/program that I have totally written myself. :D


def print_favorite_color(person):
fav_color = person.get('favorite-color')
if fav_color is None:
print("You don't have a favorite color. 😿")
else:
print(f"Your favorite color is {fav_color}! 😸.")

me = {'name': 'Tushar', 'favorite-color': 'Purple'}
print_favorite_color(me)


def print_favorite_color(person: Dict[str, str]) -> None: # added types to function definition

Choose a reason for hiding this comment

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

Again it's odd to have before and after methods both here and live. The second one will "win" so later code will call it, but maybe better to remove or comment out the before version

fav_color = person.get('favorite-color')
reveal_type(fav_color) # added this line here

Choose a reason for hiding this comment

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

Again, don't leave reveal_type in for anything checked in.

if fav_color is None:
print("You don't have a favorite color. 😿")
else:
print(f"Your favorite color is {fav_color}! 😸.")

me = {'name': 'Tushar', 'favorite-color': 'Purple'}
print_favorite_color(me)


def print_item(item):
if isinstance(item, list):
for data in item:
print(data)
else:
print(item)

print_item('Hi!')
print_item(['This is a test', 'of polymorphism'])


def print_item(item: Union[str, List[str]]) -> None:

Choose a reason for hiding this comment

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

Before version and reveal_types still here, too

reveal_type(item)

if isinstance(item, list):
for data in item:
reveal_type(item)
print(data)
else:
reveal_type(item)
print(item)

print_item('Hi!')
print_item(['This is a test', 'of polymorphism'])


#a type-annotated Python implementation of the builtin function abs:
def my_abs(num: Union[int, float, complex]) -> float:
if isinstance(num, complex):
# absolute value of a complex number is sqrt(i^2 + j^2)
return sqrt(num.real ** 2 + num.imag ** 2)

else:
return num if num > 0 else -num

print(my_abs(-5.6)) # 5.6
print(my_abs(42)) # 42
print(my_abs(0)) # 0
print(my_abs(6-8j)) # 10.0


# if you ever try to run reveal_type inside an untyped function, this is what happens:
def average(nums):
total = sum(nums)
count = len(nums)

ans = total / count
reveal_type(ans) # revealed type says it is 'Any' - 'Any' turns off type checking - so, avoid it if poss!


def post_data_to_api(data: Any) -> None:
requests.post('https://example.com/post', json=data)

data = '{"num": 42, "info": null}'
parsed_data = json.loads(data)
reveal_type(parsed_data) # Revealed type is 'Any'

post_data_to_api(data)
51 changes: 51 additions & 0 deletions prep-exercises/b_type_checking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ------------------------
# Exercise 1
# ------------------------
# Read the 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.



# added type annotations to balances of dictionary[string, integer] to represent the structure needed, string to name and integer to amount,
# with the return type of none (as there is nothing to return)
def open_account(balances: dict[str, int], name: str, amount: int) -> None:
balances[name] = amount

# added type annotation of dictionary to the accounts argument because it reflects the balances argument,
# and a return type of integer.
def sum_balances(accounts: dict[str, int]) -> int:
total = 0
for name, pence in accounts.items():
print(f"{name} had balance {pence}")

Choose a reason for hiding this comment

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

This is a criticism of the problem, not your solution. It's generally a really bad idea to put print statements inside functions people call to do stuff. You've no idea where this is being called from so spewing stuff to stdout is... reckless. Not your issue, problem with the problem, but worth remembering.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks David, appreciate your advice there. :)

total += pence
return total

# added integer type annotation and a return type of string for the output and renamed the function
# parameter to avoid shadowing issue with variable name in the outer scope
def format_pence_as_string(total_pence_amount: int) -> str:
if total_pence_amount < 100:

Choose a reason for hiding this comment

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

What happens if the account has a negative balance?

Copy link
Author

Choose a reason for hiding this comment

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

@OracPrime Good question! :)
For this exercise we had to add the type annotations and fix the obvious bugs, but as you suggested when I ran this with negative account balances (so that the totals equalled positive and negative numbers) it handled both scenarios correcttly as part of the calculation.

Choose a reason for hiding this comment

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

I may have missed something, but I surely if total_pence_amount is -150 then you want £-1.50 whereas it will return -150p ??? I haven't run it, I'm just reading it.

return f"{total_pence_amount}p"
pounds = int(total_pence_amount / 100)
pence = total_pence_amount % 100
return f"£{pounds}.{pence:02d}"

# renamed variable from 'balances' to 'account_balances' to avoid shadowing the function parameter
# 'balances' in open_account function.
account_balances = {
"Sima": 700,
"Linn": 545,
"Georg": 831,
}

# added the balances dictionary as first argument (as required above)
# and altered the amounts types from float 9.13 and string "7.13" to integers
open_account(account_balances, "Tobi", 913)
open_account(account_balances, "Olya", 713)

total_pence = sum_balances(account_balances)
# corrected the function name (originally was "format_pence_as_str")
total_string = format_pence_as_string(total_pence)

print(f"The bank accounts total {total_string}")
36 changes: 36 additions & 0 deletions prep-exercises/c_classes_and_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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: Person has no attribute address


eliza = Person("Eliza", 34, "Arch Linux")
print(eliza.name)
# print(eliza.address) # mypy: Person has no attribute address


def is_adult(person: Person) -> bool:
return person.age >= 18


print(is_adult(imran))


# ------------------------
# Exercise 1
# ------------------------
# 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_located_in(person: Person) -> bool:
return person.is_located_in == "Manchester"

print(is_located_in(eliza)) # prints: AttributeError: 'Person' object has no attribute 'is_located_in'

47 changes: 47 additions & 0 deletions prep-exercises/d_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from datetime import date


# ------------------------
# Exercise 1
# ------------------------
# Q. Advantages of methods over free functions
# A. Encapsulation - Grouping the data (the attributes) and the behaviour (the methods) together
# means that the code is more organised and easier to understand. It also allows you to hide
# the internal details of how the class works to other parts of the program, so changes
# to the class can be made without affecting external code. Plus, it prevents
# accidental changes to the class's data from outside the class.

# Ease of documentation - When you use methods, all the actions (functions) related to a specific
# thing (like a Person or a String) are grouped inside the class for that thing. This makes it
# easier to find and understand what that thing can do because everything is in one place.

# Inheritance and reusability - methods can be inherited and overridden in subclasses, making them
# reusable and offering customization.

# Access to state - Methods can directly access and modify the data (attributes) of an object using
# self, which free functions cannot do as they are not tied to any specific object.

# Polymorphism - Methods support this by allowing you to define methods with the same name in
# different classes, you call the same methods on different objects but each with behaviour,
# that is specific to that class.


# ------------------------
# Exercise 2
# ------------------------
# Change the Person class to take a date of birth (using the standard library’s datetime.date class)
# and store it in a field instead of age. Update the is_adult method to act the same as before.

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, 1, 12), "Ubuntu")
print(imran.is_adult())
47 changes: 47 additions & 0 deletions prep-exercises/e_dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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")
# imran2 = Person("Imran", 22, "Ubuntu")

# print(imran == imran2) # Prints False
# print(imran) # <__main__.Person object at 0x74b1986e2990>

from dataclasses import dataclass
from datetime import date

# @dataclass(frozen=True)
# class Person:
# name: str
# age: int
# preferred_operating_system: str

# imran = Person("Imran", 22, "Ubuntu") # We can call this constructor - @dataclass generated it for us.
# print(imran) # Prints Person(name='Imran', age=22, preferred_operating_system='Ubuntu')

# imran2 = Person("Imran", 22, "Ubuntu")
# print(imran == imran2) # Prints True


# ------------------------
# Exercise 1
# ------------------------
# Q.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
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, 1, 12), "Ubuntu")
print(imran.is_adult())
Loading