python

2026/04/22

Session 1: Getting Started with Python and Fundamentals

What is Python?

Python is a high-level, readable, and multi-purpose programming language. Its simple syntax makes writing code feel very similar to writing in English, which is why it is widely considered the best choice for beginners.

Essential Tools & Installation

  1. Installing Python: Visit python.org and download the latest version. (Ensure you check the box "Add Python to PATH" during installation).
  2. What is PyCharm? A professional Integrated Development Environment (IDE) specifically for Python. It offers smart tools that act as an assistant for large-scale projects.
  3. What is Jupyter Notebook? An interactive environment that allows you to run code in separate blocks and see the results instantly. It is perfect for learning, data analysis, and quick testing.
  4. What is Pip? It stands for "Preferred Installer Program." It is a tool used to download and install Python libraries and packages from the internet.

Variables and Data Types

In Python, variables are used to store data. The primary types include:

  • int: Integers (e.g., 10)
  • float: Floating-point numbers (e.g., 15.5)
  • str: Strings or text (e.g., "Hello")
  • bool: Logical values (True or False)

Type Detection and Casting

To check a variable's type, use the type() function:

x = 10
print(type(x))  # <class 'int'>

To convert between types (Casting):

y = float(x)   # Convert int to float -> 10.0
s = str(x)     # Convert int to string -> "10"

Input and Output

Use input() to receive data from the user and print() to display results:

name = input("Enter your name: ")
print("Hello", name)

Note: input() always returns a string. If you need a number, you must cast it: int(input()).

Mathematical Operators

  • Addition: + | Subtraction: - | Multiplication: * | Division: /
  • Power: ** (e.g., 2 ** 3 equals 8)
  • Modulus (Remainder): %
  • Floor Division: //

Exercise Solutions: Session 1

1. Swapping Two Variables Without a Temporary Variable

Python allows you to swap values in a single line:

a = 5
b = 10
a, b = b, a
print("a:", a, "b:", b)

Explanation: Python creates a "tuple" of the right-hand side first and then unpacks it into the variables on the left.


2. Calculating Tip, Tax, and Total

food_cost = float(input("Enter the meal charge: "))

tip = food_cost * 0.18
tax = food_cost * 0.07
total = food_cost + tip + tax

print(f"Tip: {tip}")
print(f"Tax: {tax}")
print(f"Total: {total}")

Explanation: We cast the input to float for precise decimal calculations, calculated the percentages, and summed them up.


3. Cookie Ingredient Adjuster

Since the original recipe makes 48 cookies, we find the ratio for one cookie and multiply it by the desired amount.

cookies_needed = int(input("How many cookies do you want to make? "))

ratio = cookies_needed / 48

sugar = 1.5 * ratio
butter = 1 * ratio
flour = 2.75 * ratio

print(f"Sugar needed: {sugar} cups")
print(f"Butter needed: {butter} cups")
print(f"Flour needed: {flour} cups")

4. Solving a Quadratic Equation

We use the Discriminant formula: $$Delta = b^2 - 4ac$$.

import math

a = float(input("Enter coefficient a: "))
b = float(input("Enter coefficient b: "))
c = float(input("Enter coefficient c: "))

delta = b**2 - 4*a*c

if delta > 0:
    x1 = (-b + math.sqrt(delta)) / (2*a)
    x2 = (-b - math.sqrt(delta)) / (2*a)
    print(f"Two real roots: {x1} and {x2}")
elif delta == 0:
    x = -b / (2*a)
    print(f"One repeated root: {x}")
else:
    print("This equation has no real roots.")

Explanation: We imported the math library to use sqrt(). The if-elif-else structure handles the three possible mathematical outcomes based on the value of Delta.

Session 2: Logic, Conditionals & Control Flow


1. Comparison Operators (The Basis of Logic)

We use these operators to compare values. The result is always a Boolean value: either True or False.

  • == : Equal to
  • != : Not equal to
  • < / > : Less than / Greater than
  • <= / >= : Less than or equal to / Greater than or equal to

2. Logical Operators (Combining Conditions)

Sometimes we need to check multiple conditions at once:

  • and: Returns True if both sides are true.
  • or: Returns True if at least one side is true.
  • not: Inverts the result (turns True to False and vice-versa).

3. Conditional Statements (If, Elif, Else)

This is how we tell Python to make decisions.
Crucial Note: In Python, you must use a colon (:) after each condition, and the next line must be indented (4 spaces). This indentation defines the block of code belonging to that condition.

  • if: If the condition is true, execute this code.
  • elif: (Short for else if) If the previous condition was false, but this one is true, execute this.
  • else: If none of the above conditions were true, execute this as a fallback.

4. The Match-Case Structure (Python 3.10+)

This is a cleaner alternative to long if-elif chains when you are checking a single variable against multiple specific values. The _ symbol acts as a "default" or "catch-all" case (similar to else).


Programming Challenges: Solutions & Explanations

1. Triangle Analyzer

Goal: Practice comparison operators and nested conditions.

a = float(input("Enter side A: "))
b = float(input("Enter side B: "))
c = float(input("Enter side C: "))

# Checking the Triangle Inequality Theorem
if (a + b > c) and (a + c > b) and (b + c > a):
    if a == b == c:
        print("Result: Equilateral triangle")
    elif a == b or a == c or b == c:
        print("Result: Isosceles triangle")
    else:
        print("Result: Scalene triangle")
else:
    print("Result: Invalid Triangle")

Explanation: First, we check if the sides can mathematically form a triangle. If valid, we enter a "nested" condition to determine its type based on how many sides are equal.


2. Advanced Discount System

Goal: Prioritizing conditions and handling independent logic.

amount = float(input("Enter amount: "))
is_member = input("Are you a member (True/False)? ").lower() == "true"

if amount > 200:
    discount = 0.20 if is_member else 0.10
elif 100 <= amount <= 200:
    discount = 0.05
else:
    discount = 0

final_price = amount * (1 - discount)
print(f"Final Price: {final_price}")

if amount > 500:
    print("Message: You get a Special Gift!")

Explanation: We use elif to define price ranges. Note that the "Special Gift" check is a separate if block because it applies regardless of the specific discount applied.


3. Leap Year Logic

Goal: Understanding complex logic using and, or, and %.

year = int(input("Enter year: "))

if (year % 400 == 0) or (year % 4 == 0 and year % 100 != 0):
    print("Result: It is a Leap Year.")
else:
    print("Result: Not a Leap Year.")

Explanation: A year is a leap year if it's divisible by 400 OR (divisible by 4 AND NOT divisible by 100). We combined this into a single logical expression.


4. Nested Security Check

Goal: Practicing priority in nested structures and using not.

username = input("Username: ")
password = input("Password: ")
is_banned = input("Is user banned (True/False)? ").lower() == "true"

if is_banned:
    print("Result: Account Locked")
else:
    if username == "admin":
        if password == "1234":
            print("Result: Welcome Admin")
        else:
            print("Result: Wrong Password")
    else:
        print("Result: User Not Found")

Explanation: Safety comes first. We immediately check if the user is banned. Only if they are not banned do we proceed to check the username and then the password.


5. Smart Converter (with Match Case)

Goal: Using match case with internal conditional checks.

number = float(input("Enter number: "))
mode = input("Enter mode (1-3): ")

match mode:
    case "1":
        if number < 0:
            print("Negative input not allowed")
        else:
            print(f"Result: {number * 0.62} miles")
    case "2":
        if number < -273.15: # Absolute zero
            print("Negative input not allowed")
        else:
            print(f"Result: {(number * 1.8) + 32} Fahrenheit")
    case "3":
        res = "Even" if number % 2 == 0 else "Odd"
        print(f"Result: The number is {res}")
    case _:
        print("Result: Invalid Code")

Explanation: The match statement allows us to jump to the correct logic based on the user's choice. Inside cases 1 and 2, we added a safety if to handle invalid inputs like negative distances or temperatures below absolute zero.


Session 3: Loops and Repetition Structures


Logic of Repetition in Programming

In algorithmic thinking, one of the fundamental needs is the repeated execution of a command or a specific block of code.

  • Main Objective: To prevent the repetition of similar code, optimize program size, and manage the execution of sequential processes.
  • Structural Difference from Conditional Statements: A conditional statement executes its dependent code only once if the condition is true; whereas repetition structures or loops continuously execute the code within their body as long as the condition holds true.

The while Loop (Condition-Based Repetition)

The while loop operates based on a logical condition. The body of this loop will be executed repeatedly as long as the condition specified at its beginning holds a true value.

  • Structural Note: The variable used in the loop condition must be updated or changed within the loop body. Otherwise, the loop condition will never become false, and the program will enter an infinite loop, which will cause the system to freeze.

Algorithm for Separating Digits of a Number with while

In this algorithm, the loop mechanism is used to separate and print the digits of an integer from right to left:

#!/usr/bin/env python3

# Get an integer from the user
enter = int(input('enter a integer number: '))

while enter != 0:
    number = enter % 10     # Extract the rightmost digit (remainder of division by 10)
    enter = int(enter / 10) # Remove the rightmost digit through integer division
    
    print(number)           # Print the extracted digit
    
print('Finish!')

Simulating the do-while Structure (Input Validation)

In Python, there is no separate structure called do-while that guarantees the code executes at least once. To implement this logic, a controlled infinite loop (while True) is used in combination with the break statement.

The following code shows an example of this usage for enforcing the input of a positive number:

#!/usr/bin/env python3

while True:
    enter = int(input('enter a number: '))
    
    if enter > 0:
        break     # Immediate exit from the loop if the condition is true
    
print(f'ok! {enter}')


The for Loop and the range Function

When the number of iterations is known in advance, using the for loop is the most optimal choice. This loop iterates over a regular sequence of numbers, which is typically generated by the range function.

Structural rules of the range function:

  • range(stop): Generates numbers from zero to stop-1.
  • range(start, stop): Generates numbers from start to stop-1.
  • range(start, stop, step): Generates numbers from start to stop with specified incremental steps.

In the following program, a for loop is tasked with repeating a specific message along with its numerical index as many times as the number entered by the user:

#!/usr/bin/env python3

enter = int(input('enter a number: '))

# The variable i as a counter changes from 0 to one less than enter
for i in range(enter):
    print(f'{i}) hello')


