0% found this document useful (0 votes)
2 views73 pages

OOP Coursework

The document outlines the coursework for the MSc Information Technology program, specifically focusing on Object Oriented Programming. It includes a declaration of academic integrity, a detailed table of contents covering various Python programming topics, and practical exercises related to Python syntax, data types, and programming concepts. The coursework emphasizes collaborative learning and adherence to university regulations regarding plagiarism and self-plagiarism.

Uploaded by

officer07nasty
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views73 pages

OOP Coursework

The document outlines the coursework for the MSc Information Technology program, specifically focusing on Object Oriented Programming. It includes a declaration of academic integrity, a detailed table of contents covering various Python programming topics, and practical exercises related to Python syntax, data types, and programming concepts. The coursework emphasizes collaborative learning and adherence to university regulations regarding plagiarism and self-plagiarism.

Uploaded by

officer07nasty
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

School of Computing, Engineering and Physical Sciences

MSc Information Technology


COMP11124 Object Oriented Programming

GROUP COURSEWORK (50% of the marks)


Session Oct-2025/2026 Term 2

Students
1. STUDENT NAME (BANNER)

Lecturer: Michael Lin


Submission Date: 15-04-2026
Declaration

I have carefully read and fully understand Regulations 3.49–3.55 of Chapter 3 of the
Regulatory Framework of the University of the West of Scotland, which outline the rules and
policies regarding cheating and plagiarism.
I confirm that this assessment is the collective work of our group, consisting of five
members, with each member contributing to the completion of this task. Where applicable,
we have clearly referenced and acknowledged the contributions or ideas of others outside
our group.
I also affirm that no part of this assessment has been written, in whole or in part, by anyone
outside the group, except for explicitly referenced sources.
Furthermore, I confirm that this assessment has not been submitted previously, either
partially or fully, for any other module or academic purpose, ensuring it does not fall under
self-plagiarism.
This declaration is made with honesty and integrity in adherence to the University’s
academic standards.

2
Table of Contents
Week 1 - Introduction to Python Programming Language.......................................................6
Introduction to Python.........................................................................................................6
Understanding Python Syntax.......................................................................................... 6
Getting Started..................................................................................................................... 6
Installing the Python Interpreter and Editor.....................................................................6
Core Concepts and Practical Exercises..................................................................................8
Exercise 1: Variables, Types, and Casting..........................................................................8
Exercise 2: Arithmetic Operators......................................................................................9
Exercise 3: Strings and f-Strings......................................................................................10
Final Task: Temperature Converter Program...................................................................11
Week 2 - Introduction to Python Programming II...................................................................12
Expanding Our Python Fundamentals................................................................................12
Comparisons and Conditional Statements.........................................................................12
Implementation: Comparing Temperatures...................................................................13
Python Lists: Data Structures and Manipulation................................................................13
Python Loops......................................................................................................................14
Obtaining User Input.......................................................................................................... 16
Week 3 - Python Functions, Scope, and Errors.......................................................................19
Introduction to Modular Programming..............................................................................19
Python Functions................................................................................................................19
Returning Values and Mathematical Computations.......................................................20
Variable Scope and Assertions........................................................................................21
Identifying and Fixing Common Errors...............................................................................21
Final Project: To-Do List Manager.......................................................................................22
Week 4 - Object Oriented Programming: Classes and Objects...............................................25
Class:.................................................................................................................................. 25
Object:................................................................................................................................25
Classes Overview................................................................................................................28
Summary............................................................................................................................ 29
Portfolio 1 and 2 Solution...................................................................................................30
Week 5 - Inheritance & Polymorphism...................................................................................38
Inheritance......................................................................................................................... 38

3
Polymorphism.................................................................................................................... 38
Multiple Inheritance...........................................................................................................39
Conceptual Example for Multiple Inheritance:...............................................................39
Polymorphism Details.........................................................................................................41
Types of polymorphic functions:....................................................................................41
Kwargs............................................................................................................................ 42
Args................................................................................................................................ 43
Generics (Duck Typing)...................................................................................................43
Week 6 - Programming Paradigms......................................................................................... 45
Procedural Programming....................................................................................................45
Functional Programming....................................................................................................46
Characteristics................................................................................................................ 46
Object-Oriented Programming (OOP).................................................................................47
Characteristics................................................................................................................ 47
Key Concepts.................................................................................................................. 47
Portfolio Exercise Lab Week 6 (Implementing Persistence)................................................50
Week 7 - SOLID Principles and Python Exceptions.................................................................56
The SOLID Principles...........................................................................................................56
Python Exception Handling.................................................................................................... 57
Putting It Together: Refactoring the To-Do App..............................................................57
Week 8 - Debugging, Properties, and Persistence..................................................................62
System Debugging and Error Tracing..................................................................................62
Implementation: Fixing the Vehicle Simulator Bug.........................................................63
Python Properties using the @property Decorator............................................................63
Implementing Persistence with CSV...................................................................................64
Implementation: The CSV Data Access Object................................................................64
Week 9 - Data Structures & Abstract Classes..........................................................................67
What is a Data Structure?...................................................................................................67
Types of Data Structures.................................................................................................67
Abstract Classes..................................................................................................................70
Portfolio Exercise 6......................................................................................................... 71

4
Table of Figures
Figure 1 Visual Studio Code Installation...................................................................................7
Figure 2 Python VS Code Extension..........................................................................................7
Figure 3 Multiple Inheritance.................................................................................................40
Figure 4 Debugging in Jupyter Notebook...............................................................................62
Figure 5 Debugging options....................................................................................................62

5
Week 1 - Introduction to Python
Programming Language
Introduction to Python
Python is a high-level, dynamic, and incredibly versatile programming language utilized
globally across domains such as web development, artificial intelligence, data analytics, and
workflow automation. The practical lab exercises presented in this document serve as our
primary introduction to Python. By successfully engaging with and completing this week’s
class, we aim to implement our very first Python procedural programs and build a robust
understanding of how to interpret and write using Python syntax.

Understanding Python Syntax


The syntax of Python is highly regarded for its readability and clean structure. Unlike other
programming languages that rely on curly braces { } to define code blocks, Python enforces
the use of whitespace and indentation. This design philosophy forces developers to write
neatly organized code. Furthermore, Python is dynamically typed, meaning we are not
required to strictly declare data types before execution; the interpreter infers them at
runtime.

Getting Started
Before we can begin drafting code, we must construct our development environment by
setting up the necessary software components.

Installing the Python Interpreter and Editor


We first need to install the Python interpreter before we start with any programming in
Python. We navigated directly to the official Python website to download and install the
current iteration of the interpreter.
To write any Python code we will first need a code editor. For this module, we decided to use
the Visual Studio Code (VS Code) editor developed by Microsoft. This specific code editor
does not only support Python, but it also supports reading and editing a range of other file
types and programming languages. We downloaded and installed the software on our local
machines. Upon opening the application for the first time, VS Code prompted us to install
the Python extension, which we confirmed and installed to unlock code suggestions and
execution features.

6
Figure 1 Visual Studio Code Installation

Figure 2 Python VS Code Extension

7
Executing Python Scripts
During our setup phase, we learned that a Python script can be executed in multiple ways,
yielding the same output results.
1. Using the VS Code Interface: You can run the file in VSCode by clicking the “Play”
button at the top right hand corner.
2. Using the Command Line Interface (CLI): We can use the command line to run our
Python scripts as well. We open our terminal window, navigate to the directory
where our script is saved using the cd command, and then type the execution
command. For example, the command to run our lab file is lab_week_1.ipynb.

Core Concepts and Practical Exercises


The following sections document our group's solutions to the week's practical programming
tasks, encompassing topics such as variables, operators, print statements, casting, and
formatted strings.

Exercise 1: Variables, Types, and Casting


A variable is a named location in memory used to store data. We can assign a value to a
variable using the assignment operator =. Python inherently supports various data types,
including integers, floats, and strings. In this exercise, we utilized the type() function to check
the data type of our variables, and we used the int(), float(), and str() functions to
successfully convert (cast) values between different data types.
Below is the code we wrote to define unique variables, check their designated types, and
perform data casting.

# Defining our initial variables with distinct types


is_session_active = True
participant_count = 1
mathematical_pi_approx = 3.14159
welcome_phrase = "Hello World"

# Using the type() function to verify and print the data types
print(type(is_session_active))
print(type(participant_count))
print(type(mathematical_pi_approx))
print(type(welcome_phrase))

# Initializing variables for our casting exercise


base_numeric_val = 5
decimal_measurement = 5.5
logical_flag = True

print("Initial states:", base_numeric_val, decimal_measurement,


logical_flag)

8
# Casting the variables to alter their data types
casted_to_float_val = float(base_numeric_val)
casted_to_integer_val = int(decimal_measurement)
casted_bool_to_int_val = int(logical_flag)

# Printing the mutated results


print("Casted states:", casted_to_float_val,
casted_to_integer_val, casted_bool_to_int_val)

Output:

Exercise 2: Arithmetic Operators