Loop Control Statements (break and continue)

To manage and change the default behavior of loops under specific conditions, two key statements are built-in:

The break Statement (Complete Stop)

Executing this statement causes the program to immediately exit the loop body and be directed to the first line of code after the loop block.

The continue Statement (Skip Current Iteration)

This statement stops the current iteration, ignores the remaining code in that cycle, and directly sends the program to the beginning of the loop to start the next iteration.

In the following code, the use of the continue statement for filtering and exclusively printing odd numbers is demonstrated:

#!/usr/bin/env python3

n = int(input('enter a number: '))

for i in range(n + 1):
    if i % 2 == 0:
        continue # If the number is even, skip printing and go to the next iteration
    print(f'i -> {i}')


Combined Implementation: Computational Validation

The following program is a comprehensive example of combining both while and for structures. In this structure, first the process of receiving a positive number is guaranteed, and then the sum of even numbers within the specified range is calculated:

#!/usr/bin/env python3

# Phase One: Guarantee receiving a valid positive input
while True:
    n = int(input('Enter a number(number > 0): '))
    if n > 0:
        break

# Phase Two: Calculate the sum of even numbers in the range
sum = 0
for i in range(n + 1):
    if i % 2 == 0:
        sum += i
        
print(f'sum: {sum}')


Nested Loops

Placing one loop inside the body of another loop forms nested loops. The governing rule for this structure is as follows: for each single complete execution of the outer loop, the inner loop runs completely from start to finish.

The following example implements a standard mathematical multiplication table using this concept:

#!/usr/bin/env python3

# Outer loop to control rows
for i in range(1, 10):
    # Inner loop to control columns and calculations
    for j in range(1, 10):
        # Using end = '' to prevent line breaks and create proper spacing
        print(f'{i} * {j} = {i*j} \t', end='')
    print() # Create a new line after completing each full row


Drawing Geometric Patterns

One of the prominent applications of nested loops is managing two-dimensional spaces and drawing patterns. Three structural patterns are analyzed below:

My Image

Right-Angled Star Pyramid Pattern

This program, by making the inner loop dependent on the outer loop's counter, increases the number of stars row by row:

#!/usr/bin/env python3

n = int(input('enter a number: '))

for i in range(1, n + 1):
    for j in range(i): # The number of iterations depends on the outer loop's step
        print('* ', end='')
    print()

Incremental Number Triangle Pattern

In this section, instead of printing a fixed character, an independent secondary variable is used to display consecutive numbers:

#!/usr/bin/env python3

n = int(input('enter a number: '))
counter = 1

for i in range(1, n + 1):
    for j in range(i):
        print(f'{counter} ', end='')
        counter += 1 # Increase the counter value after each print
    print()

Symmetrical Pyramid Pattern Based on Spacing

In this structure, controlling the empty spaces before printing the main character is crucial for maintaining the pyramid's symmetry:

#!/usr/bin/env python3

n = int(input('enter a number: ')) # Get the pyramid depth
space = n - 1 # Define the initial number of empty spaces for the first row

for i in range(1, n + 1):
    # Dedicated loop for printing empty spaces
    for j in range(space):
        print(' ', end='')
    space -= 1 # Gradually reduce empty spaces in each row
    
    # Dedicated loop for printing star characters
    for k in range(i):
        print('* ', end='')
    print()


Session 4: Advanced Data Structures


The Concept of Sequence and Index

When dealing with a collection of data in the programming world, sometimes we need this data to have a specific order. In the Python language, structures that maintain the order of elements are called Sequences. Strings, lists, and tuples are all considered sequences.

When data has order, Python assigns each of them a specific numbered seat. This seat number is called the Index or the numeric identifier of the position. Having this number allows you to directly access the desired member.

What is an Index?

The index is essentially the address or seat number of a member in a queue. In Python, counting positions starts from zero, not one. This means the first member is always in position zero. Python also has another interesting feature: reverse counting. If you want to go from the end of the queue to the beginning, you can use negative numbers. In this case, the last member of the queue has an index of negative one.

For example, consider the following string:

s = "python"
print(s[0])   # Output: p (first character from the left)
print(s[-1])  # Output: n (first character from the right)

Slicing Operation

Sometimes you don't need all the data, but only a portion of it. This operation is called Slicing. The formula and structural pattern for slicing is as follows:

sequence[start:stop:step]

  • start: The starting index (inclusive). If omitted, Python assumes the beginning of the sequence (zero).
  • stop: The ending index (exclusive). An important rule in Python: the stop index itself is never included. The program slices up to, but not including, the stop index.
  • step: The step size or jump interval between elements. If omitted, it defaults to one.
s = "python"
print(s[1:4])   # Output: yth (indices 1, 2, and 3 are extracted; index 4 itself is not included)
print(s[:3])    # Output: pyt (extracts from the beginning up to index 2)
print(s[::2])   # Output: pto (starts from the beginning with steps of 2, taking every other character)

Examining Sample Codes for Strings and Indices

In the code below, a slice with a negative step is written. When the step becomes negative, Python's movement reverses and reads from right to left:

s = 'hello'

print(s[0])       # Output: h
print(s[4:1:-2])   # Output: ol

In the first line, the letter h is printed. In the second line, it starts from index 4 which is the letter o, moves two steps backward to index 2 which is the letter l. Since index 1 (the stop point) is not included in the output, the operation ends here and the output is "ol".

In the next example, we see a program that removes non-numeric characters from text. The ord function in Python returns the ASCII code of a character. The digits zero to nine in the ASCII table have codes between 48 and 57:

enter = input() # Example input: he2llo
result = '' 

for i in range(len(enter)):
    # Check if the current character is not a numeric digit
    if ord(enter[i]) < 48 or ord(enter[i]) > 57: 
        result += enter[i] 
		
print(result) # Output: hello

This program uses the length of the string (len) to examine each character of the input one by one. If the ASCII code of a character is less than 48 or greater than 57, it means the character is not a number. Consequently, it adds that character to the result variable so that in the end, a pure text without numbers is printed.

In the code below, concatenation of two strings using the plus sign is shown, which is called string concatenation:

s1 = 'hello'
s2 = 'world'

s3 = s1 + ' ' + s2
print(s3) # Output: hello world

The program places a space between the two strings and concatenates them to print a unified phrase.

In the next example, the length of the string is used for reverse addressing:

s = 'python'
print(s[-len(s):-1]) # Output: pytho

The len function returns the length of the string, which is 6. By putting a minus sign, the slice range is set from index -6 to -1. Since index -1 itself (the letter n) is not included in the final slice, the output of the program will be "pytho".

In this section, a program is designed that takes personal information and creates a username based on their letters:

name = input('Name: ') # Sample input: saleh
family = input('Family: ') # Sample input: askari

# Get the first letter of the name, the first letter of the family name in uppercase, and concatenate with indices 1 to 2 of the family name
username = name[0].upper() + family[0].upper() + family[1:3]

print(f'UserName: {username}') # Sample output: SAsk

This program takes the first letter of the name and the first letter of the family name and converts them to uppercase using the upper method. Then it appends the letters at indices 1 and 2 of the family name (family[1:3]) to create a concise username.


Challenge and Problem Solving for the Index Section

Question: Assuming our string is msg = "Networking", how can we use slicing to create the following outputs?

msg = "Networking"

# 1. Extract the character N
print(msg[0])

# 2. Extract the part ing
print(msg[-3:])

# 3. Extract the part work
print(msg[3:7])

# 4. Slice the string so that only letters at even indices are printed
print(msg[::2])


List

Lists are the most widely used data storage containers in Python. Imagine a list as a flexible shelf where you can add items, remove items, or rearrange the order whenever you want. The key feature of lists is Mutability. You are allowed to place different data types (numbers, text, booleans) and even other lists together inside a single list.

Defining and Accessing List Elements

To create a list, we use square brackets [] and separate members with commas. Accessing them is exactly like strings, using their seat number or index:

lst = [10, 20, 30, "hi", True]
print(lst[0])   # Output: 10
print(lst[-1])  # Output: True

Due to the mutability of lists, assigning a new value to a specific index is straightforward and direct:

lst[1] = 999
print(lst)  # Output: [10, 999, 30, 'hi', True]

Key List Management Methods

Lists have many built-in methods for changing and managing data, summarized in the table below:

Method Description
append(x) Adds element x to the end of the list
insert(i, x) Inserts element x at the specified index i
pop(i) Removes and returns the element at index i (if no index is provided, removes the last element)
remove(x) Searches for and removes the first element with value x
clear() Removes all elements and empties the list
sort() Sorts the list elements in ascending order
reverse() Reverses the order of elements in the list

Examining Sample Codes for Lists

In Python, you can place a list inside another list, which is called a nested list or matrix:

l = [True , 12,'hello',12.89,[23,'world']]
l1 = l[4] # Extract the inner list at index 4, which is [23, 'world']

print(l1[1]) # Output: world (index 1 of the inner list)

In this code, the fourth position of the main list itself contains a list with two members. The program first copies this inner list into the l1 variable and then extracts and prints the first position of it, which is the word "world".

In the following code, the behavior and usage of main methods on a list of scores are examined:

score = [12, 18, 17, 18, 18]
print(score)

score.insert(1, 20) # Insert 20 at index 1, pushing other elements forward
score.append(10)     # Add 10 to the end of the list
print(score)        # Output: [12, 20, 18, 17, 18, 18, 10]

score.remove(18)    # Remove the first occurrence of 18
print(score)        # Output: [12, 20, 17, 18, 18, 10]

score.sort()        # Sort in ascending order from smallest to largest
score.reverse()     # Reverse the order of elements
print(f'Sorted(reverse): {score}') # Output: [20, 18, 18, 17, 12, 10]

score.clear()       # Empty the entire list shelf
print(score)        # Output: []

In the next program, we see how to find the largest, smallest, and sum of list members using a simple loop without using Python's built-in functions:

score = [18, 19, 15, 12, 11, 10, 9, 16]

min = 20
max = 0
sum = 0

for item in score:
    sum += item
    
    if min > item:
        min = item # Update minimum value
    if max < item:
        max = item # Update maximum value

avrage = sum / len(score)
print(f'SumFinal: {sum} \t Avrage: {avrage}')
print(f'Min: {min} \t Max:{max}')

The loop iterates over each score and adds it to the sum variable. In each iteration, if a score is smaller than the current min value, it replaces it, and the same logic is repeated for the max value. At the end, the highest and lowest scores along with the average are obtained.

An interesting point about the insert method: if you provide an index number that exceeds the list length, Python doesn't throw an error but instead adds the new element to the last possible position:

lst = [1, 2, 3]
lst.insert(6, 8)
print(lst) # Output: [1, 2, 3, 8]

The list length is 3 but we requested to insert the number 8 at index 6. Without any objection, Python adds the number 8 to the end of the list.


Challenge and Problem Solving for the List Section

Question: Suppose you have a list like a = [3, 1, 4]. How can you transform it into [1, 3, 4, 10, 20] without creating a new list and using methods?

a = [3, 1, 4]

a.sort()        # Step 1: Sort -> [1, 3, 4]
a.append(10)    # Step 2: Add 10 to the end -> [1, 3, 4, 10]
a.insert(3, 20) # Step 3: Insert 20 at index 3 -> [1, 3, 4, 20, 10]

print(a)


Tuple

Tuples are like the twin siblings of lists but with one very significant and critical difference: tuples are Immutable. Once you create a tuple, you cannot add members to it, remove anything from it, or change the value at any position. A tuple is like a sealed document. Tuple elements are placed inside parentheses (). Tuples are preferred for fixed data due to the high security they provide against data modification and their optimal memory consumption.

Limited Tuple Methods

Due to the immutable nature of tuples, modification methods (like append or remove) do not exist. Their tools are limited to searching and counting:

  • count(x): Counts the number of occurrences of value x in the tuple.
  • index(x): Finds the index of the first occurrence of value x.

Examining Sample Codes for Tuples

You might wonder what to do if you are forced to change data within a tuple. The solution is to temporarily remove the tuple's mask, convert it to a list, apply the changes, and then convert it back to its original form:

t = ('saleh', 'askari', 23)
print(type(t)) # Output: <class 'tuple'>

t = list(t)    # Convert to a dynamic list
t[0] = 'ahmad' # Apply change to index zero

t = tuple(t)   # Convert back to a secure and locked tuple
print(t)       # Output: ('ahmad', 'askari', 23)

In the following example, the use of search methods on fixed values is demonstrated:

data = (5, 2, 9, 2, 2)
print(data.count(2))  # Output: 3 (the number 2 appears three times)
print(data.index(2))  # Output: 1 (the first occurrence of 2 is at index 1)


Challenge and Problem Solving for the Tuple Section

Question: How can you extract the middle values (20, 30, 40) from the tuple t = (10, 20, 30, 40, 50) using slicing?

t = (10, 20, 30, 40, 50)
result = t[1:4]
print(result) # Output: (20, 30, 40)

We start the slice from index 1 (the number 20) and set it up to index 4 so that index 4 itself is not included and only the three middle numbers are extracted.


Dictionary

In real life, to find the meaning of a word, we don't search by page number; we search for the word itself. Dictionaries in Python work exactly the same way. They are a non-sequential data structure based on Key-Value mapping. In dictionaries, there is no concept of numeric indices (zero, one, two); instead, we have key-value pairs. You provide a unique key (like a word or national ID) and Python returns the corresponding value (like the meaning or personal details). Dictionary elements are placed inside curly braces {}.

Dictionary Structure Methods