Python provides several arithmetic operators for performing basic mathematical operations.
These include Addition (+), Subtraction (-), Multiplication (*), Division (/), Floor Division (//),
Modulus (%), and Exponentiation (**).
We practically applied these mathematical operators to calculate the average of two distinct
numbers and compute the total area of a predefined rectangle.

# Task: Calculating the Average of two numbers


math_exam_score = 85
science_exam_score = 92

# Using addition and division operators


calculated_mean_score = (math_exam_score + science_exam_score) /
2

print("The scores provided are:", math_exam_score, "and",


science_exam_score)
print("The calculated average is:", calculated_mean_score)

# Task: Calculating the Area of a Rectangle


plot_length_meters = 15.5
plot_width_meters = 10.0

# Using the multiplication operator

9
total_plot_area = plot_length_meters * plot_width_meters

print("The dimensions of the rectangle are:", plot_length_meters,


"by", plot_width_meters)
print("The calculated area is:", total_plot_area)
Output:

Exercise 3: Strings and f-Strings


Each variable that contains a string is an Object and has built-in methods (that are actions
that it can perform). For this task, we accessed the official documentation to modify our
string casings and swap specific words. Additionally, we utilized f-strings (formatted string
literals). F-Strings allow you to evaluate expressions within a string. When we declare an f-
string, we preface the quotation marks with the letter f and encapsulate our dynamic
variables inside curly brackets {expression}.

# Task: Modifying Strings utilizing built-in methods


module_overview_text = "This class covers OOP."
print("Original Text Sequence:", module_overview_text)

# Applying methods to alter the string properties


text_in_uppercase = module_overview_text.upper()
text_in_lowercase = module_overview_text.lower()
updated_module_text = module_overview_text.replace("OOP", "Object
Oriented Programming")
character_count = len(module_overview_text)

print("Converted to Uppercase:", text_in_uppercase)


print("Converted to Lowercase:", text_in_lowercase)
print("With Replaced Text:", updated_module_text)
print("Total String Length:", character_count)

# Task: Utilizing f-Strings for structured outputs


scholar_first_name = "Alex"
registered_courses_total = 4
university_site = "Main Campus"

# Building the dynamic string


formatted_biography = f"My name is {scholar_first_name} and I am
studying {registered_courses_total} classes in
{university_site}."

10
print(formatted_biography)
Output:

Final Task: Temperature Converter Program


In this culminating exercise, we developed a dedicated program that allows the user to
convert temperatures from Celsius to Fahrenheit and Kelvin. To achieve this, we
independently located the mathematical formulas required for the conversions and
implemented them strictly using standard Python arithmetic operators. We then generated
a highly formatted output block.

# Temperature Converter Logic Implementation

# The base Celsius value assigned to a numeric variable


temp_in_celsius_val = 25.0

# Formula for Fahrenheit: (Celsius * 9/5) + 32


computed_temp_fahrenheit = (temp_in_celsius_val * 9/5) + 32

# Formula for Kelvin: Celsius + 273.15


computed_temp_kelvin = temp_in_celsius_val + 273.15

# Generating the requested formatted output via f-strings


print("Welcome to the Temperature Converter!")
print(f"The temperature you have entered is {temp_in_celsius_val}
degree Celsius.")
print("Converted Temperatures:")
print(f"{temp_in_celsius_val} degree Celsius is equal to
{computed_temp_fahrenheit} Fahrenheit.")
print(f"{temp_in_celsius_val} degree Celsius is equal to
{computed_temp_kelvin} Kelvin.")
print("Thank you for using the Temperature Converter!")
Output:

11
Week 2 - Introduction to Python
Programming II
Expanding Our Python Fundamentals
Following our initial setup and exploration of basic syntax, our group continued our
introduction to programming by exploring more complex Python constructs. The primary
goal for this week's coursework was to understand how to build dynamic programs that can
handle conditional routing, iterate through data sets using loops, manage collections of
items via lists, and successfully pause execution to accept and process user input.

Comparisons and Conditional


Statements
In dynamic programming, the ability to evaluate conditions and execute specific logic based
on those evaluations is fundamental. This is achieved through comparison operators, logical
operators, and conditional statements.
1. Comparison Operators: Comparison operators are used to compare two values. In
Python, these operations always return a Boolean value (True or False).

Operato Description Example


r

== Equal to 5 == 5 (True)

!= Not equal to 5 != 3 (True)

> Greater than 10 > 5 (True)

< Less than 2 < 8 (True)

>= Greater than or equal to 5 >= 5 (True)

<= Less than or equal to 4 <= 7 (True)

2. Logical Operators: Logical operators allow for the combination of multiple Boolean
expressions to create complex conditions:
a. and: Returns True only if both operands are true.
b. or: Returns True if at least one operand is true.
c. not: Reverses the Boolean result (e.g., not True becomes False).
3. Conditional Statements (if, elif, else): Python uses these statements to control the
flow of execution. Code blocks within these statements must be indented to indicate
they belong to that specific condition.
a. if Statement: The primary check. If the condition is True, the indented code
block executes.

12
b. elif (else-if): Acts as a secondary check. It is only evaluated if the preceding if
(or elif) condition fails.
c. else: The "catch-all" statement. It executes only if none of the previous
conditions in the chain were met.

Implementation: Comparing Temperatures


To put these concepts into practice, our group authored a script that defined two distinct
temperature readings and utilized an if-else block to evaluate their parity.
In the following code block, we declare two float variables representing different times of
the day, compare them using the strict equality operator, and output a dynamic response
based on the evaluation.

# Defining our temperature variables


morning_reading_celsius = 12.5
afternoon_reading_celsius = 15.0

print(f"Comparing {morning_reading_celsius}°C and


{afternoon_reading_celsius}°C:")

# Evaluating the condition using an if-else structure


if morning_reading_celsius == afternoon_reading_celsius:
print("The recorded temperatures are exactly equal.")
else:
print("The recorded temperatures are not equal. There has
been a change in weather.")
Output:

Python Lists: Data Structures and


Manipulation
When managing multiple related data points, lists provide a more efficient alternative to
creating individual variables. A Python list is a versatile and mutable (changeable) data
structure used to store ordered collections of items.
1. Core Characteristics:
a. Syntax: Lists are defined by enclosing elements in square brackets: colors =
["Red", "Blue", "Green"].
b. Zero-Based Indexing: Python uses a zero-based system, meaning the first
element is at index 0, the second at index 1, and so on.
c. Mutability: Unlike some other data types, lists can be modified after they are
created.

13
2. Essential Operations: To manage the collection effectively, we utilized several built-in
Python techniques:
a. Slicing: Used to extract a specific sub-range of elements.
i. Syntax: list[start:end] (Note: the end index is exclusive).
b. Reassignment: Modifying an existing item by targeting its specific index (e.g.,
colors[0] = "Crimson").
c. Appending: Using the .append() method to add a single new element to the
very end of the structure.
d. Membership Testing: Using the in keyword to verify if a specific item exists
within the list.
e. Length Checking: Using the len() function to determine the total number of
items in the collection.
3. Practical Implementation: The following script demonstrates these concepts by
managing a collection of paint colors:

# 1. Initialize the list


paint_colors = ["Azure", "Beige", "Charcoal", "Denim"]

# 2. Update the first element (Index Reassignment)


paint_colors[0] = "Alabaster"

# 3. Conditional Membership Testing


if "Charcoal" in paint_colors:
print("Color found in inventory.")

# 4. Slicing a sub-list
# Extracts elements from index 1 up to (but not including) index
3
selected_shades = paint_colors[1:3]

# 5. Programmatic Addition
paint_colors.append("Emerald")
Output:

Python Loops
To automate repetitive tasks, Python offers looping mechanisms. Our group explored two
primary loop structures:
1. While Loops: These loops continuously execute a block of code as long as a specified
condition remains true. We noted that failing to update the evaluation variable inside
the loop results in a fatal infinite loop.

14
2. For Loops: These loops are designed to iterate sequentially over a collection of items,
such as our previously discussed lists.
We paired our for loops with the built-in range() function, which rapidly generates a series of
numbers within a designated boundary. To establish fine-grained control over our iterations,
we practiced implementing the break keyword (to immediately terminate the loop entirely)
and the continue keyword (to skip the remainder of the current iteration and proceed to the
next).
Implementation: Even Numbers, Sum of Squares, and Countdowns
We consolidated the three loop-based tasks into a single execution flow. This involved
filtering numbers using the modulus operator, utilizing mathematical exponents inside a
bounded range, and managing a decrementing variable for a while loop.
This code segment demonstrates iterating through a static list to identify even values,
calculating an accumulated sum of squared integers, and executing a while loop that ticks
down to zero.

print("--- Even Numbers Task ---")


# Filtering a sequence using the modulo operator to find zero
remainders
digit_sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for current_digit in digit_sequence:
if current_digit % 2 == 0:
print(f"Even number identified: {current_digit}")

print("\n--- Sum of Squares Task ---")


# Accumulating values via the range() function (1 to 5 inclusive
requires range(1, 6))
accumulated_square_total = 0
for base_val in range(1, 6):
accumulated_square_total += (base_val ** 2)
print("The final sum of squares is:", accumulated_square_total)

print("\n--- Countdown Task ---")


# Utilizing a while loop to decrement a value until a condition
is met
mission_timer = 10
while mission_timer > 0:
print(f"T-Minus: {mission_timer}")
mission_timer -= 1 # Decrementing to prevent an infinite
loop
print("Liftoff!")
Output:

15
Obtaining User Input
A program is vastly more useful when it dynamically interacts with its user. We utilized
Python's native input() function, which pauses script execution and waits for the user to
type a response via the console. Because the input() function inherently treats all received
data as strings, we learned the crucial step of type-casting (e.g., using int() or float())
whenever mathematical operations are required on the user's data.
Implementation: Interactive Age Categorization and Advanced Temperature Converter
To complete our coursework, we tackled the advanced tasks. First, we categorized a user's
age based on strict numerical thresholds. Following that, we achieved the extra task criteria
by building a highly dynamic temperature converter that prompts the user for both the
source unit (C, F, or K) and the numerical value, subsequently applying the correct
mathematical formulas through a nested if-elif structure.
The following code requires terminal interaction. It captures user inputs, casts them to their
appropriate data types, and routes the data through conditional logic to produce custom
outputs.

# --- Interactive Age Categorizer ---


print("--- Age Categorizer ---")
user_age_str = input("Please enter your current age: ")
# Casting string input to integer for numerical comparisons
evaluated_age = int(user_age_str)

if evaluated_age < 18:


print("You are a minor.")
elif evaluated_age >= 18 and evaluated_age <= 65:
print("You are an adult.")
else:

16
print("You are a senior citizen.")

# --- Advanced Interactive Temperature Converter ---


print("\n--- Universal Temperature Converter ---")
# Prompting for the unit type
source_metric = input("What temperature unit are you starting
with? (Enter C, F, or K): ").upper()
# Prompting for the numeric value and casting it immediately to a
float
source_temp_value = float(input("Enter the numerical temperature
value: "))

print(f"\nProcessing conversion for


{source_temp_value}°{source_metric}...")

if source_metric == "C":
calc_f = (source_temp_value * 9/5) + 32
calc_k = source_temp_value + 273.15
print(f"Fahrenheit: {calc_f}°F")
print(f"Kelvin: {calc_k}K")

elif source_metric == "F":


calc_c = (source_temp_value - 32) * 5/9
calc_k = calc_c + 273.15
print(f"Celsius: {calc_c:.2f}°C")
print(f"Kelvin: {calc_k:.2f}K")

elif source_metric == "K":


calc_c = source_temp_value - 273.15
calc_f = (calc_c * 9/5) + 32
print(f"Celsius: {calc_c:.2f}°C")
print(f"Fahrenheit: {calc_f:.2f}°F")

else:
print("Error: Unrecognized unit. Please restart and use C, F,
or K.")
Output:

17
18
Week 3 - Python Functions, Scope,
and Errors
Introduction to Modular Programming
As our group progressed into the third week of our Python coursework, we shifted our focus
from writing linear scripts to developing modular, reusable, and robust code. The core
objective of this week's laboratory exercises was to master the implementation of custom
functions, understand variable scope, implement assertions for quality assurance, and
systematically identify and fix common programming errors. The culmination of this week's
effort was the collaborative development of a fully functional To-Do List application.

Python Functions
A function represents a dedicated block of code engineered to perform a specific task only
when explicitly called. This prevents code duplication and drastically improves readability.
Throughout our exercises, we identified several key characteristics of functions:
 Definition: Functions are declared utilizing the def keyword followed by a unique
identifier and parentheses.
 Parameters vs. Arguments: Parameters are the temporary variables defined within
the function's signature, whereas arguments are the actual data values injected into
the function upon execution.
 Keyword Arguments & Default Values: Python allows us to pass arguments explicitly
by name (bypassing strict positional order) and establish default fallback values
within the parameter list if an argument is omitted by the user.
Implementation: Iterating Through Collections via Functions
To demonstrate our comprehension, we constructed a function designed to iterate through
a predefined list of names and output a personalized greeting for each entry.
The code block below defines a function that accepts a list as a parameter, utilizes a standard
for loop to traverse the collection, and prints a formatted greeting using an f-string.

# Defining the function with a single parameter


def welcome_peers(peer_roster):
for individual_peer in peer_roster:
print(f"Hello {individual_peer}!")

# Establishing our list of names


study_group_members = ["John", "Jane", "Jack"]

# Calling the function and passing our list as the argument


welcome_peers(study_group_members)

19
Output:

Returning Values and Mathematical Computations


Functions are not limited to merely executing commands, they can securely compute data
and pass the resulting values back to the main program flow utilizing the return keyword.
We applied this concept to build two distinct financial calculation tools.
The first tool processes flat tax deductions based on predefined parameters. The second tool
calculates complex compound interest, featuring integrated data validation to prevent
mathematical logical errors (such as negative durations or out-of-bound interest rates).
The following segment demonstrates value returns, parameter validation, and loop-based
mathematical compounding.

# --- Flat Tax Calculation Tool ---


def compute_income_tax(gross_earnings, deduction_rate):
calculated_deduction = gross_earnings * deduction_rate
return calculated_deduction

final_tax_burden = compute_income_tax(50000, 0.2)


print(f"The calculated tax burden is: £{final_tax_burden}")

# --- Compound Interest Calculation Tool ---


def evaluate_investment_growth(base_capital, investment_years,
annual_yield):
# Validating input parameters to prevent logical errors
if annual_yield < 0 or annual_yield > 1:
print("Please enter a decimal number between 0 and 1")
return None
if investment_years < 0:
print("Please enter a positive number of years")
return None

# Utilizing a for loop to track annual growth


for current_yr in range(investment_years + 1):
annual_total = base_capital * (1 + annual_yield) **
current_yr
print(f"The total amount of money earned by the
investment in year {current_yr} is £{annual_total:.2f}")

# Returning the final computed value as a casted integer


return int(annual_total)

20
# Executing the function with the assignment parameters
final_portfolio_value = evaluate_investment_growth(1000, 5, 0.03)
print(f"\nFinal returned integer value: £
{final_portfolio_value}")
Output:

Variable Scope and Assertions


During our group discussions, we identified variable scope as a critical architectural concept.
Variables initialized strictly inside a function possess a local scope, meaning they cannot be
accessed or manipulated by the global program. Conversely, variables initialized in the main
program possess a global scope and can be referenced widely.
To ensure our custom functions performed reliably against expected baselines, we utilized
Python assertions. The assert keyword validates that a condition evaluates to true during
execution; if it fails, it violently raises an AssertionError, forcefully stopping the program. We
implemented an assertion to guarantee our compound interest function resolved exactly to
1159 based on the predefined inputs.
The script below safely runs the assertion test against our previously defined function.

# Utilizing an assertion to validate our function's mathematical


integrity
print("Running automated assertion check...")

# If the function does not equal 1159, the program will crash
here
assert evaluate_investment_growth(1000, 5, 0.03) == 1159

print("Assertion passed successfully. The logic is mathematically


sound.")
Output:

21
Identifying and Fixing Common Errors
A significant portion of our lab time was dedicated to debugging intentional semantic errors.
Python raises explicit flags when a program violates strict constraints. Our group reviewed
the broken code snippets, identified the core issues, and refactored the code to run
smoothly.
 Syntax Error Fix: We corrected the misspelled pritn built-in function back to print.
 Name Error Fix: We initialized the variable favorite_color before attempting to call it.
 Value Error Fix: We type-casted the string "5" into an int() to permit mathematical
addition.
 Index Error Fix: We requested index 1 to fetch the second array element, avoiding
the out-of-bounds index 3.
 Indentation Error Fix: We properly indented the conditional print statement using
the tab key to align it within the if block.

# 1. Fixed Syntax Error


print("Hello, World!")

# 2. Fixed Name Error (Defined the missing variable)


favorite_color = "Blue"
print("My favorite color is", favorite_color)

# 3. Fixed Value Error (Casted string to integer for proper


mathematical operation)
first_digit_str = "5"
second_digit_int = 3
corrected_sum_result = int(first_digit_str) + second_digit_int
print("The sum is:", corrected_sum_result)

# 4. Fixed Index Error (Accessed a valid index to fetch 'banana')


available_fruits = ["apple", "banana", "cherry"]
print("Fetched Fruit:", available_fruits[1])

# 5. Fixed Indentation Error (Properly nested the print block)


current_hour = 11
if current_hour < 12:
print("Good morning!")
Output:

22
Final Project: To-Do List Manager
As our task for Week 3, we engineered a fully functional, interactive To-Do List application.
This program aggregates everything we have learned: while loops for continuous execution,
conditional if-elif statements for menu routing, list manipulation for data storage, and
custom functions to handle dedicated operations.
The code below establishes an empty global list and relies on custom functions triggered by
a main application loop. It features input sanitation to prevent crashes if a user attempts to
remove an item that does not exist.

# Initializing our global storage structure


active_tasks_list = []

# Function to append a new entry to the end of the array


def register_new_task():
new_entry = input("\nEnter the description of the new task:
")
active_tasks_list.append(new_entry)
print(f"Success: '{new_entry}' has been added to your
docket.")

# Function to safely display all active entries


def display_current_tasks():
print("\n--- Current Task Docket ---")
if len(active_tasks_list) == 0:
print("Your docket is currently empty.")
else:
# Utilizing a loop with an integrated counter for
readability
for index, task_item in enumerate(active_tasks_list):
print(f"Task {index + 1}: {task_item}")
print("---------------------------")

# Function to locate and remove a specific entry based on string


matching
def strike_task():
target_entry = input("\nEnter the exact name of the task to
remove: ")
if target_entry in active_tasks_list:
active_tasks_list.remove(target_entry)
print(f"Success: '{target_entry}' has been deleted.")
else:
print(f"Error: The task '{target_entry}' does not exist
in your docket.")

# --- Main Program Loop ---

23
# This block continuously prompts the user until they explicitly
trigger the 'break' command
print("Initializing To-Do List Manager...")
while True:
print("\n--- Main Menu ---")
print("1. Add a new task")
print("2. View all tasks")
print("3. Remove a task")
print("4. Quit Application")

user_menu_selection = input("Please enter your choice (1-4):


")

if user_menu_selection == "1":
register_new_task()
elif user_menu_selection == "2":
display_current_tasks()
elif user_menu_selection == "3":
strike_task()
elif user_menu_selection == "4":
print("\nShutting down To-Do Manager. Goodbye!")
break
else:
print("Invalid choice. Please verify your input and try
again.")
Output:

24
25
Week 4 - Object Oriented
Programming: Classes and Objects
In Python, classes and objects are fundamental concepts used to implement object-oriented
programming. A class serves as a blueprint for creating objects, which are instances of that
class. Here’s a breakdown of each concept with examples.

Class:
A class in Python is a blueprint that defines attributes (data) and methods (functions) that
characterize a specific type of object. Think of a class as a template for something you want
to create. For example, if you want to represent a "Dog," you’d create a `Dog` class with
attributes like `name` and `breed`, and methods like `bark()`.

Object:
An object is an instance of a class. When you create an object, you are creating an individual
instance of the class with unique attribute values. Each object is distinct but follows the
blueprint defined by its class.
Example code for a Dog class and objects

class Dog:
# Constructor method to initialize attributes
def __init__(self, name, breed):
[Link] = name # Attribute: name of the dog
[Link] = breed # Attribute: breed of the dog

# Method to make the dog bark


def bark(self):
return f"{[Link]} says Woof"

# Creating Objects:
# Creating two Dog objects with different names and breeds
dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Milo", "Beagle")

# Accessing attributes and methods of each object


print([Link])
print([Link])
print([Link]())
print([Link]())

class Dog:

26
# Constructor method to initialize attributes
def __init__(self, name, breed):
[Link] = name # Attribute: name of the dog
[Link] = breed # Attribute: breed of the dog

# Method to make the dog bark


def bark(self):
return f"{[Link]} says Woof"

# Creating Objects:
# Creating two Dog objects with different names and breeds
dog1 = Dog("Buddy", "Golden Retriever")
dog2 = Dog("Milo", "Beagle")

# Accessing attributes and methods of each object


print([Link])
print([Link])
print([Link]())
print([Link]())

Output:

Python by-default supports multiple inheritance. When a class is derived from more than
one base class is called Multiple Inheritance. The derived class inherits all the features and
properties that base class has.
Explanation:
 `Dog` is a class that defines the attributes `name` and `breed` and has a method
`bark()`.
 `dog1` and `dog2` are objects (instances) of the `Dog` class.
 `dog1` has `name = "Buddy"` and `breed = "Golden Retriever"`.
 `dog2` has `name = "Milo"` and `breed = "Beagle"`.
 Both `dog1` and `dog2` use the `bark()` method, but their output differs based on
their `name` attribute
Here is an example of a Python program that implements multiple inheritance using animal-
related classes. This program includes a base class called Animal, derived classes such as

27
Flying, Swimming, and Walking, and a hybrid class named Penguin that combines multiple
behaviours
Another Example: Class and Object in Practice

class Animal:
"""
The base class for all animals.

Attributes:
name (str): Name of the animal.
"""

def __init__(self, name: str):


[Link] = name

def breathe(self) -> str:


"""Indicates that the animal is breathing."""
return f"{[Link]} is breathing."

class Flying:
"""
A mixin class representing flying capability.
"""

def fly(self) -> str:


"""Indicates that the animal can fly."""
return f"{[Link]} is flying high in the sky!"

class Swimming:
"""
A mixin class representing swimming capability.
"""

def swim(self) -> str:


"""Indicates that the animal can swim."""
return f"{[Link]} is swimming in the water!"

class Walking:
"""
A mixin class representing walking capability.
"""

def walk(self) -> str:

28
"""Indicates that the animal can walk."""
return f"{[Link]} is walking on land!"

class Penguin(Animal, Swimming, Walking):


"""
A specific animal class for penguins, inheriting from Animal,
Swimming, and Walking.
"""

def __init__(self, name: str):


super().__init__(name)

def who_am_i(self) -> str:


"""Returns a description of the penguin."""
return f"I am {[Link]}, a penguin! I can swim and walk
but cannot fly."

# Example Usage
if __name__ == "__main__":
peng = Penguin("Peng")
print([Link]())
print([Link]())
print([Link]())
print(peng.who_am_i())

Output:

Classes Overview
1. Animal:
o Base class for all animals.
o Contains the breathe method to indicate the animal's breathing behavior.
o Attributes:
 name (str): The name of the animal.
2. Flying:
o Mixin class representing the ability to fly.

29
o Contains the fly method to indicate flying behavior.
o Assumes the name attribute exists (provided by another parent class).
3. Swimming:
o Mixin class representing the ability to swim.
o Contains the swim method to indicate swimming behavior.
o Assumes the name attribute exists (provided by another parent class).
4. Walking:
o Mixin class representing the ability to walk.
o Contains the walk method to indicate walking behavior.
o Assumes the name attribute exists (provided by another parent class).
5. Penguin:
o Combines functionality from Animal, Swimming, and Walking.
o Contains the who_am_i method to describe the penguin's unique features.
o Overrides the __init__ method to initialize the animal's name.

Summary
Key Features of Object-Oriented Programming
1. Encapsulation: Combining data and methods within a class to protect the integrity of
the data.
2. Abstraction: Concealing complex implementation details from the user to simplify
interaction.
3. Inheritance: Allowing a class to inherit properties and behaviors from another class,
promoting code reuse.
4. Polymorphism: Enabling methods to behave differently based on the object that calls
them.

30
Portfolio 1 and 2 Solution

view_options(): This function displays a menu of options for interacting with the task
management program.
propagate_task_list(task_list: TaskList) -> TaskList: This function populates the TaskList with
a set of sample tasks for testing or demonstration purposes.
main(): This is the primary function that runs the task management application. It initializes
a TaskList, populates it with sample tasks, and presents the user with options to manage
tasks interactively.
Docstrings: Each function has a docstring that describes its purpose, parameters, and return
types, providing a clear guide for understanding and modifying the code.
Type Hints: Type hints like -> None and -> TaskList specify expected return types for each
function, improving readability and helping with debugging.
Task
Explanation of Methods
 __init__: Initializes a Task object with a title, due date, and description. The
date_created attribute is set to the current date and time when the task is created,
and completed is initially set to False.
 change_description: Allows modification of the task's description.
 mark_completed: Marks the task as completed by setting the completed attribute to
True.
 change_title: Allows changing the title of the task.
 __str__: Provides a formatted string representation of the task’s key attributes,
including title, creation date, due date, completion status, and description.

Tasklist
Explanation of Methods

31
 __init__: Initializes the TaskList for a specified owner and sets up an empty tasks list
to hold individual tasks.
 add_task: Adds a Task object to the tasks list.
 remove_task: Removes a task at the specified index x. If the index is invalid (either
too low or too high), an error message is printed.
 view_tasks: Displays the list of tasks along with their indexes. Each task is printed in
the format defined by the __str__ method of the Task class, which helps provide
detailed task information.

Below is the screenshot of implementing description, change_description() and


view_overdue_tasks() methods in the portrfolio project.

import datetime

class Task:
#

def __init__(self, title: str, date_due: [Link],


description: str):
[Link] = title
[Link] = False
self.date_due = date_due
self.date_created = [Link]()
[Link] = description

def change_date_due(self, date_due: str):


self.date_due = date_due

def change_description(self, change_description: str):


[Link] = change_description

def mark_completed(self):

[Link] = True

def change_title(self, new_title: str):


[Link] = new_title

def __str__(self):

# status = "Completed" if [Link] else "Not


Completed"
if [Link]:
status = "Completed"
else:

32
status = "Not Completed"

return (f"{[Link]} (created: {self.date_created},


due: {self.date_due}, completed: {[Link]},"
f"description:{[Link]}")

[Link]

from task import Task


import datetime

class TaskList:

def __init__(self, owner):

[Link] = None
[Link] = owner
# [Link] = []
[Link] = []

def add_task(self, task: Task):


[Link](task)
# [Link](date_created)

def remove_task(self, x: int):


if 0 <= x <= len([Link]):
del [Link][x]
else:
print("Invalid index: Try again")

def view_tasks(self):
for i, items in enumerate([Link]):
print(i, items)

def view_overdue_tasks(self):
new_date = [Link]()
overdue_task = [
task
for task in [Link]
if task.date_due < new_date and not [Link]
]
if overdue_task:
print("This tasks are Over-due")
for i, items in enumerate(overdue_task):

33
print(i, items)
else:
print("No over-due task available")

Implementation in [Link]

from users import Owner


from task import Task
from tasklist import TaskList
from taskdao import TaskCsvDAO
import datetime

def view_options():
print("1. Add a new task to the list.")
print("2. View the current tasks in the list.")
print("3. Remove a task from the list.")
print("4. Mark a task as completed or \n Change due Date or
Change description).")
print("5. Change the title of a task.")
print("6. Task Overdue.")
print("7. Quit and exit the program.")

def list_options(task_list: TaskList) -> TaskList:


# Pre-populating with the tasks shown in your example output
base_time = [Link](2024, 11, 20, 15, 40, 24)
tasks = [
("Buy groceries", base_time - [Link](days=4),
"Buy groceries"),
("Do laundry", base_time + [Link](days=2),
"Do laundry"),
("Clean room", base_time - [Link](days=1),
"Clean room"),
("Do homework", base_time + [Link](days=3),
"Do homework"),
("Walk dog", base_time + [Link](days=5),
"Walk dog"),
("Do dishes", base_time + [Link](days=6), "Do
dishes"),
]
for title, due, desc in tasks:
task = Task(title, due, desc)
task_list.add_task(task)
return task_list

def main():
owner = Owner("Group4", "Group4@[Link]")

34
task_list = TaskList(owner)

# Load or initialize tasks


storage_path = "[Link]"
task_dao = TaskCsvDAO(storage_path)

try:
task_list.tasks = task_dao.get_all_tasks()
if not task_list.tasks:
task_list = list_options(task_list)
except FileNotFoundError:
task_list = list_options(task_list)

while True:
view_options()
choice = input("Please make a selection from (1-6): ")

if choice == '1':
new_task = input("Add your task please: ")
create_date = input("Set the date (YYYY-mm-dd): ")
describe = input("Write a description: ")
new_date = [Link](create_date,
"%Y-%m-%d")
task_list.add_task(Task(new_task, new_date,
describe))

elif choice == '2':


task_list.view_tasks()

elif choice == '3':


index = int(input("Select task to remove: "))
task_list.remove_task(index)

elif choice == '4':


task_list.view_tasks()
action = input("Select a task to perform (Complete a
task/ Change due Date/Change description)?: ").lower()

idx = int(input(f"Which of the above tasks


{[Link]()[-1]} do you want to change:"))
selected_task = task_list.tasks[idx]

if "complete" in action:
selected_task.mark_completed()
elif "due date" in action:

35
new_date_str = input("Type the new Date (YYYY-mm-
dd):")

selected_task.change_date_due([Link](new_date
_str, "%Y-%m-%d"))
elif "description" in action:
new_desc = input("Type the new description:")
selected_task.change_description(new_desc)

elif choice == '5':


task_list.view_tasks()
idx = int(input("Which of the above tasks do you want
to change:"))
new_title = input("Type the new title:")
task_list.tasks[idx].change_title(new_title)

elif choice == '6':


print("This tasks are Over-due")
task_list.view_overdue_tasks()

elif choice == '7':


task_dao.save_all_tasks(task_list.tasks)
print("System ends")
break

if __name__ == "__main__":
main()

Output:

1. Add a new task to the list.


2. View the current tasks in the list.
3. Remove a task from the list.
4. Mark a task as completed or
Change due Date or Change description).
5. Change the title of a task.
6. Task Overdue.
7. Quit and exit the program.
Please make a selection from (1-6): 4
0 Buy groceries (created: 2024-11-20 15:40:24.187931, due: 2024-
11-16 15:40:24.187931, completed: False,description:Buy groceries
1 Do laundry (created: 2024-11-20 15:40:24.187931, due: 2024-11-
22 15:40:24.187931, completed: False,description:Do laundry
2 Clean room (created: 2024-11-20 15:40:24.187931, due: 2024-11-
19 15:40:24.187931, completed: False,description:Clean room

36
3 Do homework (created: 2024-11-20 15:40:24.187931, due: 2024-11-
23 15:40:24.187931, completed: False,description:Do homework
4 Walk dog (created: 2024-11-20 15:40:24.187931, due: 2024-11-25
15:40:24.187931, completed: False,description:Walk dog
5 Do dishes (created: 2024-11-20 15:40:24.187931, due: 2024-11-26
15:40:24.187931, completed: False,description:Do dishes
Select a task to perform (Complete a task/ Change due Date/Change
description)?: Change description
Which of the above tasks description do you want to change:1
Type the new description:Do Laundry every saturday
1. Add a new task to the list.
2. View the current tasks in the list.
3. Remove a task from the list.
4. Mark a task as completed or
Change due Date or Change description).
5. Change the title of a task.
6. Task Overdue.
7. Quit and exit the program.
Please make a selection from (1-6): 2
0 Buy groceries (created: 2024-11-20 15:40:24.187931, due: 2024-
11-16 15:40:24.187931, completed: False,description:Buy groceries
1 Do laundry (created: 2024-11-20 15:40:24.187931, due: 2024-11-
22 15:40:24.187931, completed: False,description:Do Laundry every
saturday
2 Clean room (created: 2024-11-20 15:40:24.187931, due: 2024-11-
19 15:40:24.187931, completed: False,description:Clean room
3 Do homework (created: 2024-11-20 15:40:24.187931, due: 2024-11-
23 15:40:24.187931, completed: False,description:Do homework
4 Walk dog (created: 2024-11-20 15:40:24.187931, due: 2024-11-25
15:40:24.187931, completed: False,description:Walk dog
5 Do dishes (created: 2024-11-20 15:40:24.187931, due: 2024-11-26
15:40:24.187931, completed: False,description:Do dishes
1. Add a new task to the list.
2. View the current tasks in the list.
3. Remove a task from the list.
4. Mark a task as completed or
Change due Date or Change description).
5. Change the title of a task.
6. Task Overdue.
7. Quit and exit the program.
Please make a selection from (1-6): 6
This tasks are Over-due
0 Buy groceries (created: 2024-11-20 15:40:24.187931, due: 2024-
11-16 15:40:24.187931, completed: False,description:Buy groceries
1 Clean room (created: 2024-11-20 15:40:24.187931, due: 2024-11-
19 15:40:24.187931, completed: False,description:Clean room

37
1. Add a new task to the list.
2. View the current tasks in the list.
3. Remove a task from the list.
4. Mark a task as completed or
Change due Date or Change description).
5. Change the title of a task.
6. Task Overdue.
7. Quit and exit the program.
Please make a selection from (1-6):

In the output above, the description for the first task “Do laundry was Do laundry”
After option 4 was selected the system asked about the task to be performed. “Change
description was selected from the options” and the system asked again which task’s
description to be changed.
Task 1 do laundry was selected and the descripton was changed from “Do laundry” to ” Do
Laundry every saturday ”
Task option 6 also retrieved all overdue tasks.

38
Week 5 - Inheritance &
Polymorphism
In Week 5, we explored two of the core pillars of Object-Oriented Programming (OOP):
Inheritance and Polymorphism.

Inheritance
Inheritance is a powerful mechanism that allows a new class (the child or derived class) to
acquire the properties and behaviors of an existing class (the parent or base class). It is a
foundational OOP concept designed to promote code reusability, allowing developers to
leverage existing functionality and modify specific behaviors as needed. Notably, Python
natively supports Multiple Inheritance, a feature that is not available in many other popular
programming languages.

Polymorphism
Polymorphism translates to having "many forms." It refers to the ability to use the same
method or function name across different types of inputs or objects. While Python handles
polymorphism differently than strictly typed languages like Java or C# (it does not support
traditional method overloading), its dynamic nature allows functions to accept various
parameter types and return corresponding value types seamlessly.
Example Code for Inheritance

# Base class
class Vehicle:
def __init__(self, colour: str, weight: int, max_speed: int,
max_range: int | None = None, seats: int | None = None):
[Link] = colour
[Link] = weight
self.max_speed = max_speed
self.max_range = max_range
[Link] = seats

def move(self, speed: int):


print(f"The vehicle is moving at {speed} km/h")

def get_total_seats(self):
if [Link] is not None:
return [Link]
return 0

# Child class. Car inherits from Vehicle


class Car(Vehicle):

39
def __init__(self, colour: str, weight: int, max_speed: int,
car_type: str, fuel_capacity: int = 0, max_range=None,
seats=None):
# Calling the parent class constructor
super().__init__(colour, weight, max_speed, max_range,
seats)
self.car_type = car_type
self.fuel_capacity = fuel_capacity

def move(self, speed: int):


print(f"The {self.car_type} is driving at {speed} km/h")

# Creating instances of the Car class


electric_car = Car("yellow", 1200, 180, "Sedan", fuel_capacity=0,
max_range=330, seats=4)
electric_car.move(95)

petrol_car = Car("green", 1800, 270, "SUV", fuel_capacity=45,


max_range=845, seats=4)
petrol_car.move(220)

print(f"Total fuel capacity: {petrol_car.fuel_capacity} liters")


print(f"Total seats of the petrol car is:
{petrol_car.get_total_seats()}")

In this example, we define two classes: Vehicle and Car. The Vehicle acts as the base class,
serving as a starting template. The Car is inherited from the Vehicle class, meaning it
instantly gains all the properties and methods of the base class. When initializing the Car
class, we use super().__init__() to properly initialize the inherited parent attributes. Notice
how the move() function is overridden in the child class with custom print logic—this is a
classic practical use of inheritance.
Output:

Multiple Inheritance
By default, Python allows a class to inherit from more than one base class—a concept known
as Multiple Inheritance. The derived child class successfully absorbs all the features,
methods, and properties of every base class it is linked to.

40
Conceptual Example for Multiple Inheritance:
Imagine an Aquatic class that contains a swim method, and a Reptile class that contains a
walk method. If we create a Frog class that inherits from both Aquatic and Reptile, that Frog
object will be able to execute both the swim and walk actions.

Figure 3 Multiple Inheritance

Example Code for Multiple Inheritance

class Aquatic:
def __init__(self, name: str, fins: int):
[Link] = name
[Link] = fins
[Link] = "Aquatic"

def swim(self):
print(f"{[Link]} is swimming under water with
{[Link]} fins")

class Reptile:
def __init__(self, name: str, limbs: int):
[Link] = name
[Link] = limbs

def walk(self):
print(f"{[Link]} is walking on the ground with
{[Link]} legs")

# Frog inherits from both Aquatic and Reptile


class Frog(Aquatic, Reptile):
def __init__(self):
# Explicitly calling the Aquatic class constructor

41
Aquatic.__init__(self, "Frog", 2)
# Explicitly calling the Reptile class constructor
Reptile.__init__(self, "Frog", 4)

def jump(self):
print(f"{[Link]} is jumping on the ground with
{[Link]} limbs")

frog1 = Frog()

[Link]() # Inherited from the Aquatic class


[Link]() # Inherited from the Reptile class
[Link]() # Unique to the Frog class
Output:

This script translates the conceptual multiple inheritance picture into code. Because the Frog
class inherits from both Aquatic and Reptile, it successfully calls both swim() and walk(),
while also introducing its own unique jump() method.

Polymorphism Details
Polymorphism ensures that the same function or method name can adapt to different data
types or varying numbers of arguments.

Types of polymorphic functions:


1. User-Defined: Custom functions we write ourselves to adapt to different scenarios
based on specific project requirements.
2. Pre-Defined: Built-in Python functions that natively support multiple data types right
out of the box.
Example Code for User-Defined Function

def Addition(a, b):


# Check if the parameters are either ints or floats
if (type(a) == float or type(a) == int) and (type(b) == float
or type(b) == int):
return a + b
# Check if the parameters are strings containing numbers,
then cast to float
elif type(a) == str and type(b) == str and [Link]() and
[Link]():
return float(a) + float(b)

42
# Fallback: if data types are invalid, return -1 as an error
code
else:
return -1

print("\nUser defined functions:")


print("Addition of 2 standard numbers: ", Addition(1, 2))
print("Addition of 2 string numbers: ", Addition("10", "20"))
Output:

The Addition function acts differently depending on the input provided. It evaluates the
arguments and makes a processing decision: if the parameters are integers or floats, it
simply adds them. If they are numeric strings, it safely converts them to floats before adding.
If an unsupported data type is passed (like a boolean or list), it defaults to returning -1 to
indicate an invalid parameter.
Example Code for Pre-Defined Function

# Pre-defined functions
print("\nPre-Defined functions:")
# Takes a single argument
print("Hello")
# Takes multiple arguments of completely different types
print("Hello", 1234, False)

# The type() function naturally accepts varying arguments


print("Type:", type(123))
print("Type:", type("abc"))
print("Type:", type(True))
Output:

The Python interpreter is full of pre-defined polymorphic functions. The print() and type()
functions are perfect examples; they seamlessly accept everything from single string

43
arguments to multiple mixed-type parameters (ints, floats, bools, lists) without throwing
errors.

Kwargs
**kwargs is a special syntax allowing you to pass an arbitrary number of named (keyword)
arguments to a function. The function intercepts them and packages them into a dictionary.
This is particularly useful when passing settings or optional configurations.
Syntax & Example Code:

def multiple_args(**kwargs):
print("Type of **kwargs:", type(kwargs))
print(kwargs)

multiple_args(name="Prajwal", age=24)
Output:

Args
Similar to kwargs, *args is used to pass a variable number of unnamed arguments. Instead of
a dictionary, the function receives them as a sequence (tuple). You simply pass in comma-
separated values.

def sum_numbers(*args):
total = 0
for num in args:
total += num
return total

print("Sum:", sum_numbers(1, 2, 3, 4))


Output:

Generics (Duck Typing)


While Python does not feature strict function overloading, it relies heavily on generics and
"duck typing." If different objects share the same method signature, Python expects them to
perform identically when called, regardless of the underlying class differences.

class Dog:
def make_sound(self):
print("Dog is barking")

class Cat:

44
def make_sound(self):
print("Cat is meowing")

class Wolf:
def make_sound(self):
print("Wolf is howling")

# Grouping different objects that share the same method name


objects = [Dog(), Cat(), Wolf()]

for obj in objects:


obj.make_sound() # Operates flawlessly even though the object
classes are different
Output:

Learning Outcome
In Week 5, we focused on two of the most critical elements of Object-Oriented
Programming: Inheritance and Polymorphism.
 Inheritance: Inheritance grants a child class the ability to adopt the features and
methods of a parent class, significantly streamlining code reuse. For instance, a Car
class can inherit common traits (like speed or color) from a Vehicle class while
establishing its own unique characteristics. Python goes a step further by supporting
multiple inheritance, allowing a Frog to combine the abilities of an Aquatic parent
and a Reptile parent.

 Polymorphism: This concept dictates that a single method name can operate across
different data types. A great example is Python’s print() function, which intuitively
handles text, numbers, and arrays. While Python bypasses the traditional method
overloading seen in languages like C#, it keeps functions highly versatile through
dynamic typing.

 *args and **kwargs: We learned specialized ways to make function inputs flexible:
o *args gathers multiple unnamed arguments into a sequence.
o **kwargs gathers named arguments into a dictionary.
 Generics: Python's flexible nature means that different class objects can safely share
method names. Grouping a Dog, Cat, and Wolf together is perfectly valid; calling
make_sound() on each will trigger their respective behaviors automatically.

45
In short, we successfully learned how to implement inheritance for scalability, utilize
polymorphism for flexibility, and leverage specialized argument syntax for adaptable code.

46
Week 6 - Programming Paradigms
Programming paradigms dictate the fundamental style and methodology used to solve
problems through code. Below, we explore three primary paradigms: Procedural
Programming, Functional Programming, and Object-Oriented Programming (OOP).

Procedural Programming
Procedural programming is a paradigm centered around procedures, which are commonly
referred to as functions or routines. This methodology breaks down a program into smaller,
highly manageable pieces (functions) that execute when called. It relies on a linear, step-by-
step approach to accomplish tasks, and it fundamentally treats data and functions as
separate entities.
Characteristics
 Top-Down Approach: Execution flows sequentially from the top to the bottom.
 Simplicity: Highly straightforward and easy to implement for smaller applications.
 Reusability: Functions can be invoked multiple times, significantly reducing code
duplication.
 No Encapsulation: Data is not bound to specific functions, meaning it remains
exposed and separate from the behavior.
Example: Procedural Approach

def calc_efficiency(distance: float, fuel_used: float) -> float:


"""Calculate the fuel efficiency of a vehicle (km per
liter)."""
return distance / fuel_used

def show_vehicle_efficiency(vehicle: str, efficient: float):


"""Display the formatted vehicle fuel efficiency."""
print(f"The fuel efficiency of {vehicle} is {efficient:.2f}
km/l.")

# Main program execution


if __name__ == "__main__":
vehicle_name = "Toyota Corolla"
distance_traveled = 500 # in kilometers
fuel_consumed = 25 # in liters

efficiency = calc_efficiency(distance_traveled,
fuel_consumed)
show_vehicle_efficiency(vehicle_name, efficiency)
Output:

47
Information to Note
The procedural program effectively divides concerns into distinct functions. The data—such
as the vehicle's name, the distance traveled, and the fuel consumed—is explicitly passed
into these functions as arguments.
Advantages
 Very simple to learn and implement for beginners.
 Code execution is highly linear, making it easier to debug simple scripts.
Disadvantages
 Becomes increasingly difficult to manage and scale for large programs.
 Lacks encapsulation, offering no mechanisms for data hiding.
 Highly prone to errors when multiple functions modify global or shared data.

Functional Programming
Functional programming treats functions as the absolute core building blocks of software
architecture. It relies heavily on immutable data and "pure" functions—meaning functions
that do not produce unpredictable side effects. This paradigm utilizes concepts such as
higher-order functions and operations like map, filter, reduce, and recursion. It champions a
declarative coding style, emphasizing what needs to be achieved rather than mapping out
exactly how to achieve it.

Characteristics
In functional programming, functions are considered "first-class citizens." This means they
can be passed as arguments, returned by other functions, and assigned directly to variables.
This paradigm rigidly avoids altering the state of existing data, promoting the exclusive use
of immutable data structures.
Example: Functional Approach

from typing import List

# Pure functions
def is_electric(vehicle: dict) -> bool:
"""Check if a vehicle is classified as electric."""
return vehicle["type"] == "electric"

def average_range(vehicles: List[dict]) -> float:


"""Calculate the average range of a provided list of electric
vehicles."""
total_range = sum(vehicle["range"] for vehicle in vehicles)
return total_range / len(vehicles) if vehicles else 0

48
# Main program execution
if __name__ == "__main__":
vehicles = [
{"name": "Tesla Model S", "type": "electric", "range":
600},
{"name": "Ford F-150", "type": "diesel", "range": 800},
{"name": "Nissan Leaf", "type": "electric", "range":
300},
]

# Utilize filter to extract only electric vehicles


electric_vehicles = list(filter(is_electric, vehicles))
avg_range = average_range(electric_vehicles)

print("Electric Vehicles:", electric_vehicles)


print(f"Average Range of Electric Vehicles: {avg_range:.2f}
km")
Output:

Built-in operations like filter and map handle computations in a fully declarative manner.
They focus solely on the desired outcome (extracting data and computing averages) without
mutating the original list.

Object-Oriented Programming (OOP)


Object-Oriented Programming (OOP) structures code around classes and objects. Classes act
as structural blueprints, while objects are the specific, tangible instances born from those
blueprints. OOP is built upon four foundational pillars: encapsulation, inheritance,
polymorphism, and abstraction.

Characteristics
OOP fundamentally binds data and behavior (methods) together inside unified objects. It
drastically improves code reusability and system scalability while heavily enforcing the DRY
(Don't Repeat Yourself) principle.

Key Concepts
 Encapsulation: The bundling of data variables and the methods that control them
into a single, secure class.
 Inheritance: The ability to derive new, specialized classes from existing parent
classes, promoting logical code extension.
 Polymorphism: The capability of methods to behave differently based on the specific
object that is executing them.

49
 Abstraction: The practice of hiding complex implementation details internally,
exposing only the safe, necessary features to external components.

class Vehicle:
"""Base blueprint for standard vehicles."""
def __init__(self, name: str, fuel_capacity: float,
fuel_efficiency: float):
[Link] = name
self.fuel_capacity = fuel_capacity # measured in
liters
self.fuel_efficiency = fuel_efficiency # measured in
km/l

@property
def max_range(self) -> float:
"""Calculate the maximum potential range of the
vehicle."""
return self.fuel_capacity * self.fuel_efficiency

def __str__(self) -> str:


return f"{[Link]}: Max Range = {self.max_range:.2f}
km"

class ElectricVehicle(Vehicle):
"""Specialized electric vehicle class inheriting from the
Vehicle base."""
def __init__(self, name: str, battery_capacity: float,
efficiency: float):
# Initializing the parent class with zeroed fuel values
super().__init__(name, fuel_capacity=0,
fuel_efficiency=0)
self.battery_capacity = battery_capacity # measured in
kWh
[Link] = efficiency # measured in
km/kWh

@property
def max_range(self) -> float:
"""Override the max_range property specifically for
electric metrics."""
return self.battery_capacity * [Link]

def __str__(self) -> str:


return f"{[Link]} (Electric): Max Range =
{self.max_range:.2f} km"

50
# Main program execution
if __name__ == "__main__":
car = Vehicle("Toyota Corolla", 50, 15) # 50L tank, 15
km/L
ev = ElectricVehicle("Tesla Model 3", 75, 6) # 75 kWh
battery, 6 km/kWh

print(car)
print(ev)
Output:

Advantages
 Modular and Scalable: Logic can be cleanly reused via inheritance and object
composition.
 Encapsulation provides strict data security and access control.
 Polymorphism allows dynamic flexibility across different object types.
Disadvantages
 Often considered overkill and overly verbose for small, simple scripts.
 Requires a solid architectural understanding of design principles to implement
correctly.
Comparison Table
Feature Procedural Functional Object-Oriented
Programming Programming Programming (OOP)
Focus Step-by-step Pure functions and data Classes and object
procedures and immutability instances
instructions
State Handled via Data is strictly Encapsulated safely
Management global/shared immutable within objects
variables
Reusability Code is reused by Function reuse and Leverages inheritance
calling global composition and polymorphism
functions
Key Concepts Functions, top- Pure functions, Encapsulation,
down flow, recursion, higher-order inheritance,
subroutines functions polymorphism,
abstraction

51
Ideal Use Case Small utility scripts, Complex data Large-scale, modular,
basic linear tasks transformations, scalable enterprise
scientific computing systems

Portfolio Exercise Lab Week 6


(Implementing Persistence)

import csv
import datetime
from task import (
Task,
RecurringTask,
) # Assumes Task and RecurringTask are defined in [Link]

class TaskCsvDAO:
"""Data Access Object for managing Task persistence via
CSV."""

def __init__(self, storage_path: str) -> None:


self.storage_path = storage_path
[Link] = [
"title",
"type",
"date_due",
"completed",
"interval",
"completed_dates",
"date_created",

52
"description",
]

def get_all_tasks(self) -> list[Task]:


"""Reads the CSV file and reconstructs Task objects."""
task_list = []
try:
with open(self.storage_path, "r") as file:
reader = [Link](file)
for row in reader:
# Validate that the row is properly parsed as
a dictionary
if not isinstance(row, dict):
print(f"Skipping invalid row format:
{row}")
continue

# Ensure crucial baseline fields are present


if (
not [Link]("title")
or not [Link]("type")
or not [Link]("date_due")
):
print(f"Skipping row missing required
fields: {row}")
continue

task_type = row["type"]
title = row["title"]
description = row["description"]

try:
date_due = [Link](
row["date_due"], "%Y-%m-%d"
)
date_created =
[Link](
row["date_created"], "%Y-%m-%d"
)
completed = row["completed"] == "True"
except (ValueError, KeyError):
print(f"Skipping row due to invalid date
formats: {row}")
continue

completed_dates = []

53
if [Link]("completed_dates"):
completed_dates = [

[Link]([Link](), "%Y-%m-%d")
for date in
row["completed_dates"].split(",")
if [Link]()
]

if task_type == "Task":
task = Task(title, date_due, description)
[Link] = completed
task.date_created = date_created
elif task_type == "RecurringTask":
interval = [Link](
days=int(row["interval"].split()[0])
)
task = RecurringTask(title, date_due,
description, interval)
task.completed_dates = completed_dates
[Link] = completed
task.date_created = date_created
else:
print(f"Unknown task classification in
row: {row}")
continue

task_list.append(task)
except FileNotFoundError:
print("Task CSV file not found on disk. Initializing
a fresh database.")

return task_list

def save_all_tasks(self, tasks: list[Task]) -> None:


"""Serializes current Task objects and overwrites the CSV
file."""
try:
with open(self.storage_path, "w", newline="") as
file:
writer = [Link](file,
fieldnames=[Link])
[Link]()
for task in tasks:
try:
row = {

54
"title": [Link],
"type": (
"RecurringTask"
if isinstance(task,
RecurringTask)
else "Task"
),
"date_due": (
task.date_due.strftime("%Y-%m-
%d")
if isinstance(task.date_due,
[Link])
else task.date_due
),
"completed": [Link],
"description": [Link],
"interval": (
str([Link])
if isinstance(task,
RecurringTask)
else ""
),
"completed_dates": (
",".join(
[Link]("%Y-%m-%d")
for date in
task.completed_dates
)
if isinstance(task,
RecurringTask)
else ""
),
"date_created": (
task.date_created.strftime("%Y-
%m-%d")
if isinstance(task.date_created,
[Link])
else task.date_created
),
}
[Link](row)
except Exception as e:
print(f"Error serializing task:
{[Link]}, Details: {e}")
print("All tasks successfully serialized and
saved to CSV.")

55
except Exception as e:
print(f"Critical error accessing disk to save tasks:
{e}")

Output:

4 tasks loaded from CSV.


----------MENU------------
| 1. Add a new task to the list.
| 2. View the completed and uncompleted tasks in the list.
| 3. Remove a task from the list.
| 4. Select a Task to perform
(Complete Task/ description/Change due date).
| 5. Change the title of a task.
| 6. View Task Overdue.
| 7. Save and exit the program.
--------------------------
Please make a selection from (1-6): 1

Select task Type(normal/recurring) task: normal


Add your task please: Attend OOP Lectures
Set the date (YYYY-mm-dd) :2024-11-22
Write a description for this task (or press Enter to skip): OOP
Demonstration is on Friday!!!

----------MENU------------
| 1. Add a new task to the list.
| 2. View the completed and uncompleted tasks in the list.
| 3. Remove a task from the list.
| 4. Select a Task to perform
(Complete Task/ description/Change due date).
| 5. Change the title of a task.
| 6. View Task Overdue.
| 7. Save and exit the program.
--------------------------
Please make a selection from (1-6): 2

Select a task to view (completed / uncompleted)?: uncompleted

Task list owner: Owner: Group4, Email: Group4@[Link]


1. Task-eating (created: 2024-11-19 00:00:00, due: 2024-11-19
00:00:00, Completed: False). always eating
2. To the cinema - Recurring (created: 2024-11-19, due: 2024-11-
28, completed dates: [2024-11-27], status: Not Completed,
description: To watch the latest movie in town)

56
3. Task-Swiming (created: 2024-11-19 00:00:00, due: 2024-11-25
00:00:00, Completed: False). I love to swim
4. Task-Attend OOP Lectures (created: 2024-11-20 14:40:01.793828,
due: 2024-11-22 00:00:00, Completed: False). OOP Demonstration is
on Friday!!!

----------MENU------------
| 1. Add a new task to the list.
| 2. View the completed and uncompleted tasks in the list.
| 3. Remove a task from the list.
| 4. Select a Task to perform
(Complete Task/ description/Change due date).
| 5. Change the title of a task.
| 6. View Task Overdue.
| 7. Save and exit the program.
--------------------------
Please make a selection from (1-6): 7

Tasks saved successfully.


System ends

Process finished with exit code 0

57
Week 7 - SOLID Principles and
Python Exceptions
As we advance in our Object-Oriented Programming (OOP) journey, we must shift our focus
from merely making code work to making it maintainable, scalable, and robust. In Week 7,
we explored the SOLID design principles and how to handle runtime anomalies using Python
Exceptions. Finally, we applied these concepts to massively refactor our To-Do List
application by properly separating user interfaces from business logic.

The SOLID Principles


SOLID is an acronym representing five fundamental design principles intended to make
software designs more understandable, flexible, and easier to maintain.
S - Single Responsibility Principle (SRP)
 Overview: A class should have only one reason to change, meaning it should only
have a single job or responsibility.
 Application: This is closely tied to the "Separation of Concerns." Good OOP design
avoids "coupling" (the degree of interdependence between modules). For example, a
class that manages data storage should not also be responsible for printing menus to
the user console.
O - Open/Closed Principle (OCP)
 Overview: Software entities (classes, modules, functions) should be open for
extension but closed for modification.
 Application: If we want to add a new feature, we should be able to extend a class's
behavior (often via inheritance) rather than rewriting the existing, tested code. In our
app, creating a RecurringTask by inheriting from Task rather than constantly
modifying the base Task class is a perfect example of OCP.
L - Liskov Substitution Principle (LSP)
 Overview: Objects of a superclass shall be replaceable with objects of its subclasses
without breaking the application.
 Application: Because RecurringTask inherits from Task and shares the same interface,
our app can substitute a normal task with a recurring one without the system
crashing.
I - Interface Segregation Principle (ISP)
 Overview: Clients should not be forced to depend on interfaces (or methods) they do
not use.

58
 Application: This principle promotes smaller, highly specific blueprints over large,
monolithic ones. Our Task class only provides necessary methods; it doesn't force
standard tasks to implement recurring logic they don't need.

D - Dependency Inversion Principle (DIP)


 Overview: High-level modules should depend on abstractions, not on concrete low-
level details.
 Application: Our TaskList manages tasks without needing to know if they are saved in
a CSV or a Database. We inverted the dependency by using Data Access Objects
(DAOs) to handle the low-level file writing, keeping the high-level manager
decoupled from the storage mechanism.

Python Exception Handling


Often, programs encounter errors that are outside the developer's immediate control—such
as a user entering an invalid list index or the system trying to open a file that doesn't exist.
Instead of letting the application crash abruptly, we use Exception Handling to catch these
errors and fall back to a safe state or notify the user gracefully.
Python utilizes try, except, else, and finally blocks to manage these scenarios.

def safely_get_task(task_list, user_index_input):


try:
# We try to convert the input and access the list
index = int(user_index_input)
selected_task = task_list[index]
print(f"Task selected: {selected_task}")

except IndexError:
# This catches out-of-bound list accesses
print("Error: That task number does not exist. Please try
again.")

except ValueError:
# This catches non-integer text inputs
print("Error: Please enter a valid numerical digit.")

Putting It Together: Refactoring the To-Do App


To align our To-Do application with SOLID principles (specifically SRP), we must tear apart
our old monolithic [Link]. We are separating the application into distinct layers:

59
1. The Factory Pattern (TaskFactory): A dedicated method responsible purely for
instantiating objects dynamically based on the arguments provided (e.g., creating a
RecurringTask if an interval is detected, otherwise a standard Task).
2. The Business Logic (TaskManagerController): A middleman class that handles the
core application logic (adding tasks, completing tasks, calling the DAOs) without ever
touching print() or input().
3. The Presentation Layer (CommandLineUI): A dedicated class purely for printing the
menus, capturing user keyboard inputs, and catching IndexError exceptions
smoothly.
Full Refactored Application Code
Below is the updated structure, split into appropriate modules to reflect the Separation of
Concerns.

# ==========================================
# Module: [Link]
# Contains: Task, RecurringTask, and TaskFactory
# ==========================================
import datetime
from typing import Any

class Task:
def __init__(self, title: str, date_due: [Link]):
[Link] = title
self.date_due = date_due
[Link] = False

class RecurringTask(Task):
def __init__(self, title: str, date_due: [Link],
interval: int):
super().__init__(title, date_due)
[Link] = interval

class TaskFactory:
"""Factory Pattern to cleanly handle object instantiation."""
@staticmethod
def create_task(title: str, date: [Link],
**kwargs: Any) -> Task:
# Determines task type based on the presence of the
'interval' keyword
if "interval" in kwargs:
return RecurringTask(title, date, kwargs["interval"])
return Task(title, date)

# ==========================================

60
# Module: task_list.py
# Contains: TaskList
# ==========================================
class TaskList:
def __init__(self, owner: str):
[Link] = owner
[Link] = []

def add_task(self, task: Task) -> None:


[Link](task)

def check_task_index(self, ix: int) -> bool:


"""DRY Principle: Safely validates if an index exists."""
return 0 <= ix < len([Link])

# ==========================================
# Module: [Link]
# Contains: TaskManagerController
# ==========================================
class TaskManagerController:
"""Handles business logic, fully separated from UI console
prints."""
def __init__(self, owner: str):
self.task_list = TaskList(owner)

def process_new_task(self, title: str, date:


[Link], interval: int = None) -> None:
if interval:
new_task = TaskFactory.create_task(title, date,
interval=interval)
else:
new_task = TaskFactory.create_task(title, date)

self.task_list.add_task(new_task)

def process_completion(self, index: int) -> bool:


if self.task_list.check_task_index(index):
self.task_list.tasks[index].completed = True
return True
return False

def get_all_tasks(self) -> list:


return self.task_list.tasks

61
# ==========================================
# Module: [Link]
# Contains: CommandLineUI
# ==========================================
class CommandLineUI:
"""Handles ALL user inputs and console outputs. Catches
Exceptions."""
def __init__(self, controller: TaskManagerController):
[Link] = controller

def _print_menu(self):
print("\n--- SOLID To-Do Manager ---")
print("1. Add Standard Task")
print("2. Add Recurring Task")
print("3. View Tasks")
print("4. Complete a Task")
print("5. Exit")

def run(self):
while True:
self._print_menu()
choice = input("Select an option: ")

if choice == "1":
title = input("Enter task title: ")
# Simplified date for demonstration
date = [Link]()
[Link].process_new_task(title, date)
print("Task added successfully.")

elif choice == "2":


title = input("Enter recurring task title: ")
date = [Link]()
try:
interval = int(input("Enter repeat interval
(days): "))
[Link].process_new_task(title, date,
interval=interval)
print("Recurring Task added successfully.")
except ValueError:
print("Error: Interval must be an integer.")

elif choice == "3":


tasks = [Link].get_all_tasks()
for idx, t in enumerate(tasks):
status = "[X]" if [Link] else "[ ]"

62
print(f"{idx}: {status} {[Link]}")

elif choice == "4":


try:
idx = int(input("Enter task index to
complete: "))
success =
[Link].process_completion(idx)
if success:
print("Task marked as complete.")
else:
print("Error: Task index out of range.")
except ValueError:
print("Error: Please enter a valid number.")
except IndexError: # Explicit exception handling
as required
print("Error: That task does not exist in the
list.")

elif choice == "5":


print("Shutting down safely.")
break
else:
print("Invalid selection.")

# ==========================================
# Module: [Link]
# Contains: Execution Entry Point
# ==========================================
if __name__ == "__main__":
# The dependencies are injected here, satisfying DIP
app_controller = TaskManagerController("Group4")
app_ui = CommandLineUI(app_controller)

# Start the separated presentation loop


app_ui.run()

63
Week 8 - Debugging, Properties,
and Persistence
As our software applications grow in complexity, the methods we use to maintain and
enhance them must also evolve. This week's laboratory exercises transition our focus from
writing initial code to maintaining and preserving it. We explored the critical process of
Debugging to systematically eliminate logical errors, utilized the @property decorator to
create dynamic object attributes, and implemented the Data Access Object (DAO) pattern
alongside CSV serialization to ensure our application's data persists between sessions.

System Debugging and Error Tracing


Programming errors fall into several categories. While syntax errors crash the program
immediately, logical errors are far more insidious the code runs perfectly without crashing,
but the resulting output is fundamentally incorrect.
Instead of cluttering our codebase with endless print() statements to track variable states,
professional developers utilize a Debugger. A debugger allows us to pause code execution at
specific breakpoints and step through the instructions line-by-line.
Core Debugging Techniques
 Breakpoints: Designated markers placed on specific lines of code. The interpreter
halts execution right before executing a breakpoint line, allowing developers to
inspect the current state of the application.
 Step Over: Executes the current line and moves to the next line without diving into
the internal logic of any called functions.
 Step Into: If the current line contains a function call, the debugger jumps inside that
function to allow line-by-line inspection of its internal logic.
 Variable Watch: A dedicated panel in the IDE where developers can monitor the live,
changing values of specific variables and mathematical expressions.

Figure 4 Debugging in Jupyter Notebook

64
Figure 5 Debugging options

Implementation: Fixing the Vehicle Simulator Bug


In our provided exercise, we had an automobile simulator that reported 0 miles traveled
despite accelerating. By using the debugger, we watched the speed variable and discovered
that braking multiple times caused the speed to become a negative number. Thus, when the
system calculated the distance, the negative speed subtracted from the odometer.
We resolved this logical error by introducing a protective conditional structure inside the
brake method.
The following code demonstrates the fixed simulator class, ensuring speed can never drop
below zero.

class VehicleSimulator:
def __init__(self, initial_velocity: int = 0) -> None:
self.current_velocity = initial_velocity
self.total_distance = 0
self.elapsed_time = 0

def apply_throttle(self) -> None:


"""Increases vehicle speed."""
self.current_velocity += 5

def apply_brakes(self) -> None:


"""Decreases speed safely without entering negative
values."""
# Fixed Logical Error: Prevents negative velocity
if self.current_velocity >= 5:
self.current_velocity -= 5
else:
self.current_velocity = 0

def advance_time(self) -> None:


"""Simulates the passage of one time unit."""
self.total_distance += self.current_velocity
self.elapsed_time += 1

def calculate_mean_velocity(self) -> float:


if self.elapsed_time == 0:
return 0.0
return self.total_distance / self.elapsed_time

65
Python Properties using the @property
Decorator
In Object-Oriented Programming, we often need to derive data based on existing attributes.
Writing a standard method requires the user to call it with parentheses (). However, Python
provides the @property decorator, which transforms a standard method so that it can be
accessed exactly like a standard attribute.

This is incredibly useful for data that needs to be computed strictly when requested. We
applied this to our To-Do List manager by creating a property that filters out completed tasks
using elegant list comprehension.
This snippet shows how the decorator is applied above the function definition, creating a
seamlessly calculated attribute.

class AdvancedTaskList:
def __init__(self, list_owner: str):
self.list_owner = list_owner
self.master_task_array = []

# The decorator transforms this method into a dynamic


attribute
@property
def pending_tasks_only(self) -> list:
"""Returns a filtered list of tasks that are NOT
completed."""
# Utilizing list comprehension for concise filtering
return [item for item in self.master_task_array if not
item.is_finished]

def display_pending(self) -> None:


print("\n--- The following tasks are still to be done
---")
# Accessing the property without parentheses
for idx, item in enumerate(self.pending_tasks_only):
print(f"{idx}: {[Link]}")

Implementing Persistence with CSV


Up to this point, our To-Do application suffered from complete amnesia; all data was wiped
from RAM the moment the program terminated. Persistence is the mechanism of saving
application state to a permanent storage medium (like a hard drive).

66
We utilized Serialization, which is the process of converting complex Python objects into a
raw byte stream or text format. Specifically, we used the Comma Separated Values (CSV)
format. To keep our code modular, we implemented a Data Access Object (DAO). The DAO
pattern isolates all file-handling logic away from the main application, ensuring the core
program doesn't need to understand how or where the data is saved.

Implementation: The CSV Data Access Object


We utilized Python's built-in [Link] and [Link] to parse our text files into
dictionaries. We had to carefully cast our string-based dates back into actual datetime
objects when loading, and format them back to standard strings when saving.
The code below demonstrates our persistence layer, showing how raw CSV text is translated
back into usable Python objects and vice-versa.

import csv
import datetime
from typing import List, Any

# Mock Task classes for demonstration purposes


class TrackableTask:
def __init__(self, title: str, date_due: [Link]):
[Link] = title
self.date_due = date_due
self.is_finished = False

class TaskPersistenceManager:
"""DAO pattern implementation for CSV file handling."""
def __init__(self, file_destination: str) -> None:
self.file_destination = file_destination
self.column_headers = ["title", "type", "date_due",
"completed"]

def retrieve_saved_tasks(self) -> List[TrackableTask]:


"""Deserializes CSV rows into Python Objects."""
extracted_tasks = []
try:
with open(self.file_destination, "r") as open_file:
csv_parser = [Link](open_file)
for row_data in csv_parser:
# Parsing string to datetime
parsed_date =
[Link](row_data["date_due"], "%Y-%m-%d")

reconstructed_task =
TrackableTask(row_data["title"], parsed_date)
# Casting string "True"/"False" to boolean

67
reconstructed_task.is_finished =
(row_data["completed"] == "True")

extracted_tasks.append(reconstructed_task)
except FileNotFoundError:
print("Warning: Storage file missing. Booting with a
fresh database.")

return extracted_tasks

def commit_tasks_to_disk(self, current_tasks:


List[TrackableTask]) -> None:
"""Serializes Python Objects into CSV rows."""
with open(self.file_destination, "w", newline="") as
open_file:
csv_writer = [Link](open_file,
fieldnames=self.column_headers)
csv_writer.writeheader()

for task_item in current_tasks:


formatted_row = {
"title": task_item.title,
"type": "Standard",
# Converting datetime back to string for
storage
"date_due": task_item.date_due.strftime("%Y-
%m-%d"),
"completed": str(task_item.is_finished)
}
csv_writer.writerow(formatted_row)

68
Week 9 - Data Structures &
Abstract Classes
In Week 9, we explored Python's core Data Structures (Lists, Dictionaries, Tuples, Sets) and
the concept of Abstract Classes.

What is a Data Structure?


A Data Structure is a specialized format for organizing, processing, and storing data in
memory so it can be accessed and modified efficiently.

Types of Data Structures


1. Lists
A List is an ordered, mutable (changeable) collection that can store multiple items of varying
data types in a single variable.
Basic Operations:
Lists support adding, removing, and measuring size.

fruits = ["Mango", "Peach", "Plum"]


print("Original:", fruits)

[Link]("Kiwi") # Adds to the end


print("After append:", fruits)

[Link]("Peach") # Removes specific item


print("After remove:", fruits)
print("List length:", len(fruits))
Output:

Loops and Enumeration:


You can iterate through elements directly or use enumerate() to access both the index and
the value.

brands = ["Sony", "Apple", "Samsung"]

# Standard loop
for brand in brands:

69
print(brand)

# Enumerate loop (Index + Value)


for index, brand in enumerate(brands):
print(f"{index}) {brand}")
Output:

Indexing, Slicing, and Nested Lists:


Lists are 0-indexed. You can extract a subset using the range operator [start:end]. Slicing
creates a new list (immutability) without altering the original. Lists can also contain other
lists (Nested Lists), which is perfect for 2D matrices.

names = ["Alice", "Bob", "Charlie", "David", "Eve"]

# Slicing (exclusive of the end index)


print("Index 0 to 2:", names[0:2])
print("Last element:", names[-1])

# Nested List (Matrix)


matrix = [
[10, 20],
[30, 40]
]
print("Row 0, Col 1 is:", matrix[0][1]) # Outputs 20
Output:

2. Dictionaries
A Dictionary stores data in key:value pairs. Keys must be unique, allowing for rapid data
retrieval. They can be created using {} or the dict() constructor.

capitals = {
"Japan": "Tokyo",
"France": "Paris",

70
"Canada": "Ottawa"
}

# Modifying and Adding


capitals["Germany"] = "Berlin" # Adds new pair
capitals["Japan"] = "Kyoto" # Modifies existing pair

# Deleting
del capitals["France"]

print("Dictionary:", capitals)
print("All Keys:", [Link]())
print("All Values:", [Link]())

# Looping through a dictionary


for country in capitals:
print(f"Key: {country}, Value: {capitals[country]}")
Output:

3. Tuples
A Tuple is similar to a list but immutable—once created, its elements cannot be changed.
They are defined using parentheses (). Tuples support destructuring, allowing you to unpack
values directly into variables.

devices = ("laptop", "mouse", "keyboard")


print("1st element:", devices[0])

# Destructuring
device1, device2, device3 = devices
print(device1, device2, device3)

# toys[0] = "monitor" --> THIS WOULD THROW AN ERROR (Immutable)

# However, the unpacked variables can be reassigned freely


device1 = "desktop"
print("Reassigned unpacked variable:", device1)
Output:

71
4. Sets
A Set is an unordered collection of unique and immutable elements. It automatically
removes duplicates and supports mathematical operations like Union, Intersection, and
Difference.

primes = {2, 3, 5, 7}
[Link](11)

# Set Operations
set_b = {5, 7, 9, 13}

print("Union (Combines all unique):", [Link](set_b))


print("Intersection (Common items):", [Link](set_b))
print("Difference (In primes, not in set_b):",
[Link](set_b))
Output:

Abstract Classes
An Abstract Class acts as a strict blueprint for other classes. It cannot be instantiated on its
own. Any child class inheriting from an abstract class must implement the methods marked
with the @abstractmethod decorator.
In Python, this requires importing ABC (Abstract Base Class) and abstractmethod from the
abc module.

from abc import ABC, abstractmethod

# Abstract Base Class


class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass

def standard_method(self):
print("This is a normal inherited method.")

72
# Child Classes
class Circle(Shape):
def calculate_area(self):
print("Calculating Circle Area: pi * r^2")

class Square(Shape):
def calculate_area(self):
print("Calculating Square Area: side * side")

# Instantiating the subclasses


my_circle: Shape = Circle()
my_circle.calculate_area()

my_square: Shape = Square()


my_square.calculate_area()

# my_shape = Shape() --> THIS WOULD THROW AN ERROR


Output:

Portfolio Exercise 6
Priority Task

class PriorityTask(Task):
def __init__(
self, title: str, date_due: datetime, priority="low",
description: str = ""
):
super().__init__(title, date_due, description,
"PriorityTask")
[Link] = priority

def __str__(self):
status = "Completed" if [Link] else "Not
Completed"
return f"{[Link]} - Priority - {[Link]}
(created: {self.date_created.strftime("%Y-%m-%d")}, status:
{status})"

73

You might also like