Method Description
keys() Returns a collection of all keys in the dictionary
values() Returns a collection of all stored values in the dictionary
items() Returns key-value pairs as sequences of tuples
update(dict) Updates the current dictionary with data from another dictionary
pop(key) Removes the specified key and returns its value
popitem() Removes and returns the last key-value pair inserted
get(key, default) Gets the value for a key (prevents errors if key doesn't exist by returning a default value)

Examining Sample Codes for Dictionaries

Modifying values or adding new pairs is done by referencing the key. In the code below, the usage of the keys method for iterating over keys and their corresponding values is shown:

d = {'name': 'saleh', 'family': 'askari', 'birth_day': (1382, 7, 22)}

for key in d.keys():
    print(f'{key} -> {d[key]}')

The keys method provides the loop with a list of all the titles or keys in the dictionary so we can print each key and its corresponding values one by one.

Combining lists and dictionaries can create very complex and powerful data structures, such as a university management database system:

students = []

student = {
    'student_code': '40113119821',
    'name': ('saleh', 'askari'),
    'birth_day': (1382, 7, 22),
    'course': {
        'AI': {'unit': 3 , 'score': 20},
        'DB': {'unit': 3 , 'score': 15},
        'QH': {'unit': 2 , 'score': 11}
    }
}
students.append(student)

for i in range(len(students)):
    for key in students[i].keys():
        if key == 'name':
            print(f"name: {students[i][key][0]} \t family: {students[i][key][1]}")
            continue
        if key == 'birth_day':
            print(f"birth day: {students[i][key][0]}/{students[i][key][1]}/{students[i][key][2]}")
            continue
        print(f'{key}: {students[i][key]}')

In this example, complete information of a student including name (as a tuple) and courses (as a nested dictionary) is placed inside a larger dictionary, and this dictionary is added to the general list of students. Then, with the help of loops and defined conditions, the information is extracted and displayed in a detailed manner.


Challenge and Problem Solving for the Dictionary Section

Question: How can you filter the names of students who have scores above 15 from a dictionary of student scores (scores) and place them in a separate list?

scores = {"Ali": 18, "Sara": 20, "Reza": 12}
high_scorers = []

for name, score in scores.items():
    if score > 15:
        high_scorers.append(name)

print(high_scorers) # Output: ['Ali', 'Sara']

Using the items method, in each iteration of the loop we simultaneously access the key (name) and the value (score). The condition checks if the score is greater than 15, and if so, adds the person's name to the new list.


Set

A Set is a data structure with two fundamental characteristics: its elements are unordered and no duplicate members are allowed. You cannot say first or second member because positions are not stable and are constantly changing. This structure is excellent for situations where you want to immediately remove duplicate data. Set elements are defined inside curly braces {}.

Set Management Methods

  • add(x): Adds a new member x to the set.
  • discard(x) / remove(x): Removes a specified member from the set (unlike remove, the discard method does not throw an error if the member doesn't exist).

Examining Sample Codes for Sets

To add a new member to a set, we use the add method. If duplicate data is entered, Python automatically removes the extra values:

s = {1, 2, 3, 4, 5}
s.add(6)
print(s) # Output: {1, 2, 3, 4, 5, 6}


Challenge and Problem Solving for the Set Section

Question: If we have two sets, how can we implement the main mathematical operations (union, intersection, difference, and symmetric difference) on them?

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# 1. Find common members in both sets (intersection)
print(a & b) # Output: {3, 4}

# 2. Members that are in either set but not in both (symmetric difference)
print(a ^ b) # Output: {1, 2, 5, 6}

# 3. Members that are in the first set but not in the second (difference)
print(a - b) # Output: {1, 2}

# 4. Combine and unify all members of both sets (union)
print(a | b) # Output: {1, 2, 3, 4, 5, 6}


Continuous Input Until Exit

One of the most widely used patterns in programming is combining loop concepts with list data structures for dynamic and continuous collection of data from the user. Suppose you want to enter scores for a class but you don't know the number of students in advance. The solution is to create an infinite loop and define a specific number (like -1) as the exit condition:

score = []

while True:
    enter = int(input('Enter a Score(enter -1 to end): '))
    
    if enter == -1:
        break # Exit the loop immediately when -1 is entered
    
    score.append(enter) # Add the score to the scores list
	
print(f'Score: {score}')

The program continuously receives scores from the user and adds them to the list. This process continues until the user enters exactly -1. Upon entering -1, the break statement executes, the loop stops, and the final list of scores is displayed.


Session 5: Functions and Code Localization


The Concept of Function and Why We Use It

In the real world, to make a cup of coffee, you don't reinvent the coffee machine or assemble its parts every time. You press the machine's button, and the machine does the behind-the-scenes work and delivers the coffee to you.

In programming, a Function is exactly like that coffee machine. A function is a named block of code that you design once, write instructions inside it, and then wherever in your program you need that task, you just call its name.

Using functions gives us several major benefits:

  • We are saved from writing repetitive code and long copy-pastes.
  • If there's a mistake somewhere in our code, we only need to fix it inside that block, and the entire program gets fixed.
  • Our program becomes very clean, readable, and organized, as if it's closer to human language.

Defining and Calling a Function

To create a function in Python, we use the keyword def, which is short for "define". We put a name for the function in front of it and place a pair of parentheses. All the code that is supposed to go inside this block must have one level of indentation (spaces) so that Python understands these instructions belong to this function.

Until you call the function, the code inside it will not execute. Calling or invoking the function means bringing its name along with the parentheses later in the program.

def say_hello():
    print("Hello, welcome to the Python class!")

# Calling the function
say_hello()

When the program reaches the last line, it sees the function name, goes to the say_hello block, prints the message inside it, and returns to the main path of the program.


Input-Oriented Functions or Arguments

Imagine a juicer; if you give it oranges, it gives orange juice, and if you give it apples, it gives apple juice. Functions can also take inputs to perform different tasks based on those inputs. In programming terminology, these inputs are called parameters (when defining the function) or arguments (when calling the function and passing actual values).

Functions with One or Multiple Inputs

You can define as many variables as needed inside the function parentheses to serve as inputs. These variables only exist and work inside that specific function.

def greet(name):
    print("Hello", name, "have a nice day!")

greet("Ali")
greet("Sara")

In this code, once the word "Ali" goes into the variable name and the message is printed for Ali, and in the next round, this process repeats for "Sara".

If a function has multiple inputs, the order of passing values is very important:

def show_score(student_name, score):
    print(f"The Python score for {student_name} is: {score}")

show_score("Reza", 18)
show_score(20, "Maryam") # This line prints incorrectly and breaks the logic

In the first call, Reza is passed as the name and 18 as the score, which is perfectly correct. But in the next line, due to not following the order, the number 20 is introduced as the student's name, creating a logical error.


The Magic of Return Values or the Return Statement

This is one of the most important and crucial parts of learning functions. Suppose you ask your friend: "Write on the blackboard, what is 2 plus 2?" They do this, and you just see the result on the board. This is similar to the print statement.

Now suppose you tell your friend: "Add 2 and 2, keep the answer in your hand, and give it back to me so I can buy a car with that answer!" This is called returning or giving back a value, which in Python is done with the return statement.

When a function returns a value, that answer settles in the program line, and you can store it in a new variable, use it in subsequent calculations, or even pass it to another function.

Deep Dive into the Difference Between print and return

Let's see this big difference with a tangible piece of code:

def func_print():
    print("Hello")

def func_return():
    return "Hello"

x = func_print()
y = func_return()

print("Value stored in x:", x)
print("Value stored in y:", y)

The output of this code is fascinating: When func_print is called, the word "Hello" is printed on the screen. But this function doesn't give anything back to variable x! So variable x remains empty, and its value becomes None (meaning nothing). But when func_return executes, the word "Hello" is not printed on the screen; instead, this string is given as a gift to variable y. Now variable y owns the word "Hello" and can be easily printed or used in later lines.

A golden rule in Python: as soon as the program reaches the return statement inside a function, that function's work ends immediately and at that very moment, and it jumps out of the function. Any code written after return will never be executed.


Advanced Input Management

Python has designed very flexible tools for function inputs to give programmers freedom in passing information.

Default Arguments

Sometimes you want the function to consider a default value if the user doesn't pass a value for an input, so the program doesn't encounter an error.

def power(base, exp=2):
    return base ** exp

print(power(5))    
print(power(5, 3)) 

In the first call, we only passed one number (the base). Python notices that the second input hasn't been passed, so it automatically considers the number 2 for the exponent and raises 5 to the power of 2 (result 25). In the second call, since we passed the number 3 ourselves, the default value is ignored and 5 is raised to the power of 3 (result 125).

Keyword Arguments

If you remember, we said that in functions with multiple inputs, order is very important. But if you write the variable names when calling, the order no longer matters:

def display_info(name, job):
    print(f"Name: {name} | Job: {job}")

display_info(job="Programmer", name="Saleh")

Even though we wrote "job" first and "name" second, Python intelligently places the values in their correct positions based on the key names.


The Magic of Asterisks in Variable-Length Arguments

Sometimes you write a function where you don't know how many inputs the user is going to pass! For example, you want to create a function that calculates the average of grades; one user might enter 3 grades and another might enter 10. Python has two excellent tools for this using the asterisk symbol.

The Single-Asterisk Tool or *args

When you put an asterisk before a parameter name, you're telling Python: "Accept any number of values that come in and package them into a Tuple."

def calculate_average(*numbers):
    total = sum(numbers)
    return total / len(numbers)

print("First set average:", calculate_average(10, 20, 30))
print("Second set average:", calculate_average(18, 19, 15, 20, 14))

The function dynamically realizes that in the first call it has a 3-member tuple and in the second call it has a 5-member tuple, performing calculations without any issues.

The Double-Asterisk Tool or **kwargs

This tool is for when the user wants to send information as named pairs (key and value), and the count is also unspecified. Python packages this information into a Dictionary.

def save_user_profile(**info):
    print("Received data type:", type(info))
    for key, value in info.items():
        print(f"{key}: {value}")

save_user_profile(username="saleh_99", age=23, country="Iran")

The program converts all these inputs into a dictionary so you can loop through the keys and values and perform database operations.


Variable Scope: Local vs Global

Variables in Python have a specific scope or lifetime. Understanding this scope prevents many strange bugs in your program.

Local Variables

Any variable created inside a function is called local. This variable is like an employee inside a room; outside that room, no one knows them, and as soon as the function's work is done, that variable is also erased from the computer's memory.

def test_scope():
    inside_variable = 50
    print("Inside function:", inside_variable)

test_scope()
# print(inside_variable) -> This line gives a severe error because such a variable doesn't exist outside the function!

Global Variables and the global Keyword

Variables created in the main program space (outside all functions) are called global. All functions can see and read the value of this variable, but they cannot directly change its value!

If a function wants to manipulate a global variable outside itself and assign a new value to it, it must use the global keyword to tell Python that it intends to directly affect the original variable:

sugar_bags = 10 # Global variable

def hire_employee():
    sugar_bags = 20 # This is a new local variable with the same name and doesn't affect the outside variable

def hire_warehouse_manager():
    global sugar_bags
    sugar_bags = 50 # This command changes the main outside variable

hire_employee()
print("After employee:", sugar_bags) # Still 10

hire_warehouse_manager()
print("After warehouse manager:", sugar_bags) # Changed to 50


Recursive Functions: A Function That Calls Itself

Recursive functions are one of the fascinating and challenging topics in programming. Imagine a repeating image in a mirror that contains another mirror inside itself. A recursive function is a function that, within its own body, calls itself again!

To prevent these types of functions from getting stuck in an infinite loop and crashing the computer, having a Base Case is absolutely mandatory. That is, we must have a point where we say "That's enough, return."

Tangible Example: Calculating Factorial

We remember from math; factorial of 5 means (5 \times 4 \times 3 \times 2 \times 1). We can say factorial of 5 equals 5 times factorial of 4! This chain continues backwards until it reaches 1.

def factorial(n):
    # Critical base case
    if n == 1:
        return 1
    
    # Recursive step that calls itself again with a smaller number
    return n * factorial(n - 1)

print("Factorial of 5 is:", factorial(5))

When factorial(5) is called, Python calculates: 5 * factorial(4). But it doesn't know the answer for factorial of 4 yet! So it goes to calculate factorial of 4, which becomes 4 * factorial(3). This ladder is built downwards, and when it reaches 1, the base case executes, and the values are multiplied in a chain from bottom to top, giving the final answer of 120.


Exercises and Challenges

To make all the above concepts fully ingrained in your mind, let's solve some practical and realistic challenges together.

Challenge: Filtering and Calculating Grades

Question: Write a function that takes a list of student grades, removes grades below 10 (since they've failed!), and calculates and returns the average of the passing grades.

def clean_and_average(all_scores):
    passed_scores = []
    
    for score in all_scores:
        if score >= 10:
            passed_scores.append(score)
            
    if len(passed_scores) == 0:
        return 0 # If no one passed, the average is zero
        
    total = sum(passed_scores)
    return total / len(passed_scores)

scores_list = [18, 7, 15, 9, 20, 11]
final_result = clean_and_average(scores_list)
print("Average of passing students in the class:", final_result)

Challenge: Creating an Advanced Shopping Invoice with Variable-Length Arguments

Question: Write a function that takes a customer's name and then receives any number of products they've purchased (along with their prices). The function should print a neat invoice and return the total price.

def print_invoice(customer_name, **items):
    print(f"--- Invoice for Mr./Ms. {customer_name} ---")
    total_price = 0
    
    for item_name, price in items.items():
        print(f"Item: {item_name} \t Price: {price} Tomans")
        total_price += price
        
    print("---------------------------------------")
    return total_price

# Calling the function with different purchased items
final_bill = print_invoice("Saleh", laptop=35000000, mouse=450000, keyboard=1200000)
print(f"Final payable amount: {final_bill} Tomans")

Challenge: Countdown Using Recursion

Question: Write a function that takes a number and counts down to zero in reverse order, printing the characters, but without using for or while loops.

def countdown(number):
    # Base case
    if number < 0:
        print("Blast off!")
        return
        
    print("Number:", number)
    # Calling itself with a smaller number
    countdown(number - 1)

countdown(5)


Session 6: File Management and Structural Error Control


The Concept of Working with Files and Its Importance in Programming

So far, all the variables, lists, and dictionaries we created in our programs were temporary. Meaning that as soon as the program execution ended or we turned off the computer, all that data was erased from temporary memory (RAM).

In the real world, we need to store our data permanently. Working with files in Python allows us to write data to permanent storage (hard disk) so it persists, or read previously stored data and process it.


Types of Files and How to Open Them

Files are generally divided into two categories: Text files and Binary files. In this session, we will practice working with text files and structured files. For working with any file in Python, we follow three main steps: opening the file, performing operations (reading or writing), and closing the file.

The open function is used to open a file. This function takes two main arguments: the file name and the Mode for opening the file. The main modes are:

  • Mode r (Reading): Only for reading the file. If the file doesn't exist, the program throws an error.
  • Mode w (Writing): For writing to the file. If the file doesn't exist, it creates it. If it exists, it deletes all previous content and writes from scratch!
  • Mode a (Appending): For adding data to the end of the existing file without deleting previous content.

The Smart 'with' Statement

In the old method, if we forgot to close the file with the close command after opening it, the file would remain locked in system memory, and over time, the computer's memory would fill up. Python has introduced an extremely secure structure called with. When you write your code under the with block, as soon as your work is done or even if an error occurs in the middle of your program, Python automatically closes the file for you.


Working with Simple Text Files (txt)

Let's start with the simplest type of file: regular text files.

Writing to a Text File

Using the write method, we can write text into a file. To go to the next line, we use the \n character.

with open("students.txt", "w", encoding="utf-8") as file:
    file.write("Saleh Askari\n")
    file.write("Ali Moradi\n")
    file.write("Mohammad Nazari\n")

The encoding="utf-8" parameter tells Python that we want to store Persian characters correctly without corruption.

Reading from a Text File

There are different ways to read a file. The read method reads the entire file at once. However, the best and most efficient method for line-based files is to iterate over the file with a simple for loop. Python treats each line as an item:

with open("students.txt", "r", encoding="utf-8") as file:
    for line in file:
        # The strip method removes extra spaces and newlines at the end of the line
        print("Student name:", line.strip())


Introduction to the Popular JSON Structure

Simple text files are great for everyday note-taking, but they are not suitable for storing complex information (like a student's information along with their grades and courses). This is where the JSON format comes in. JSON stands for JavaScript Object Notation and is a global standard for transferring and storing data in web and mobile applications.

The appearance of the JSON structure is exactly like nested dictionaries and lists in Python. To work with this format, Python has a ready-made library called json that we need to import at the top of our program. The two main methods we work with are:

  • json.dump: Converts a Python dictionary or list to JSON structure and saves it in a file.
  • json.load: Reads a JSON file and directly converts it to a Python dictionary or list.

Storing Structured Information in a JSON File

Suppose we want to store a user's complete profile information in a categorized way:

import json

user_profile = {
    "username": "saleh_askari",
    "skills": ["Python", "Linux", "Network"],
    "is_active": True,
    "age": 23
}

with open("profile.json", "w", encoding="utf-8") as json_file:
    # The indent argument is used to save the JSON file with readable tabs and spaces
    json.dump(user_profile, json_file, indent=4)

If you open the profile.json file, you will see the data stored with excellent structural organization.

Reading Information from a JSON File

Now let's read the same JSON file and treat it as a dictionary in our program:

import json

with open("profile.json", "r", encoding="utf-8") as json_file:
    data = json.load(json_file)

print("Retrieved data type:", type(data))
print("Username:", data["username"])
print("First skill:", data["skills"][0])


Working with Tabular Files (CSV)

CSV files stand for Comma-Separated Values. These files store information in rows and columns, separating the items in each row with a comma. The biggest advantage of CSV files is that they can be opened directly in Excel or Google Sheets as clean tables.

Python also has a dedicated library called csv for these files.

Writing Tabular Data to a CSV File

Let's write a list of products and their prices into a tabular file:

import csv

products = [
    ["Product Name", "Price (Toman)", "In Stock"],
    ["Laptop", 35000000, "Yes"],
    ["Mouse", 450000, "Yes"],
    ["Keyboard", 1200000, "No"]
]

with open("inventory.csv", "w", newline="", encoding="utf-8") as csv_file:
    writer = csv.writer(csv_file)
    # The writerows method writes all nested lists as rows in the file
    writer.writerows(products)

Note: The newline="" parameter ensures that no extra blank lines are created between rows across different operating systems.

Reading Data from a CSV File

Now let's read the created table and display its information row by row:

import csv

with open("inventory.csv", "r", encoding="utf-8") as csv_file:
    reader = csv.reader(csv_file)
    for row in reader:
        # The row variable in each iteration is a list of cells in that row
        print(f"Product: {row[0]} \t| Price: {row[1]} \t| Stock: {row[2]}")


Error and Exception Management (Try, Except, Finally)

In the real world, things don't always go smoothly! Suppose you've written a program to read a file from the desktop, but the user has mistakenly deleted that file. Or you ask the user to enter their age, but they type "twenty" in letters. In these cases, your program crashes, shows scary red system error messages to the user, and stops working.

These runtime errors are called Exceptions. A professional programmer must anticipate and manage these errors so that even if a problem occurs, the program continues smoothly and displays an appropriate message to the user. Python's tool for this is the try and except blocks.

Structure of the Error Management Block

  • try block: We write code that is suspicious of errors (like opening a file or getting user input) in this section.
  • except block: If an error occurs in the try section, Python immediately stops executing the code above and moves to this section. Here we specify what response to show in case of any error.
  • finally block: This section is completely loyal! Whether an error occurs in the try section or the program runs perfectly, the code inside finally will be executed under any circumstances at the end. This is a great place for tasks like closing network connections or databases.

Practical Example of Error Management in Data Conversion and Files

Let's write a program that manages both file-not-found errors and calculation errors:

try:
    with open("numbers.txt", "r") as file:
        content = file.read()
        number = int(content) # Potential error in converting text to number
        result = 100 / number # Potential error in division by zero
        print("Calculation result:", result)

except FileNotFoundError:
    print("Error: The file numbers.txt was not found on the system!")

except ValueError:
    print("Error: The content inside the file is not a valid number!")

except ZeroDivisionError:
    print("Error: The number inside the file is zero, and division by zero is not possible!")

except Exception as error:
    # This section acts as a general safety net for other unpredicted errors
    print(f"An unexpected error occurred: {error}")

finally:
    print("File checking operation completed.")

With this structure, our program will never crash under any circumstances and will display an appropriate Persian message to the user based on each scenario.

Some famous errors you should know:

ValueError      # Invalid value
TypeError       # Wrong data type
IndexError      # Index out of range
KeyError        # Key not found
FileNotFoundError # File not found
ZeroDivisionError # Division by zero
NameError       # Variable not defined
AttributeError  # Attribute or method doesn't exist
Exception       # Parent of most errors

Challenges and Exercises

To fully solidify the concepts of this session, we will examine three combined and advanced challenges with line-by-line analysis of the solution approach.

Challenge: Registration and Login System with Text File

Question: Write a program that has a menu including "Register National IDs" and "Display People". The program should save entered national IDs in a text file. If the user enters a duplicate national ID, the system should prevent registration and show an error. Also, if the file doesn't already exist, the program should not crash.

Solution Approach and Code Logic Explanation: To solve this problem, we first need to check if the file exists. The best way is to open the file in read mode (r); if the file doesn't exist, a FileNotFoundError occurs, which we handle in the except section and create the file for the first time. To prevent duplicates, we read all national IDs in the file and store them in a Set (since sets don't have duplicate members and searching is fast in them). If the new national ID is not in this set, we append it to the end of the file using append mode (a).

def load_national_codes():
    codes = set()
    try:
        with open("database.txt", "r", encoding="utf-8") as file:
            for line in file:
                codes.add(line.strip())
    except FileNotFoundError:
        # If the file doesn't exist, it means no one has registered yet, so we create an empty file
        with open("database.txt", "w", encoding="utf-8") as file:
            pass
    return codes

def register_user(new_code):
    existing_codes = load_national_codes()
    
    if new_code in existing_codes:
        print("Error: This national ID is already registered in the system!")
        return
        
    with open("database.txt", "a", encoding="utf-8") as file:
        file.write(new_code + "\n")
    print("Your registration was successful.")

# Testing the program
register_user("1234567890")
register_user("1234567890") # This time it will show duplicate message

Challenge: Data Backup and Conversion from CSV to JSON

Question: We have a store information file in tabular format (CSV). Write a program that reads this file, calculates the price of each product with 9% value-added tax, and saves the final output as a modern structured file (JSON) for backup.

Solution Approach and Code Logic Explanation: In the first step, we open the tabular file with the csv library. Since the first row contains headers (product name, price), we need to skip it with the next command so it doesn't enter mathematical calculations. Then we move row by row, convert the price (which is in text format) to a decimal number, apply tax to it, create a dictionary for each product, and add it to a main list. In the final step, we save this list of dictionaries in a new file with json.dump.

import csv
import json

def convert_csv_to_json(csv_filename, json_filename):
    data_list = []
    
    try:
        with open(csv_filename, "r", encoding="utf-8") as csv_file:
            reader = csv.reader(csv_file)
            # Skipping the first row (headers)
            header = next(reader)
            
            for row in reader:
                item_name = row[0]
                base_price = float(row[1])
                # Calculating price with 9% tax
                final_price = base_price * 1.09
                
                # Creating a dictionary for each product
                product_dict = {
                    "product_name": item_name,
                    "original_price": base_price,
                    "tax_included_price": round(final_price, 2)
                }
                data_list.append(product_dict)
                
        # Writing the output to JSON file
        with open(json_filename, "w", encoding="utf-8") as json_file:
            json.dump(data_list, json_file, indent=4, ensure_ascii=False)
        print("Format conversion and tax calculation completed and saved successfully.")
        
    except FileNotFoundError:
        print("Error: The source CSV file was not found!")
    except ValueError:
        print("Error: Some prices in the file are not valid numbers!")

# Simulating the challenge execution
# For actual execution, the inventory.csv file from previous challenges should exist first.
convert_csv_to_json("inventory.csv", "backup_products.json")

Challenge: Crash-Proof Calculator with Continuous Input Receiving

Question: Write a program that in an infinite loop, takes two numbers and a mathematical operator (addition, subtraction, multiplication, division) from the user and prints the result. The program should be hardened with error management so that if the user enters text instead of numbers, or tries to divide by zero, the program displays an appropriate message and without exiting the loop, asks for correct input again. The user can exit the program by typing "exit".

Solution Approach and Code Logic Explanation: We write the input receiving commands inside a while True loop. To prevent the program from crashing with incorrect inputs, we put the entire calculation and receiving process inside a try block. If the user types "exit" at any stage, we escape the loop with the break command. Otherwise, we convert the values to numbers. The ValueError catches text-to-number conversion errors, and ZeroDivisionError catches division by zero errors, and the loop continues to the next iteration without interruption.

print("--- Welcome to the Python Secure Calculator (type 'exit' to quit) ---")

while True:
    try:
        user_input1 = input("First number: ").strip()
        if user_input1.lower() == "exit":
            break
            
        operator = input("Mathematical operator (+ , - , * , /): ").strip()
        
        user_input2 = input("Second number: ").strip()
        if user_input2.lower() == "exit":
            break
            
        # Converting inputs to floating point numbers
        num1 = float(user_input1)
        num2 = float(user_input2)
        
        # Performing calculations based on operator
        if operator == "+":
            print("Result:", num1 + num2)
        elif operator == "-":
            print("Result:", num1 - num2)
        elif operator == "*":
            print("Result:", num1 * num2)
        elif operator == "/":
            print("Result:", num1 / num2)
        else:
            print("Error: The entered operator is invalid!")
            
        print("-" * 30)
        
    except ValueError:
        print("Input Error: Please only use numbers! Try again.")
        print("-" * 30)
    except ZeroDivisionError:
        print("Math Error: Division by zero is not possible! Try again.")
        print("-" * 30)

print("Thank you for using our program. Exiting.")


Session 7: Object-Oriented Programming (OOP)


Class and Object Concept

Until now, we have been writing our programs in a structured or functional way; that is, we defined a number of variables and created functions that perform operations on these variables. This method is excellent for small programs, but when a project becomes large, managing hundreds of scattered variables and functions turns into a nightmare.

Object-Oriented Programming, abbreviated as OOP, is a new perspective on the world of coding. In this method, we try to simulate the real world. In the real world, we deal with "objects": cars, humans, laptops, or even a bank account. Each of these objects has two main characteristics:

  • Attributes: Data or information related to that object (e.g., car color, human name).
  • Methods: Actions that the object can perform (e.g., car moving, human speaking).

What is the difference between a class and an object?

Imagine a car manufacturing factory. Engineers first design a blueprint or initial template for the car. This blueprint is not a car itself; you cannot ride the blueprint and drive it! But the factory can use this blueprint to produce thousands of actual cars with different colors and specifications.

In Python programming:

  • Class: The blueprint, template, or general instruction manual.
  • Object: The actual car built from that blueprint. The process of creating an object from a class is called instantiation.

Creating the First Class and Object in Python

To create a class, we use the class keyword. According to a convention among programmers, we start class names with a capital letter.

class Car:
    pass # We'll leave the class body empty for now

Now let's create two actual objects or cars from this template:

car1 = Car()
car2 = Car()

print(type(car1)) # Output shows that this is an object of the Car class


Class Constructors and the init Method

When a car leaves the factory, right from the first moment it has a color, its model is determined, and its number of gears is set. We cannot build a car that has no specifications.

In Python, to set the initial specifications of an object as soon as it is created, we use a special and magical function called the Constructor. Its name is always fixed: __init__ (two underscores at the beginning and two underscores at the end).

Understanding the use of the self keyword

When you write code related to a class, how does Python know which car's attributes to modify? This is where the keyword self comes in. self refers to the same object that is currently being created or operated on. When we say self.color, it means the color of "this very car you are building".

class Car:
    # Constructor function with initial specifications
    def __init__(self, brand, model, color):
        self.brand = brand   # Car brand
        self.model = model   # Car model
        self.color = color   # Car color
        self.speed = 0       # Initial speed of all cars is zero

# Now when creating an object, we pass the specifications
my_car = Car("Toyota", "Corolla", "White")
friend_car = Car("BMW", "X5", "Black")

print(my_car.brand)     # Output: Toyota
print(friend_car.brand) # Output: BMW


Class Attributes and Behaviors (Methods)

The attributes we created inside __init__ are object variables. Now it's time to add "behavior" to our class. Behaviors of a class are functions written inside the class, and they are called Methods. The only difference between these functions and regular functions is that their first parameter must always be self so they can access the object's own data.

class Car:
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color
        self.speed = 0
    
    # Method to display information
    def show_info(self):
        print(f"Car: {self.brand} {self.model} | Color: {self.color} | Speed: {self.speed}")
        
    # Method to accelerate and increase speed
    def accelerate(self, amount):
        self.speed += amount
        print(f"Car accelerated! Current speed: {self.speed}")

# Using the methods
my_car = Car("Persia", "Pars", "Gray")
my_car.show_info() 

my_car.accelerate(40)
my_car.accelerate(20)
my_car.show_info()


Inheritance

Suppose you want to create a new class for "Truck" and a class for "Motorcycle". Trucks and motorcycles, like cars, have features such as brand, model, color, and speed, and they also perform the action of accelerating. Is it logical to copy all the code we wrote for the Car class and write it again for the truck? Not at all!

In object-oriented programming, there is a wonderful feature called Inheritance. Inheritance allows us to create a general class (parent or base class) and then create more specialized classes (child or derived classes) from it. The child class inherits all the attributes and behaviors of the parent class, and it can also have its own new features.

Implementing Inheritance and the super Keyword

Let's create a base class called Vehicle and make the car and truck its children:

# Parent class
class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        
    def honk(self):
        print("Beep beep!")

# Child class (the class inside parentheses means who it inherits from)
class Truck(Vehicle):
    def __init__(self, brand, model, capacity):
        # The super command passes specifications to the parent class constructor
        super().__init__(brand, model)
        self.capacity = capacity # Truck-specific feature (load capacity)
        
    def load_cargo(self):
        print(f"Truck loaded. Capacity: {self.capacity} tons")

# Creating an object from the child class
heavy_truck = Truck("Volvo", "FH500", 25)

# Inherited method from parent
heavy_truck.honk() 

# Child's own specific method
heavy_truck.load_cargo()


Polymorphism

The word polymorphism simply means "a consistent behavior with different execution forms".

Suppose we have a "play sound" button. If we press this button on a "Cat" object, it makes a meow sound. If we press the same button on a "Dog" object, it makes a woof sound. The method name is the same in both classes (make_sound), but each performs differently based on its own nature.

In inheritance, the child can change the parent's behavior, which is called Method Overriding.

class Animal:
    def make_sound(self):
        print("Unspecified animal sounds")

class Cat(Animal):
    # Overriding the parent method
    def make_sound(self):
        print("Meow meow!")

class Dog(Animal):
    # Overriding the parent method
    def make_sound(self):
        print("Woof woof!")

# A function that takes an animal object and produces its sound
def animal_concert(animal_object):
    animal_object.make_sound()

# Creating objects
my_cat = Cat()
my_dog = Dog()

# Polymorphism in action: a fixed function with different inputs produces different results
animal_concert(my_cat) # Output: Meow meow!
animal_concert(my_dog) # Output: Woof woof!


Challenges and Exercises

To become fully proficient in these advanced concepts, we will review and solve three realistic and combined challenges line by line.

Challenge: Secure Bank Account Management System

Question: Create a class called BankAccount that takes the account holder's name and initial balance. Design methods for "deposit", "withdraw", and "display balance". In the withdraw method, ensure that if the balance is insufficient, it does not allow the withdrawal and prints an error message.

Solution approach and code explanation: In the class constructor, we store the account holder's name and balance. In the deposit method, we add the amount to the balance. In the withdraw method, we first put a condition with if; if the requested amount is greater than the current balance (self.balance), we print a warning message and stop the operation; otherwise, we deduct the amount.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        
    def deposit(self, amount):
        self.balance += amount
        print(f"Amount {amount} Tomans deposited. New balance: {self.balance}")
        
    def withdraw(self, amount):
        if amount > self.balance:
            print(f"Error: Insufficient balance! Your current balance: {self.balance} Tomans")
        else:
            self.balance -= amount
            print(f"Amount {amount} Tomans withdrawn. New balance: {self.balance}")
            
    def display(self):
        print(f"Account holder: {self.owner} | Balance: {self.balance} Tomans")

# Testing the banking system
account = BankAccount("Saleh Askari", 500000)
account.deposit(200000)
account.withdraw(800000) # Gives an error because balance is 700,000 Tomans
account.withdraw(300000) # Executes successfully

Challenge: Employee Payroll System with Inheritance

Question: Create a base class called Employee that takes the name and employee ID. Then create two child classes called FullTimeEmployee (fixed monthly salary) and PartTimeEmployee (hourly salary: based on hours worked and hourly rate). Both child classes should have a method called calculate_salary to calculate the salary (polymorphism).

Solution approach and code explanation: The parent class records general information. The first child class takes a feature called monthly_salary and simply returns it. The second child class takes two specific features called hours_worked and hourly_rate. The salary calculation method in the second child multiplies these two numbers together. This is a complete example of combining inheritance and polymorphism.

class Employee:
    def __init__(self, name, id):
        self.name = name
        self.id = id

class FullTimeEmployee(Employee):
    def __init__(self, name, id, monthly_salary):
        super().__init__(name, id)
        self.monthly_salary = monthly_salary
        
    def calculate_salary(self):
        return self.monthly_salary

class PartTimeEmployee(Employee):
    def __init__(self, name, id, hours_worked, hourly_rate):
        super().__init__(name, id)
        self.hours_worked = hours_worked
        self.hourly_rate = hourly_rate
        
    def calculate_salary(self):
        return self.hours_worked * self.hourly_rate

# Creating objects and calculating salaries
emp1 = FullTimeEmployee("Mohammad Moradi", "101", 15000000)
emp2 = PartTimeEmployee("Ali Shafiei", "102", 80, 120000)

print(f"Salary of Mr. {emp1.name}: {emp1.calculate_salary()} Tomans")
print(f"Salary of Mr. {emp2.name}: {emp2.calculate_salary()} Tomans")

Challenge: Roles and Permissions System in a Website

Question: Create a class called User that records username and email, and has a method called show_permissions that prints "Permission: read-only access". Then create a child class called Admin that, in addition to inheritance, overrides the permissions method to print "Permission: full site and user management". The admin should also have a specific method called delete_comment to delete comments.

Solution approach and code explanation: This challenge shows us how a child class can both customize (override) old behaviors and add completely new tools that regular users do not have access to.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email
        
    def show_permissions(self):
        print(f"User {self.username} -> Permission: read-only access")

class Admin(User):
    def __init__(self, username, email, admin_level):
        super().__init__(username, email)
        self.admin_level = admin_level # Management level
        
    # Overriding the parent method (polymorphism)
    def show_permissions(self):
        print(f"Admin {self.username} (Level {self.admin_level}) -> Permission: full site management")
        
    # Admin-specific method
    def delete_comment(self, comment_id):
        print(f"Admin {self.username} deleted comment with ID {comment_id}.")

# Checking permissions
user_simple = User("saleh_user", "saleh@example.com")
user_admin = Admin("narjes_admin", "narjes@example.com", "Senior")

user_simple.show_permissions()
user_admin.show_permissions()

# A regular user does not have this method and the program would error if called
user_admin.delete_comment(2044)


Session 8: Modules, Libraries, and pip Tool


Concept of Module, Package, and Library

So far, all the code we've written has been in a single file. But imagine you're developing a large program with thousands of lines of code. If you leave all this code in one file, finding a specific line or fixing an error will be extremely difficult.

Python has introduced a code management and separation structure to solve this problem. Let's learn these concepts with a real and very simple analogy:

  • Module: A simple Python file with a .py extension that contains some ready-to-use functions or variables. A module is like a "specific tool" (for example, a wrench or screwdriver) in your toolbox.
  • Package: A collection of several related modules (files) placed inside a folder. A package is like a "specific drawer or section" of your large toolbox that collects similar tools together.
  • Library: A very large and comprehensive collection of various packages and modules gathered together to solve a broad category of problems (like graphics, mathematics, or game development). A library is like the "workbench or giant toolbox" itself.

Import Statements and Different Import Methods

To be able to use code written in another module or library, we need to import them into our current program. Python uses the import keyword for this, which can be written in several different ways.

Method 1: Importing the Entire Module

In this method, you import the entire file or library. To use the tools inside it, you must first write the module name and then a dot (.):

import math

# Using the square root function from the math library
result = math.sqrt(16)
print(result) # Output: 4.0

Method 2: Importing a Specific Tool (From ... Import)

If you only need one or a few specific functions and don't want to load the entire library, you use this method. In this case, you no longer need to write the library name and dot before the function:

from math import sqrt, pi

print(sqrt(25)) # Output: 5.0
print(pi)       # Output: 3.1415

Method 3: Using an Alias (As)

Sometimes a module or library name is too long, or you want to shorten its name in your program to speed up coding:

import datetime as dt

# Getting the current system time
now = dt.datetime.now()
print(now)


Creating Our Own Custom Module

The biggest advantage of modules is that you can create them yourself! Let's make a small tool for geometric calculations.

Create a file named geometry.py and put the following code in it (this file is our module):

# Contents of geometry.py file
PI = 3.1415

def circle_area(radius):
    return PI * (radius ** 2)

def rectangle_area(width, height):
    return width * height

Now create a new file in the same folder called main.py (the main program) and call your module in it:

# Contents of main.py file
import geometry

area1 = geometry.circle_area(5)
area2 = geometry.rectangle_area(4, 6)

print(f"Area of circle: {area1}")
print(f"Area of rectangle: {area2}")

Just like that, you've categorized and separated your code!


What is the pip Download Tool?

Python is an endless ocean. Millions of programmers around the world have written amazing code and made it available for free on the internet for others to download. But how do we install these libraries on our computer?

The pip tool (short for Pip Installs Packages) is actually the large marketplace or store of Python (similar to a bazaar, Google Play, or App Store on your phone). You don't need to visit different websites and download files; you just need to know the name of the library you want.

How to Use pip

To use pip, you shouldn't write code inside the Python environment! Instead, you need to open the terminal (Terminal) or Command Prompt (CMD) on Windows and type the following commands:

  • Command to install a new library:
pip install library_name

(Instead of library_name, write the name of the desired library, for example, turtle).

  • Command to uninstall a library:
pip uninstall library_name

  • Viewing the list of all libraries installed on your system:
pip list


Introduction to Standard and Exciting Libraries to Get Started

Python comes with some extremely entertaining built-in tools that don't require installation and make understanding code logic very engaging and visual for beginners.

The Magical and Visual Turtle Library (Turtle Painter)

The turtle library is one of the best tools for learning Python. Python gives you a virtual turtle with a pen attached to its tail! You give it commands with your code to move forward, turn, and draw on the screen. This library greatly helps in understanding loops and functions.

import turtle

# Creating a painter turtle
my_turtle = turtle.Turtle()
my_turtle.shape("turtle") # Make our painter look like a turtle
my_turtle.color("red")    # The pen color becomes red

# Command to the turtle to draw a square with a for loop
for i in range(4):
    my_turtle.forward(100) # Go forward 100 steps
    my_turtle.right(90)    # Turn 90 degrees to the right

# Keeping the drawing window open after completion
turtle.done()

The Random Library (Generating Random Choices)

This library is extremely practical and tangible for building number guessing games, lotteries, or chance simulations.

import random

# 1. Generating a random integer between 1 and 6 (dice simulation)
dice = random.randint(1, 6)
print(f"Your dice number: {dice}")

# 2. Randomly selecting a classmate for a presentation
students = ["Ali", "Sara", "Reza", "Maryam"]
selected = random.choice(students)
print(f"Selected person: {selected}")

The Creative Cowsay Library (Talking Cow - Requires pip installation)

Let's use the pip tool we learned about! First, go to CMD or terminal on your computer and type: pip install cowsay. Now this fun library has been added to your Python and you can write funny code with it:

import cowsay

# Command to a cartoon cow to say your words!
cowsay.cow("Hello! I'm glad you're learning Python!")


Challenges and Classroom Solved Exercises

To fully solidify the concepts of this session, we'll review three complete programming challenges appropriate for beginners with step-by-step solutions.

Challenge: Drawing Regular Polygons with Turtle and Functions

Question: Write a function that takes the number of sides of a geometric shape (e.g., triangle, square, pentagon) along with the side length from the user and automatically draws the shape using the turtle library.

Solution method and code logic explanation: The mathematical formula for rotation in polygons is: the sum of exterior angles is always 360 degrees. So if we divide 360 by the number of sides, we get the exact turning angle for the turtle. We repeat this logic inside a for loop for the number of sides to complete the shape.

import turtle

def draw_shape(sides, side_length):
    my_turtle = turtle.Turtle()
    my_turtle.color("blue")
    
    # Calculating the rotation angle
    angle = 360 / sides
    
    for i in range(sides):
        my_turtle.forward(side_length)
        my_turtle.left(angle) # Turn left based on the calculated angle

# Test: Draw a regular hexagon with side length of 70 steps
draw_shape(6, 70)

turtle.done()

Challenge: Rock, Paper, Scissors Game Against the Computer

Question: Write a program that simulates the famous Rock, Paper, Scissors game. The user enters their choice and the computer makes a random choice using the random library, then announces the winner.

Solution method and code logic explanation: We put the options in a list. We determine the computer's choice with random.choice(). We compare the user's input with the computer's choice using if and elif structures to determine the winner, loser, or tie.

import random

def play_game():
    options = ["Rock", "Paper", "Scissors"]
    
    print("--- Welcome to Rock, Paper, Scissors ---")
    user_choice = input("Your choice (Rock, Paper, Scissors): ").strip()
    
    if user_choice not in options:
        print("Invalid choice!")
        return
        
    # Computer's random choice
    computer_choice = random.choice(options)
    print(f"Computer's choice: {computer_choice}")
    
    # Checking the winning logic
    if user_choice == computer_choice:
        print("Result: It's a tie!")
    elif (user_choice == "Rock" and computer_choice == "Scissors") or \
         (user_choice == "Paper" and computer_choice == "Rock") or \
         (user_choice == "Scissors" and computer_choice == "Paper"):
        print("Congratulations! You won!")
    else:
        print("Computer won! Try again.")

# Run the game
play_game()

Challenge: Drawing a Magical Colorful Circle

Question: Using a for loop and your creativity, make the painter turtle draw many circles with slightly different angles to create a very beautiful artistic graphic.

Solution method and code logic explanation: We can draw a circle with radius 80 using the my_turtle.circle(80) method. If we put this inside a loop and rotate the turtle's heading by, say, 10 degrees after drawing each circle, the next circle will be drawn at a new angle, creating a unique flower-like pattern.

import turtle

def draw_magic_circles():
    window = turtle.Screen()
    window.bgcolor("black") # Set background to black to make the drawing more attractive
    
    artist = turtle.Turtle()
    artist.speed(0) # Set turtle speed to the fastest possible
    artist.color("cyan")
    
    # Drawing 36 circles in different directions
    for i in range(36):
        artist.circle(80)   # Drawing a circle
        artist.left(10)     # Change direction by 10 degrees for the next circle
        
    turtle.done()

# Running the artistic challenge
draw_magic_circles()


Session 9: Introduction to Python in the Data World (NumPy, Pandas, and Matplotlib)

The Concept of Data Analysis and Why We Need New Tools?

So far, we have been using Python's default data structure, the list, for storing collections. Lists are great for general purposes, but when we are dealing with millions of data points (like sales information from a large store or grades of all students in a country), two major problems arise:

  • Low Speed: Python lists are very slow for heavy and million-level mathematical calculations.
  • Lack of Computational Tools: If you want to calculate the average, the highest value, or plot a list, you have to write many loops and conditions yourself.

In this session, we will get to know three incredibly popular tools that have made Python the most powerful language for working with data.

Do you remember that in the previous session we got familiar with the pip tool? To use these three libraries, you first need to open your computer's terminal or CMD and write this simple command to install all three at once:

pip install numpy pandas matplotlib


NumPy Library (Fast Numerical Computing)

The NumPy library (short for Numerical Python) is a tool that allows us to create lists of numbers that perform mathematical calculations at an incredible speed (up to 100 times faster than regular Python lists). These advanced lists are called Arrays.

By standard convention, programmers import this library with the alias np.

Creating a Numeric Array and Performing Calculations on It

In regular Python lists, if you add two lists with the + operator, they get concatenated. But in NumPy, operations are performed mathematically and element-wise:

import numpy as np

# Creating two simple numeric arrays
scores_term1 = np.array([15, 18, 12, 20])
scores_term2 = np.array([17, 16, 15, 19])

# Adding the scores of two terms element-wise and mathematically
total_scores = scores_term1 + scores_term2
print("Total scores:", total_scores)  # Output: [32 34 27 39]

# Multiplying all scores by 2 simultaneously
doubled_scores = scores_term1 * 2
print("Doubled scores:", doubled_scores)  # Output: [30 36 24 40]

Ready-made Mathematical Tools in NumPy

You don't need to write loops to calculate the average or find the highest score; NumPy has ready-made methods for these:

import numpy as np

prices = np.array([5000, 12000, 8500, 23000, 11000])

print("Average price:", np.mean(prices))  # Automatically calculates the average of prices
print("Maximum price:", np.max(prices))   # Finds the highest price
print("Minimum price:", np.min(prices))   # Finds the lowest price


Pandas Library (Data Management in Tabular Form)

If NumPy is for working with a line of numbers, the Pandas library is for working with tables! Pandas allows you to import a file like Excel into Python, name its rows and columns, and interact with it exactly like a real table.

In Pandas, these tables are called DataFrames. Pandas is usually imported with the alias pd.

Creating a Simple Table (DataFrame) in Python

Let's create a table of product specifications and warehouse inventory using a dictionary:

import pandas as pd

# Our initial data structured in a dictionary
data = {
    "product_name": ["Laptop", "Mouse", "Keyboard", "Monitor"],
    "price": [35000000, 450000, 1200000, 8500000],
    "stock": [5, 20, 15, 8]
}

# Converting the dictionary to a Pandas table (DataFrame)
df = pd.DataFrame(data)

# Displaying the entire table in the program output
print(df)

The output of this code will be a very clean and organized table as shown below:

  product_name     price  stock
0       Laptop  35000000      5
1        Mouse    450000     20
2     Keyboard   1200000     15
3      Monitor   8500000      8

Filtering and Extracting Information from the Table

Pandas allows you to easily extract specific parts of the table:

import pandas as pd

# Displaying only the column containing product names
print(df["product_name"])

# Filtering products that have more than 10 items in stock
high_stock_items = df[df["stock"] > 10]
print(high_stock_items)


Matplotlib Library (Plotting and Visualization)

Looking at dry and empty numbers in a table is not always appealing. Humans understand data much better when they see charts. The Matplotlib library is the drawing and plotting tool in Python. We usually use the pyplot module from it with the alias plt.

Plotting a Simple Line Chart

import matplotlib.pyplot as plt

# Data for days of the week and daily sales
days = ["Sat", "Sun", "Mon", "Tue", "Wed"]
sales = [12, 19, 15, 25, 22]

# Plotting a simple line chart
plt.plot(days, sales, marker='o', color='green')

# Adding the main title and labels for horizontal and vertical axes to the chart
plt.title("Daily Sales Performance")
plt.xlabel("Days of the Week")
plt.ylabel("Number of Sales")

# Finally displaying the chart on the computer screen
plt.show()


Importing a Real Dataset, Formatting, and Data Analysis

Now let's combine all these tools in a real-world scenario. Suppose we have a file named students_report.csv that stores the grades of students in a class. We want to import this file, examine its status, add a new column to it, and finally plot its chart.

Step One: Reading the CSV File

Using the pd.read_csv command, we can directly convert the file into a table:

import pandas as pd
import matplotlib.pyplot as plt

# Loading and reading the Excel or CSV file in Python
df = pd.read_csv("students_report.csv")

# Displaying only the first 2 rows as a sample to understand the file structure
print(df.head(2))

Step Two: Formatting and Data Analysis (New Column and Filtering)

Suppose we want to add 2 points as a bonus to all students' scores and find those whose final score is above 15:

import pandas as pd

# Adding a new calculated column called final score
df["final_score"] = df["actual_score"] + 2

# Filtering and finding the top students of the class
top_students = df[df["final_score"] >= 15]
print("--- Top Students ---")
print(top_students[["name", "final_score"]])

Step Three: Plotting a Bar Chart of Student Scores

import matplotlib.pyplot as plt

# Plotting a bar chart for better comparison of the final scores of the entire class
plt.bar(df["name"], df["final_score"], color="skyblue")

# Beautifying and naming different parts of the chart
plt.title("Class Final Scores Report")
plt.xlabel("Student Name")
plt.ylabel("Scores")

# Finally displaying the chart window
plt.show()


Challenges and Exercises

To consolidate these concepts, we will solve three highly practical challenges in the world of data analysis.

Challenge: Automatic Profit Calculation with NumPy

Question: An array of "purchase prices" for 5 items and another array of "selling prices" for the same 5 items are given. Using NumPy arrays, create a new array that shows the profit for each item. Then determine what the maximum profit was?

Solution Approach and Code Logic Explanation: First, we convert the lists to NumPy arrays. Since arrays understand element-wise calculations, by subtracting the purchase array from the sales array, we get the profits array. Finally, we use the np.max() method to find the highest profit.

import numpy as np

# Entering the list of purchase and sale prices in thousands of Tomans
purchase = np.array([100, 250, 80, 400, 150])
sale = np.array([130, 290, 110, 450, 180])

# Calculating the profit for each item simultaneously and element-wise
profits = sale - purchase
print("Profits per item:", profits)  # Output: [30 40 30 50 30]

# Finding the highest profit earned
max_profit = np.max(profits)
print("Maximum profit earned:", max_profit)

Challenge: Managing Employee Information and Filtering Salaries with Pandas

Question: Create the following employee information table in Pandas. Write a program that finds employees who work in the "Sales" department and have a salary above 12 million, and prints their names.

name  | department | salary
Ali   | Sales      | 14000000
Sara  | Finance    | 16000000
Reza  | Sales      | 10000000
Mary  | Sales      | 15000000

Solution Approach and Code Logic Explanation: First, we create the table with pd.DataFrame. To filter with two conditions simultaneously, we put each condition in parentheses and use the & operator (meaning logical "and" in Pandas).

import pandas as pd

# Creating an initial dictionary for employee information
employees_data = {
    "name": ["Ali", "Sara", "Reza", "Mary"],
    "department": ["Sales", "Finance", "Sales", "Sales"],
    "salary": [14000000, 16000000, 10000000, 15000000]
}

# Converting the dictionary to a Pandas DataFrame
df = pd.DataFrame(employees_data)

# Applying two conditions simultaneously: 1. Department must be Sales 2. Salary greater than 12 million
filtered_df = df[(df["department"] == "Sales") & (df["salary"] > 12000000)]

# Printing the final output of qualified people's names
print("Qualified Employees:")
print(filtered_df["name"])

Challenge: Clean Air Analysis and Visualization with Charts

Question: Suppose you have a table of the average air pollution index of a city for the first 5 months of the year. You want to find the month with the worst pollution using Pandas and plot a line chart of the air pollution trend.

month | aqi_index
Month1 | 65
Month2 | 80
Month3 | 110
Month4 | 145
Month5 | 130

Solution Approach and Code Logic Explanation: We first create the table and use the max() method to find the highest index (the higher the AQI index, the more polluted the air). Then we plot a line chart using the Matplotlib library and customize its appearance with dashed red lines to convey a warning state.

import pandas as pd
import matplotlib.pyplot as plt

# Defining the structure of information for months and air quality index
weather_data = {
    "month": ["Month1", "Month2", "Month3", "Month4", "Month5"],
    "aqi_index": [65, 80, 110, 145, 130]
}

# Converting the data to a table
df = pd.DataFrame(weather_data)

# Finding the highest pollution index value in the entire column
worst_aqi = df["aqi_index"].max()

# Finding the name of the month to which this highest index belongs
worst_month = df[df["aqi_index"] == worst_aqi]["month"].values[0]
print(f"Worst month: {worst_month} with AQI: {worst_aqi}")

# Plotting the pollution trend chart as a dashed red line with X markers for key points
plt.plot(df["month"], df["aqi_index"], marker='X', linestyle='--', color='red')

# Setting visual features of the chart for better readability
plt.title("Air Quality Index Trend")
plt.xlabel("Months")
plt.ylabel("AQI Score")
plt.grid(True)  # Enabling background grid lines

# Displaying the graphical chart window to the user
plt.show()


Session 10: Final Project of Python Course: Developing an Intelligent Dashboard for Habit Management (Smart Habit Tracker)

Project Description

In this final project, you are going to design a completely real and practical system for daily life. The goal of this project is to implement a Habit Tracker and Daily Task Manager based on Object-Oriented Programming (OOP) architecture under the terminal. This program is connected to a permanent storage file and ultimately analyzes your data as a graphical and stylish chart.


Educational Objectives and Topics Assessed in This Project

By implementing this program, all the topics you have learned throughout the course will be connected and consolidated in a chain-like manner:

  • Object-Oriented Programming (OOP): Building a single class for modeling each habit along with its attributes and internal methods.
  • Loop and Conditional Structures: Managing the main program menu live and continuously (while True and if-elif).
  • Data Structures and Lists: Managing and maintaining constructed objects within a main list.
  • File Management (File I/O): Permanently storing information in a CSV file so that data is not lost when the program is closed.
  • Error Management (Exception Handling): Handling incorrect user inputs (try-except) to prevent the program from crashing.
  • Data Analysis and Visualization (Pandas & Matplotlib): Loading the repository table and drawing a comparative bar chart.

Data Structure and Storage Architecture

Your program should store information in a text file named habits.csv. Each habit has 3 main attributes that are defined in the class:

  1. habit_name: The name of the habit (e.g., Reading or Gym).
  2. target: Your goal (how many times per month you should do this, e.g., 20 times).
  3. done: Current progress (how many times it has been done so far, e.g., 12 times).

The data storage format in the file must be exactly as follows (separated by commas):

habit_name,target,done
Reading,20,12
Gym,15,8
Water,30,25


Step-by-Step Guide for Implementing the Project (Main Code Sections)

To write this project, divide your code structure into the following sections.

Step One: Designing the Habit Class

  • Create a class named Habit.
  • The constructor method (__init__) of this class should receive and initialize the habit name, target, and the amount done (which defaults to zero).
  • Write a method (behavior) inside the class called add_progress that, whenever called, adds one unit to the amount done (done).

Step Two: File Management and Setup

  • Write functions that, at the beginning of the program, read the habits.csv file, create an Object from the Habit class for each row, and store all these objects in a main list called habits_list.
  • Write a function that, when changes are made, converts the list of objects back to CSV format and saves it in the CSV file.

Step Three: Interactive Menu and Habit Management (Core Features)

  • Create a main loop that, until the user selects the exit option, prints the following options:
  1. Display current status of habits (by iterating over the list of objects and printing the attributes of each object)
  2. Add a new habit (creating a new object from the class and updating the file)
  3. Log daily progress (calling the add_progress method on the selected object)
  4. View graphical dashboard
  5. Exit the program
  • In the target input section, be sure to use try-except so that if the user enters text, the program does not throw an error.

Step Four: Chart and Dashboard (Data Visualization)

  • Write a function that uses pd.read_csv to load the data file into Pandas.
  • Using Matplotlib, draw a bar chart (plt.bar) in which the target ceiling of each habit is compared alongside the user's actual progress with two different colors.

Sample Guide Code (To Get Started)

Open your programming environment, write the class structure and initial menu below, and start filling in the internal sections:

import os
import pandas as pd
import matplotlib.pyplot as plt

FILE_NAME = "habits.csv"

# Step One: Designing the template and object-oriented class for each habit
class Habit:
    def __init__(self, habit_name, target, done=0):
        self.habit_name = habit_name
        self.target = int(target)
        self.done = int(done)
        
    def add_progress(self):
        # Method to increase progress by one unit
        self.done += 1

def load_habits():
    # Reading the file and converting each row to an object from the Habit class and returning a list
    habits_list = []
    return habits_list

def save_habits(habits_list):
    # Taking the list of objects and saving their attributes into the CSV file
    pass

def main():
    # Loading the list of habit objects at the beginning of the program
    my_habits = load_habits()
    
    while True:
        print("\n=== OOP SMART HABIT TRACKER ===")
        print("1. Show Status | 2. Add Habit | 3. Log Progress | 4. Dashboard | 5. Exit")
        choice = input("Select an option: ")
        
        if choice == "5":
            break

if __name__ == "__main__":
    main()

saleh askari
saleh askari

Thank you so much for reading this blog post.

FA