Index of Python Learning Scripts

Root Scripts

AND OR to get fail and approved.py (root)
# Get input from the user for two grades.
# `float()` converts the text input into a number with decimal points.
grade1 = float(input("Input first grade (0-10):"))
grade2 = float(input("Input second grade (0-10):"))

# Get input for the number of absences and total classes.
# `int()` converts the text input into a whole number.
absences = int(input("Input number of absences:"))
total_classes = int(input("Input total number of classes:"))

# Calculate the average of the two grades.
avg_grade = (grade1 + grade2) / 2

# Calculate the attendance rate as a percentage.
# 1. Subtract absences from total classes to get attended classes.
# 2. Divide attended classes by total classes.
# 3. Multiply by 100 to get a percentage.
attendance_rate = ((total_classes - absences) / total_classes) * 100

# Print the calculated average grade.
print("Average grade:", round(avg_grade, 2))
# Print the calculated attendance rate, followed by a '%' sign.
print("Attendance rate:", round(attendance_rate, 2), "%")

# Determine if the student is approved or failed based on grade and attendance.
# The student is approved if their average grade is 6 or higher AND their attendance rate is 80% or higher.
if avg_grade >= 6 and attendance_rate >= 80:
    print("Approved")
# If both average grade is less than 6 AND attendance rate is less than 80%, they fail due to both.
elif avg_grade < 6 and attendance_rate < 80:
    print("Failed due to low grade and low attendance.")
# If attendance is sufficient (80% or higher) but the grade is low (less than 6), they fail due to low grade.
elif attendance_rate >= 80: # This implies avg_grade < 6 because the first 'if' condition was not met.
    print("Failed due to low grade.")
# If the grade is sufficient (6 or higher) but attendance is low (less than 80%), they fail due to low attendance.
# This 'else' covers the case where avg_grade >= 6 (because the second 'elif' was not met)
# and attendance_rate < 80 (because the first 'if' and third 'elif' were not met).
else: # This implies avg_grade >=6 and attendance_rate < 80
    print("Failed due to low attendance.")
BMI conditional.py (root)
# Get input from the user for their weight and height.
# The `float()` function is used to convert the input (which is initially text) into a number with decimal points.
weight = float(input("Enter Weight (in kilograms):"))
height = float(input("Enter Height (in meters):"))

# Calculate BMI using the formula: weight / (height squared)
# BMI (Body Mass Index) is a measure of body fat based on height and weight.
bmi = weight / (height**2)

# Print the calculated BMI, rounded to two decimal places for readability.
print("Your Body Mass Index =", round(bmi, 2))

# Check the BMI value and print the corresponding category.
# These categories are based on common BMI classifications.

# If BMI is less than 18.5, the person is considered Underweight.
if bmi < 18.5:
    print("You Are Under Weight")
# If BMI is between 18.5 (inclusive) and 24.9 (inclusive), the person is considered Normal Weight.
elif bmi >= 18.5 and bmi <= 24.9:
    print("You Are Normal Weight")
# If BMI is between 24.9 (exclusive, so greater than 24.9) and 29.9 (exclusive, so less than 29.9),
# the person is considered Overweight.
elif bmi > 24.9 and bmi < 29.9:
    print("You Are Over Weight")
# If BMI is 29.9 or greater, the person is considered to have Obesity.
# This 'else' covers all cases where BMI is not in the ranges above (i.e., >= 29.9).
else:
    print("You Are Obesity")
CHATGPT.py (root)

Error handling.py (root)
number = input("Type a number:")

try:
    number=float(number)
    print("The number is ",number)
except:
    print("Invalid Entry")

 
Exercise no 1.py (root)
fname=input("Enter First Name:")
mname=input("Enter middle Name:")
lname=input("Enter Last Name:")

print ("Your Initials are " ,fname[0],'',mname[0],'',lname[0])
Exercise no 2.py (root)
lot =  "037-00901-00027"

print ('Country code:', lot[0:3])
print ('Product code:', lot[4:9])
print ('Batch code:' ,lot[-5:])
This program calculates the average of two numbers.py (root)
print ("This program calculates the average of two numbers")
print ('The numberss are 4 and 8') ; print ("The average is ", (4+8) / 2)
Using tuple find month.py (root)
birth=input("Enter your birthday as {DD-MM-YYYY}:")
month=int(birth[3:5])
month=month-1
months=("January","Feburary","March","April","May","June","July","Agust","Septumber","Octuber","November","December")
print("You were born in",months[month])
advanced_data_structures.py (root)
# Python Advanced Data Structures: Sets, Tuples (In-depth), Dictionaries (In-depth)

import collections # For collections.namedtuple

# -----------------------------------
# 1. SETS
# -----------------------------------
# Sets are unordered collections of unique elements.
# This means an element cannot appear more than once in a set, and the order of elements is not guaranteed.
# Sets are mutable (you can add or remove elements).
# They are very useful for membership testing, removing duplicates from a sequence,
# and performing mathematical set operations like union, intersection, etc.

print("--- 1. Sets ---")

# --- a. Creating Sets ---
print("\n[Sets] Creating Sets:")
# Creating an empty set (must use set(), {} creates an empty dictionary)
empty_set = set()
print(f"Empty set: {empty_set}, type: {type(empty_set)}")

# Creating a set from a list (duplicates are automatically removed)
numbers_list = [1, 2, 2, 3, 4, 4, 4, 5]
numbers_set = set(numbers_list)
print(f"Set from list {numbers_list}: {numbers_set}") # Output: {1, 2, 3, 4, 5}

# Creating a set directly with elements
fruits_set = {"apple", "banana", "cherry", "apple"} # "apple" appears once
print(f"Set of fruits: {fruits_set}")

# --- b. Adding and Removing Elements ---
print("\n[Sets] Adding and Removing Elements:")
my_set = {1, 3}
print(f"Initial set: {my_set}")

# Add a single element
my_set.add(2) # Adds 2 to the set
print(f"After adding 2: {my_set}")
my_set.add(3) # Adding an existing element does nothing
print(f"After adding 3 again: {my_set}")

# Remove an element
# `remove()` will raise a KeyError if the element is not found.
my_set.remove(3)
print(f"After removing 3: {my_set}")
# my_set.remove(4) # This would cause a KeyError

# `discard()` also removes an element, but does not raise an error if it's not found.
my_set.discard(1)
print(f"After discarding 1: {my_set}")
my_set.discard(4) # No error, element 4 is not in the set
print(f"After discarding 4 (which wasn't there): {my_set}")

# Remove and return an arbitrary element using `pop()`.
# Raises KeyError if the set is empty.
my_set = {'a', 'b', 'c'}
print(f"Set before pop: {my_set}")
popped_element = my_set.pop()
print(f"Popped element: {popped_element}")
print(f"Set after pop: {my_set}")

# Clear all elements from a set
my_set.clear()
print(f"Set after clear: {my_set}")


# --- c. Common Set Operations ---
print("\n[Sets] Common Set Operations:")
set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}
print(f"Set A: {set_a}")
print(f"Set B: {set_b}")

# Union: Elements present in either set_a or set_b (or both). Represented by | operator or .union() method.
union_set = set_a.union(set_b)
# union_set_operator = set_a | set_b # Alternative using operator
print(f"Union (A | B): {union_set}") # Output: {1, 2, 3, 4, 5, 6}

# Intersection: Elements present in both set_a and set_b. Represented by & operator or .intersection() method.
intersection_set = set_a.intersection(set_b)
# intersection_set_operator = set_a & set_b # Alternative
print(f"Intersection (A & B): {intersection_set}") # Output: {3, 4}

# Difference: Elements present in set_a but not in set_b. Represented by - operator or .difference() method.
difference_set_ab = set_a.difference(set_b) # A - B
# difference_set_ab_operator = set_a - set_b # Alternative
print(f"Difference (A - B): {difference_set_ab}") # Output: {1, 2}

difference_set_ba = set_b.difference(set_a) # B - A
print(f"Difference (B - A): {difference_set_ba}") # Output: {5, 6}

# Symmetric Difference: Elements present in either set_a or set_b, but not in both.
# Represented by ^ operator or .symmetric_difference() method.
symmetric_diff_set = set_a.symmetric_difference(set_b)
# symmetric_diff_set_operator = set_a ^ set_b # Alternative
print(f"Symmetric Difference (A ^ B): {symmetric_diff_set}") # Output: {1, 2, 5, 6}

# --- d. Set Use Cases ---
print("\n[Sets] Use Cases:")
# Membership testing (very efficient for sets)
my_data = {"apple", "banana", "cherry"}
print(f"Is 'apple' in my_data? {'apple' in my_data}")   # True
print(f"Is 'grape' in my_data? {'grape' in my_data}") # False

# Removing duplicates from a list
my_list_with_duplicates = [10, 20, 30, 20, 10, 40, 50, 10]
unique_items_list = list(set(my_list_with_duplicates))
print(f"Original list: {my_list_with_duplicates}")
print(f"List after removing duplicates using set: {unique_items_list}")


# -----------------------------------
# 2. TUPLES (IN-DEPTH)
# -----------------------------------
# Tuples are ordered, immutable sequences. Immutable means once a tuple is created,
# its elements cannot be changed, added, or removed.
# They are often used to store collections of heterogeneous data (i.e., items of different types)
# or when you want to ensure that the data cannot be changed.

print("\n\n--- 2. Tuples (In-depth) ---")

# --- a. Revisiting Immutability ---
print("\n[Tuples] Immutability:")
my_tuple = (1, "hello", 3.14)
print(f"My tuple: {my_tuple}")
# my_tuple[0] = 2 # This would raise a TypeError: 'tuple' object does not support item assignment
# my_tuple.append("world") # This would raise an AttributeError: 'tuple' object has no attribute 'append'

# However, if a tuple contains mutable objects (like a list), the mutable object itself can be changed.
mutable_tuple = ([1, 2], "a_string")
print(f"Mutable tuple: {mutable_tuple}")
mutable_tuple[0].append(3) # The list inside the tuple is changed
print(f"Mutable tuple after modification: {mutable_tuple}") # Output: ([1, 2, 3], 'a_string')

# --- b. Packing and Unpacking ---
print("\n[Tuples] Packing and Unpacking:")
# Packing: When you assign a sequence of values to a single variable, Python packs them into a tuple.
packed_tuple = 10, 20, "packed" # No parentheses needed, but often used for clarity: (10, 20, "packed")
print(f"Packed tuple: {packed_tuple}, type: {type(packed_tuple)}")

# Unpacking: Assigning elements of a tuple to multiple variables.
# The number of variables must match the number of elements in the tuple.
a, b, c = packed_tuple
print(f"Unpacked: a={a}, b={b}, c={c}")

# Unpacking can be useful for swapping variables
x, y = 5, 10
print(f"Before swap: x={x}, y={y}")
x, y = y, x # y (10) is assigned to x, x (5) is assigned to y
print(f"After swap: x={x}, y={y}")

# Extended unpacking (Python 3) using *
numbers_tuple = (1, 2, 3, 4, 5)
first, second, *rest = numbers_tuple
print(f"Extended unpacking: first={first}, second={second}, rest={rest} (type: {type(rest)})")
first, *middle, last = numbers_tuple
print(f"Extended unpacking: first={first}, middle={middle}, last={last}")

# --- c. Named Tuples (collections.namedtuple) ---
# Named tuples provide a way to create tuple subclasses with named fields.
# This makes your code more readable as you can access elements by name instead of index.
print("\n[Tuples] Named Tuples:")

# Define a named tuple 'Point' with fields 'x' and 'y'
Point = collections.namedtuple('Point', ['x', 'y'])
# Point = collections.namedtuple('Point', 'x y') # Alternative string syntax for fields

# Create instances of the Point named tuple
p1 = Point(10, 20)
p2 = Point(x=30, y=40) # Can also use keyword arguments

print(f"Point p1: {p1}")
print(f"Point p2: {p2}")

# Access elements by name
print(f"p1.x = {p1.x}, p1.y = {p1.y}")

# Access elements by index (still possible, like regular tuples)
print(f"p2[0] = {p2[0]}, p2[1] = {p2[1]}")

# Named tuples are still immutable
# p1.x = 15 # This would raise an AttributeError

# You can convert a named tuple to a dictionary
print(f"p1 as dictionary: {p1._asdict()}")


# -----------------------------------
# 3. DICTIONARIES (IN-DEPTH)
# -----------------------------------
# Dictionaries are unordered (in Python versions before 3.7, ordered in 3.7+) collections of
# key-value pairs. Each key must be unique and immutable (e.g., string, number, or tuple).
# Values can be of any type and can be duplicated.
# Dictionaries are mutable.

print("\n\n--- 3. Dictionaries (In-depth) ---")
my_dict = {"name": "Alice", "age": 30, "city": "New York"}
print(f"Initial dictionary: {my_dict}")

# --- a. Iterating Through Dictionaries ---
print("\n[Dicts] Iterating:")
# Iterating through keys (this is the default iteration behavior)
print("Keys:")
for key in my_dict:
    print(f"  {key} (value: {my_dict[key]})")

# Iterating through values using .values()
print("\nValues:")
for value in my_dict.values():
    print(f"  {value}")

# Iterating through key-value pairs (items) using .items()
print("\nItems (key-value pairs):")
for key, value in my_dict.items():
    print(f"  Key: {key}, Value: {value}")

# --- b. Dictionary Comprehensions ---
# A concise way to create dictionaries.
print("\n[Dicts] Dictionary Comprehensions:")

# Example 1: Create a dictionary of squares
squares_dict = {x: x*x for x in range(1, 6)}
# Equivalent for loop:
# squares_dict_loop = {}
# for x in range(1, 6):
#   squares_dict_loop[x] = x*x
print(f"Squares dictionary: {squares_dict}") # Output: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Example 2: From two lists
keys_list = ["a", "b", "c"]
values_list = [10, 20, 30]
dict_from_lists = {keys_list[i]: values_list[i] for i in range(len(keys_list))}
# A more Pythonic way for the above using zip:
# dict_from_lists_zip = {k: v for k, v in zip(keys_list, values_list)}
print(f"Dictionary from lists {keys_list} and {values_list}: {dict_from_lists}")

# Example 3: With a condition
original_prices = {"apple": 1.0, "banana": 0.5, "orange": 0.75, "grape": 2.0}
expensive_items = {item: price for item, price in original_prices.items() if price > 0.8}
print(f"Expensive items from {original_prices}: {expensive_items}")

# --- c. Safe Access with .get() and .setdefault() ---
print("\n[Dicts] Safe Access:")
student_grades = {"Alice": 85, "Bob": 92}
print(f"Grades: {student_grades}")

# Accessing a key that exists
print(f"Alice's grade: {student_grades['Alice']}")

# Accessing a key that does NOT exist using [] will raise a KeyError
# print(student_grades['Charlie']) # This would cause a KeyError

# Using .get(key, default_value) for safe access
# If the key exists, its value is returned.
# If the key does not exist, the default_value is returned (None if not specified).
charlie_grade = student_grades.get("Charlie")
print(f"Charlie's grade (using get()): {charlie_grade}") # Output: None

charlie_grade_default = student_grades.get("Charlie", "Not found") # Providing a default
print(f"Charlie's grade (using get() with default): {charlie_grade_default}") # Output: Not found

bob_grade = student_grades.get("Bob", "Not found")
print(f"Bob's grade (using get() with default): {bob_grade}") # Output: 92 (key exists)

# Using .setdefault(key, default_value)
# If the key exists, its value is returned.
# If the key does not exist, it is inserted into the dictionary with the default_value,
# and then default_value is returned.
print(f"\nUsing setdefault():")
print(f"Grades before setdefault for David: {student_grades}")
david_grade = student_grades.setdefault("David", 70) # David is not in dict, so he's added with grade 70
print(f"David's grade (using setdefault()): {david_grade}")
print(f"Grades after setdefault for David: {student_grades}")

alice_grade_setdefault = student_grades.setdefault("Alice", 90) # Alice exists, her grade is returned, dict not changed
print(f"Alice's grade (using setdefault()): {alice_grade_setdefault}")
print(f"Grades after setdefault for Alice: {student_grades}")


print("\n\n--- Advanced Data Structures Demonstration Complete ---")

# To run this file:
# 1. Save it as advanced_data_structures.py
# 2. Open a terminal or command prompt.
# 3. Navigate to the directory where you saved the file.
# 4. Run the script using the command: python advanced_data_structures.py
# The script will print its actions to the console, demonstrating each concept.
advanced_error_handling.py (root)
# Python Advanced Error Handling Techniques

# Error handling is crucial for writing robust and user-friendly programs.
# Python provides a flexible `try...except` mechanism to manage exceptions.

print("--- Advanced Error Handling ---")

# -------------------------------------------
# 1. Specific Exceptions
# -------------------------------------------
# It's good practice to catch specific exceptions rather than a generic `except Exception:` or `except:`.
# This allows you to handle different types of errors in different ways and prevents
# your program from accidentally catching exceptions you didn't intend to (like SystemExit or KeyboardInterrupt).

print("\n--- 1. Catching Specific Exceptions ---")

def divide_numbers_specific():
    try:
        numerator = int(input("[Specific] Enter numerator: "))
        denominator = int(input("[Specific] Enter denominator: "))
        result = numerator / denominator
        print(f"[Specific] Result of division: {result}")
    except ValueError:
        # Handles errors if input cannot be converted to an integer (e.g., user types "abc")
        print("[Specific] Error: Invalid input. Please enter numbers only.")
    except ZeroDivisionError:
        # Handles errors if the denominator is zero
        print("[Specific] Error: Cannot divide by zero.")
    # except Exception as e: # A more general fallback, but try to be specific first
    #     print(f"[Specific] An unexpected error occurred: {e}")

# divide_numbers_specific() # Uncomment to test this section

# -------------------------------------------
# 2. Multiple Exception Blocks
# -------------------------------------------
# You can have multiple `except` blocks to handle different types of exceptions
# that might occur within a single `try` block. Python will execute the first
# `except` block that matches the type of exception raised.

print("\n--- 2. Multiple Exception Blocks (same as above, just emphasizing the structure) ---")
# The `divide_numbers_specific()` function above already demonstrates multiple except blocks.
# Each `except` targets a different potential error: `ValueError` or `ZeroDivisionError`.

def open_and_process_file_multiple():
    file_path = input("[Multiple] Enter file path to open (e.g., 'data.txt' or 'non_existent.txt'): ")
    try:
        with open(file_path, 'r') as file:
            content = file.read()
            # Let's imagine we want to convert the first line to an integer
            first_line_value = int(content.splitlines()[0])
            print(f"[Multiple] First line as integer: {first_line_value}")
            print(f"[Multiple] File content:\n{content}")
    except FileNotFoundError:
        print(f"[Multiple] Error: The file '{file_path}' was not found.")
    except ValueError:
        print("[Multiple] Error: The first line of the file could not be converted to an integer.")
    except IndexError:
        print("[Multiple] Error: The file is empty or does not have enough lines.")
    except Exception as e: # Catch-all for other unexpected errors
        print(f"[Multiple] An unexpected error occurred: {e}")

# open_and_process_file_multiple() # Uncomment to test

# -------------------------------------------
# 3. The `else` Clause
# -------------------------------------------
# The `else` block is optional and, if present, is executed only if the `try` block
# completes without raising any exceptions.
# It's useful for code that should run only when the `try` part was successful.

print("\n--- 3. The `else` Clause ---")

def divide_numbers_with_else():
    try:
        numerator = int(input("[Else] Enter numerator: "))
        denominator = int(input("[Else] Enter denominator: "))
        result = numerator / denominator
    except ValueError:
        print("[Else] Error: Invalid input. Please enter numbers only.")
    except ZeroDivisionError:
        print("[Else] Error: Cannot divide by zero.")
    else:
        # This block runs only if no exceptions occurred in the try block.
        print(f"[Else] Division successful! Result: {result}")
        print("[Else] The 'else' block is a good place for code that depends on the try block succeeding.")

# divide_numbers_with_else() # Uncomment to test

# -------------------------------------------
# 4. The `finally` Clause
# -------------------------------------------
# The `finally` block is optional and, if present, is always executed,
# regardless of whether an exception occurred in the `try` block or not.
# It's typically used for cleanup actions, like closing files or releasing resources,
# ensuring these actions happen no matter what.

print("\n--- 4. The `finally` Clause ---")

def example_with_finally():
    file = None # Initialize file variable
    try:
        print("\n[Finally] Attempting to open 'temp_file.txt' for writing.")
        file = open("temp_file.txt", "w")
        user_input = input("[Finally] Enter data to write (or type 'error' to cause one): ")
        if user_input.lower() == 'error':
            # Intentionally cause an error after the file is opened
            result = 10 / 0
        file.write(user_input)
        print("[Finally] Data written to temp_file.txt successfully.")
    except ZeroDivisionError:
        print("[Finally] Error: Oops, a ZeroDivisionError occurred!")
    except Exception as e:
        print(f"[Finally] An unexpected error occurred: {e}")
    finally:
        # This block will always execute.
        print("[Finally] The 'finally' block is executing now.")
        if file: # Check if file was successfully opened
            file.close()
            print("[Finally] File 'temp_file.txt' has been closed.")
        else:
            print("[Finally] File was not opened, so no need to close.")
        # You might also remove the temp file here if it's just for this example
        # import os
        # if os.path.exists("temp_file.txt"):
        #     os.remove("temp_file.txt")
        #     print("[Finally] temp_file.txt removed.")

# example_with_finally() # Uncomment to test

# -------------------------------------------
# 5. Raising Exceptions (`raise`)
# -------------------------------------------
# You can manually trigger (raise) exceptions using the `raise` keyword.
# This is useful when you detect an error condition in your code and want to
# signal that something has gone wrong, often based on custom logic.

print("\n--- 5. Raising Exceptions ---")

def validate_age(age):
    try:
        age_num = int(age)
        if age_num < 0:
            # Raise a ValueError if the age is negative, which is logically incorrect.
            raise ValueError("Age cannot be negative.")
        elif age_num < 18:
            # Raise a custom message or a specific type of error for under-age.
            raise Exception("User is underage for this service.") # Can be more specific
        else:
            print(f"[Raise] Age {age_num} is valid.")
            return True
    except ValueError as ve: # Catch the ValueError we might raise, or from int()
        print(f"[Raise] Value Error: {ve}")
        return False
    except Exception as e: # Catch other exceptions we might raise
        print(f"[Raise] General Error: {e}")
        return False

# print("\n[Raise] Testing age validation:")
# validate_age("25")
# validate_age("-5")
# validate_age("abc")
# validate_age("15")


# -------------------------------------------
# 6. Custom Exceptions
# -------------------------------------------
# You can define your own custom exception classes by inheriting from the base `Exception` class
# (or a more specific built-in exception class).
# Custom exceptions make your error handling more specific and readable.

print("\n--- 6. Custom Exceptions ---")

# Define a custom exception class
class NegativeNumberError(Exception):
    """Custom exception raised when a non-negative number is expected but a negative one is given."""
    def __init__(self, number, message="Negative numbers are not allowed here."):
        self.number = number
        self.message = message
        # Call the base class constructor with the message
        super().__init__(f"{message} (Received: {self.number})")

class ValueTooSmallError(Exception):
    """Custom exception for values that are too small."""
    pass # Can be as simple as this if no extra attributes/methods are needed


def process_positive_number(num):
    try:
        if not isinstance(num, (int, float)):
            raise TypeError("Input must be a number.")
        if num < 0:
            # Raise our custom exception
            raise NegativeNumberError(num)
        if num < 10:
            raise ValueTooSmallError(f"The number {num} is too small, expected 10 or more.")
        print(f"[CustomExc] Processing number: {num}. Square is {num*num}.")
    except NegativeNumberError as nne:
        # Handle our custom exception specifically
        print(f"[CustomExc] NegativeNumberError Caught: {nne}")
        # print(f"[CustomExc] The problematic number was: {nne.number}") # Access custom attribute
    except ValueTooSmallError as vtse:
        print(f"[CustomExc] ValueTooSmallError Caught: {vtse}")
    except TypeError as te:
        print(f"[CustomExc] TypeError Caught: {te}")


# print("\n[CustomExc] Testing custom exceptions:")
# process_positive_number(25)
# process_positive_number(-5)
# process_positive_number(5) # Will raise ValueTooSmallError
# process_positive_number("text") # Will raise TypeError


print("\n--- Advanced Error Handling Demonstration Complete ---")
print("Uncomment the function calls in the script to test each section interactively.")

# How to test:
# 1. Save this file as advanced_error_handling.py
# 2. Run `python advanced_error_handling.py` in your terminal.
# 3. Uncomment one function call at a time (e.g., `divide_numbers_specific()`)
#    and re-run the script to see that specific error handling technique in action.
#    Follow the prompts to provide input that might cause errors.
age using boolean.py (root)
myage=20

userage=int(input("Enter the user age:"))

if(myage>userage):
    print("Programmer is older then you")
elif(myage<userage):
    print("You are older then Programmer")
else:
    print("You and Programmer are of same age")
area of a circle.py (root)
import math
r = float(input("Enter radius of the circle:"))
cir=2*math.pi*r

area=math.pi*(r**2)
area=round(area,2)
print('Area of a circle is :',area)
cir=round(cir,2)
print('Circumference of a circle :',cir)
boolean fucntion.py (root)
num1=int(input("Enter first number:"))
num2=int(input("Enter second number:"))
if (num1>num2):
    print(num1,"is greater then",num2)
elif(num1<num2):
    print(num2,"is greater then",num1)
elif(num1==num2):
    print(num1,"is equal to",num2)
else:
    print("Wrong value")
color game using loop.py (root)
colors=["red","yellow","purple","grey","pink","blue","black","white"]

import random
while True:
    random_num=random.randint(0,len(colors)-1)

    random_col=colors[random_num]

    #print(random_col)
    while True:
        guess=input("Enter Color:").lower()

        if (random_col==guess):
            break
        else:
            print("Oops try again")

    print("You Guessed it", random_col)

    play_again=input("Play again 'Yes/No' :").lower()

    if play_again=='no':
        break
    elif play_again=='yes':
        continue
print("It was Fun, thanks")
data type str.py (root)
myString="Some text"
type(myString)
mystring='Hi i am "MIKRAN"'
print (mystring)
len(mystring)
mystring[0]
mystring[len(mystring)-8]
data validation.py (root)
data_valid=False

while data_valid==False:
    grade1=int(input("Enter grade:"))
    if grade1<0 or grade1>10:
        print("Grade must be Between 0 and 10")
        continue
    else:
        data_valid=True

data_valid=False

while data_valid == False:
    grade2=int(input("Enter grade:"))
    if grade2<0 or grade2>10:
        print("Grade must be between 0 and 10")
        continue
    else:
        data_valid=True
data_valid=False

while data_valid == False:
    attendence=int(input("Enter Attendence:"))
    if attendence<0 or attendence>50:
        print("Attendence must be between 0 and 50")
        continue
    else:
        data_valid=True

data_valid=False

while data_valid == False:
    Absences=int(input("Enter Absences:"))
    if Absences<0 or Absences>attendence:
        print("Absences must be between 0 and ",attendence)
        continue
    else:
        data_valid=True


avg=(grade1+grade2)/2

total=(attendence-Absences)/attendence


print("Your Average Grade:",round(avg,2))
print("Your Attendence:",total*100,"%")

if avg>=6.0 and total>=0.8:
    print("Congrats! You have Passed")
elif avg<6.0 and total>=0.8:
    print("Sorry! You failed due to low Grades")
elif avg>=6.0 and total<0.8:
    print("Sorry! You failed due to low Attendence")
else:
    print("Sorry! You failed due to low Attendence and Grades")
    
    
dictionares of information.py (root)
person={"name":"Mikran Sandhu","gender":"male","age":20,"address":"Gujranwala","phone":+923217112944}
key=input("What information you want of the person (name,age,gender,address,phone):").lower()

result=person.get(key,"That information is not avaliable")
print( result )
exercise json and request exercise.py (root)
#json and request exercise

import requests
import json
import pprint

endGame=""
url="https://opentdb.com/api.php?amount=1&category=12&difficulty=easy&type=multiple"

while endGame != "quit":
    x = requests.get(url)
    if(x.status_code != 200):
        endGame=input("Sorry! 404 _____ Press enter to try again or type 'quit' to Quit Game")
    else:
        data=json.loads(x.text)
        pprint.pprint(data)
        input("Press enter to get a new question")
    
     

               
exercise of loop.py (root)
people=[]
for x in range(0,8):
    person=input("Enter a name:")
    people.append(person)

import random

random_num=random.randint(0,7)

random_person=people[random_num]

print("lucky winner is:",random_person)
exersice using error handling.py (root)
#exercise error handling

data_valid=False

while data_valid==False:
    grade1= input("Enter grade:")
    try:
        grade1=float(grade1)
    except:
        print("Invalid input! Only numbers are accepted. Decimals should be seperated by a dot.")
        continue
    if grade1<0 or grade1>10:
        print("Grade must be Between 0 and 10")
        continue
    else:
        data_valid=True

data_valid=False

while data_valid == False:
    grade2=input("Enter grade:")
    try:
        grade2=float(grade2)
    except:
        print("Invalid input! Only numbers are accepted. Decimals should be seperated by a dot.")
        continue
    if grade2<0 or grade2>10:
        print("Grade must be between 0 and 10")
        continue
    else:
        data_valid=True
data_valid=False

while data_valid == False:
    attendence=input("Enter Attendence:")
    try:
        attendence=float(attendence)
    except:
        print("Invalid input! Only numbers are accepted. Decimals should be seperated by a dot.")
        continue
    if attendence<0 or attendence>50:
        print("Attendence must be between 0 and 50")
        continue
    else:
        data_valid=True

data_valid=False

while data_valid == False:
    Absences=int(input("Enter Absences:"))
    try:
        Absences=float(Absences)
    except:
        print("Invalid input! Only numbers are accepted. Decimals should be seperated by a dot.")
        continue
    if Absences<0 or Absences>attendence:
        print("Absences must be between 0 and ",attendence)
        continue
    else:
        data_valid=True


avg=(grade1+grade2)/2

total=(attendence-Absences)/attendence


print("Your Average Grade:",round(avg,2))
print("Your Attendence:",total*100,"%")

if avg>=6.0 and total>=0.8:
    print("Congrats! You have Passed")
elif avg<6.0 and total>=0.8:
    print("Sorry! You failed due to low Grades")
elif avg>=6.0 and total<0.8:
    print("Sorry! You failed due to low Attendence")
else:
    print("Sorry! You failed due to low Attendence and Grades")
    
    
file_operations.py (root)
# Python File Operations: Text, CSV, and JSON

import os  # To check for file existence and remove files
import csv # For working with CSV files
import json # For working with JSON files

# -----------------------------------
# 1. WORKING WITH TEXT FILES (.txt)
# -----------------------------------
print("--- 1. Text File Operations ---")

# --- a. Opening and Writing to a Text File ---
# The `open()` function is used to open a file.
# 'w' mode: Opens the file for writing.
#           If the file exists, its content is overwritten.
#           If the file does not exist, it's created.
# Using `with open(...) as ...` is recommended because it automatically closes the file
# even if errors occur.
file_path_txt = "sample.txt"

print(f"\n[Text] Writing to {file_path_txt}...")
try:
    with open(file_path_txt, 'w') as file:
        # The `write()` method writes a string to the file.
        file.write("Hello, Python File I/O!\n")
        file.write("This is the second line.\n")
        # `writelines()` can write a list of strings. Each string should ideally end with '\n'.
        lines_to_write = ["Third line here.\n", "And a fourth one.\n"]
        file.writelines(lines_to_write)
    print(f"[Text] Successfully wrote to {file_path_txt}")
except IOError as e:
    print(f"[Text] Error writing to file: {e}")


# --- b. Reading from a Text File ---
# 'r' mode: Opens the file for reading (this is the default mode).
#           Raises an error if the file does not exist.
print(f"\n[Text] Reading from {file_path_txt}...")
try:
    with open(file_path_txt, 'r') as file:
        # `read()`: Reads the entire content of the file into a single string.
        print("\n[Text] Using file.read():")
        content = file.read()
        print(content)

    # Re-open to demonstrate other read methods (cursor is at the end after read())
    with open(file_path_txt, 'r') as file:
        # `readline()`: Reads a single line from the file, including the newline character.
        print("[Text] Using file.readline():")
        line1 = file.readline()
        print(f"Line 1: {line1.strip()}") # .strip() removes leading/trailing whitespace like '\n'
        line2 = file.readline()
        print(f"Line 2: {line2.strip()}")

    with open(file_path_txt, 'r') as file:
        # `readlines()`: Reads all lines from the file and returns them as a list of strings.
        # Each string in the list includes the newline character.
        print("\n[Text] Using file.readlines():")
        lines = file.readlines()
        for i, line in enumerate(lines):
            print(f"Line {i+1}: {line.strip()}")
except FileNotFoundError:
    print(f"[Text] Error: The file {file_path_txt} was not found.")
except IOError as e:
    print(f"[Text] Error reading file: {e}")

# --- c. Appending to a Text File ---
# 'a' mode: Opens the file for appending.
#           New data is written to the end of the file.
#           If the file does not exist, it's created.
print(f"\n[Text] Appending to {file_path_txt}...")
try:
    with open(file_path_txt, 'a') as file:
        file.write("This line was appended.\n")
        file.write("Another appended line.\n")
    print(f"[Text] Successfully appended to {file_path_txt}")

    # Verify by reading again
    with open(file_path_txt, 'r') as file:
        print("\n[Text] Content after appending:")
        print(file.read())
except IOError as e:
    print(f"[Text] Error appending to file: {e}")

# --- d. Reading and Writing ('r+' mode) ---
# 'r+' mode: Opens the file for both reading and writing.
#            The file pointer is at the beginning. Overwrites existing content.
#            Raises an error if the file does not exist.
print(f"\n[Text] Using 'r+' mode with {file_path_txt}...")
try:
    with open(file_path_txt, 'r+') as file:
        print(f"[Text] Initial content (r+): {file.readline().strip()}") # Read the first line
        file.write("OVERWRITTEN FIRST LINE (r+)\n") # Overwrite from cursor position
        # Note: Be careful with r+ as it can be tricky to manage cursor position.
        # For complex operations, reading all, modifying, then writing all ('w') is often safer.
    print(f"[Text] Successfully used 'r+' on {file_path_txt}")

    with open(file_path_txt, 'r') as file:
        print("\n[Text] Content after 'r+' modification:")
        print(file.read())
except FileNotFoundError:
    print(f"[Text] Error: The file {file_path_txt} was not found for 'r+' operation.")
except IOError as e:
    print(f"[Text] Error with 'r+' operation: {e}")


# -----------------------------------
# 2. WORKING WITH CSV FILES (.csv)
# -----------------------------------
# CSV (Comma Separated Values) files are simple text files used to store tabular data.
print("\n\n--- 2. CSV File Operations ---")
file_path_csv = "sample.csv"

# --- a. Writing to a CSV File ---
# Data to write (list of lists, where each inner list is a row)
csv_data_to_write = [
    ["Name", "Age", "City"],
    ["Alice", 30, "New York"],
    ["Bob", 24, "Los Angeles"],
    ["Charlie", 28, "Chicago"]
]

print(f"\n[CSV] Writing to {file_path_csv}...")
try:
    # `newline=''` is important to prevent blank rows in the CSV on some platforms.
    with open(file_path_csv, 'w', newline='') as csvfile:
        # `csv.writer` creates a writer object.
        csv_writer = csv.writer(csvfile)
        # `writerow()` writes a single row.
        # `writerows()` writes multiple rows from a list of lists.
        csv_writer.writerows(csv_data_to_write)
    print(f"[CSV] Successfully wrote to {file_path_csv}")
except IOError as e:
    print(f"[CSV] Error writing CSV file: {e}")

# --- b. Reading from a CSV File ---
print(f"\n[CSV] Reading from {file_path_csv}...")
try:
    with open(file_path_csv, 'r', newline='') as csvfile:
        # `csv.reader` creates a reader object.
        csv_reader = csv.reader(csvfile)
        # The reader object can be iterated over to get rows.
        # Each row is returned as a list of strings.
        print("[CSV] Contents:")
        for row in csv_reader:
            print(row) # Each 'row' is a list of strings
except FileNotFoundError:
    print(f"[CSV] Error: The file {file_path_csv} was not found.")
except IOError as e:
    print(f"[CSV] Error reading CSV file: {e}")

# --- c. Writing CSV data using csv.DictWriter (writing dictionaries) ---
csv_dict_data_to_write = [
    {'Name': 'David', 'Age': 35, 'City': 'Boston'},
    {'Name': 'Eve', 'Age': 22, 'City': 'Miami'}
]
# Define the fieldnames (column headers)
csv_fieldnames = ['Name', 'Age', 'City']
file_path_dict_csv = "sample_dict.csv"

print(f"\n[CSV] Writing dictionary data to {file_path_dict_csv}...")
try:
    with open(file_path_dict_csv, 'w', newline='') as csvfile:
        # `csv.DictWriter` needs the file object and a list of fieldnames.
        dict_writer = csv.DictWriter(csvfile, fieldnames=csv_fieldnames)
        # `writeheader()` writes the header row (fieldnames).
        dict_writer.writeheader()
        # `writerows()` writes all dictionaries in the list.
        dict_writer.writerows(csv_dict_data_to_write)
    print(f"[CSV] Successfully wrote dictionary data to {file_path_dict_csv}")
except IOError as e:
    print(f"[CSV] Error writing DictWriter CSV: {e}")

# --- d. Reading CSV data using csv.DictReader (reading into dictionaries) ---
print(f"\n[CSV] Reading data as dictionaries from {file_path_dict_csv}...")
try:
    with open(file_path_dict_csv, 'r', newline='') as csvfile:
        # `csv.DictReader` treats each row as a dictionary,
        # where keys are taken from the first (header) row.
        dict_reader = csv.DictReader(csvfile)
        print("[CSV] Contents (as dictionaries):")
        for row_dict in dict_reader:
            # Each 'row_dict' is an OrderedDict or dict (depending on Python version)
            print(dict(row_dict)) # Convert to regular dict for cleaner printing
            # print(f"Name: {row_dict['Name']}, Age: {row_dict['Age']}, City: {row_dict['City']}")
except FileNotFoundError:
    print(f"[CSV] Error: The file {file_path_dict_csv} was not found.")
except IOError as e:
    print(f"[CSV] Error reading DictReader CSV: {e}")


# -----------------------------------
# 3. WORKING WITH JSON FILES (.json)
# -----------------------------------
# JSON (JavaScript Object Notation) is a lightweight data-interchange format.
# It's easy for humans to read and write and easy for machines to parse and generate.
print("\n\n--- 3. JSON File Operations ---")
file_path_json = "sample.json"

# --- a. Writing to a JSON File (Serialization) ---
# Python dictionary to be stored as JSON
json_data_to_write = {
    "name": "John Doe",
    "age": 30,
    "isStudent": False,
    "courses": [
        {"title": "History", "credits": 3},
        {"title": "Math", "credits": 4}
    ],
    "address": {
        "street": "123 Main St",
        "city": "Anytown"
    }
}

print(f"\n[JSON] Writing to {file_path_json}...")
try:
    with open(file_path_json, 'w') as jsonfile:
        # `json.dump()` serializes a Python dictionary into a JSON formatted string
        # and writes it to a file object.
        # `indent=4` makes the JSON file human-readable with pretty printing.
        json.dump(json_data_to_write, jsonfile, indent=4)
    print(f"[JSON] Successfully wrote to {file_path_json}")
except IOError as e:
    print(f"[JSON] Error writing JSON file: {e}")

# --- b. Reading from a JSON File (Deserialization) ---
print(f"\n[JSON] Reading from {file_path_json}...")
try:
    with open(file_path_json, 'r') as jsonfile:
        # `json.load()` deserializes a JSON formatted string from a file object
        # into a Python dictionary.
        loaded_data = json.load(jsonfile)
        print("[JSON] Contents (as Python dictionary):")
        print(loaded_data)
        print(f"[JSON] Name from loaded data: {loaded_data['name']}")
        print(f"[JSON] First course title: {loaded_data['courses'][0]['title']}")
except FileNotFoundError:
    print(f"[JSON] Error: The file {file_path_json} was not found.")
except json.JSONDecodeError as e:
    print(f"[JSON] Error decoding JSON: {e}")
except IOError as e:
    print(f"[JSON] Error reading JSON file: {e}")

# --- c. `dumps` and `loads` (string operations) ---
# `json.dumps()`: Serializes a Python object to a JSON formatted string (not to a file).
# `json.loads()`: Deserializes a JSON formatted string to a Python object.

print("\n[JSON] Using json.dumps() and json.loads()...")
python_dict = {"key": "value", "number": 42}
json_string = json.dumps(python_dict, indent=2) # Serialize to string
print(f"[JSON] Python dict serialized to JSON string:\n{json_string}")

reloaded_python_dict = json.loads(json_string) # Deserialize from string
print(f"[JSON] JSON string deserialized back to Python dict:\n{reloaded_python_dict}")
print(f"[JSON] Value from reloaded dict: {reloaded_python_dict['key']}")


# -----------------------------------
# 4. CLEANUP (Optional)
# -----------------------------------
# This section removes the files created by the script.
# You might want to comment this out if you want to inspect the files after running.
print("\n\n--- 4. Cleaning Up Sample Files ---")
files_to_remove = [file_path_txt, file_path_csv, file_path_dict_csv, file_path_json]
for f_path in files_to_remove:
    try:
        if os.path.exists(f_path):
            os.remove(f_path)
            print(f"[Cleanup] Successfully removed {f_path}")
        else:
            print(f"[Cleanup] File not found, no need to remove: {f_path}")
    except OSError as e:
        print(f"[Cleanup] Error removing file {f_path}: {e}")

print("\n--- File Operations Demonstration Complete ---")

# To run this file:
# 1. Save it as file_operations.py
# 2. Open a terminal or command prompt.
# 3. Navigate to the directory where you saved the file.
# 4. Run the script using the command: python file_operations.py
# The script will print its actions to the console and create/delete sample files.
first python.py (root)
print ('My first python program')
print (2+3)
for loop.py (root)
blog_post=["python","","cpp","","java"]

for  post in blog_post:
    if post=="":
        continue
    else:
        print(post)

print("________________________")

mystring="this is a string"

for char in mystring:
    print(char)
    
print("________________________")

for x in range(0,10):
    print(x)

print("________________________")

person={"Name":"Mikran","Age":10,"Gender":"male"}

for key in person:
    print(key, ":" ,person[key])
    
print("________________________")
blog_post={"one":["python","","cpp","","java"],"two":["python","","cpp","","java"]}

for cat in blog_post:
    print("Post no:",cat)
    for post in blog_post[cat]:
        if post =="":
            continue
        else:
            print(post)
    
functions.py (root)
# Functions are reusable blocks of code that perform a specific task.

# This is a simple function that greets a person.
# 'person' is a parameter - it's a placeholder for the value that will be passed to the function when it's called.
def say_hello_simple(person):
    # This function now returns a greeting string.
    # It uses an f-string (formatted string literal) to include the 'person' variable directly in the string.
    return f"Hello {person}, How are you"

# Calling the say_hello_simple function and printing its returned value.
# An argument is the actual value passed to a function's parameter.
returned_greeting_simple = say_hello_simple("mms")
print(f"Output of say_hello_simple('mms'): {returned_greeting_simple}")

# This function converts a temperature from Fahrenheit to Celsius.
# 'fahr' is the parameter representing the temperature in Fahrenheit.
def fahrtocelsius(fahr):
    # This is the formula to convert Fahrenheit to Celsius.
    # 1. Subtract 32 from the Fahrenheit temperature.
    # 2. Multiply the result by 5.
    # 3. Divide that result by 9.
    celsius = (5 * (fahr - 32)) / 9
    # This line returns the calculated Celsius temperature.
    # The 'return' statement sends a value back to where the function was called.
    return celsius

# Calling the fahrtocelsius function with 100 as the argument.
# The result is rounded to 2 decimal places for better readability.
celsius_temp = fahrtocelsius(100)
print(f"Celsius output for fahrtocelsius(100): {round(celsius_temp, 2)}")

# This is a more advanced greeting function with a default parameter value.
# 'person1' is a required parameter.
# 'person2' is an optional parameter with a default value of "mafia".
# If no value is provided for 'person2' when the function is called, "mafia" will be used.
def say_hello_adv(person1, person2="mafia"):
    # This function now returns a greeting string including both person1 and person2.
    return f"Hello {person1}, How are you? And hello to {person2}!"

# Calling the say_hello_adv function with two arguments and printing its returned value.
returned_greeting_adv1 = say_hello_adv("mms", "ucp")
print(f"Output of say_hello_adv('mms', 'ucp'): {returned_greeting_adv1}")

# Calling the say_hello_adv function with only one argument and printing its returned value.
returned_greeting_adv2 = say_hello_adv("friend")
print(f"Output of say_hello_adv('friend'): {returned_greeting_adv2}")
guess game using while.py (root)
import random
number=random.randint(0,10)
guess=int(input("'I am thinking about a number  between 1-10' Guess:"))

while True:
    if guess==number:
        print(number,"it is")
        break
    else:
        print("Try again")
        guess=int(input("'I am thinking about a number  between 1-10' Guess:"))
if elif else.py (root)
grade1=float(input("Input grade:"))
grade2=float(input("Input grade:"))

absences=int(input("Input absents:"))
total_classes = int(input("Total classes:"))

avg_grade=(grade1+grade2)/2
attendence=((total_classes-absences)/total_classes)*100
print("Average grade:",avg_grade)
print("Attendence rate:",attendence ,"%"
      )
if(avg_grade>=6):
    if(attendence>=80):
        print("Approved")
    else:
        print("Fail because of low attendence")
elif(attendence>=80):
    print("Fail because of low grade")
else:
    print("Fail because of low attendence and low grade")
input to give average.py (root)
a=input ("Enter a number=")
b=input ("Enter a number=")
summ=int(a)+int(b)
avg=summ/2
print ('Average =' , avg)
json pprint.py (root)
import requests
import json
import pprint
r=requests.get("https://opentdb.com/api.php?amount=1&category=12&difficulty=easy&type=multiple")
r.status_code

#json to dictionary
question=json.loads(r.text)

pprint.pprint(question)

question["results"][0]["category"]

#dictionary to json
person={"name":"ali","age":"20"}
person_json=json.dumps(person)
person_json
km to miles.py (root)
km = input("Input Kilometer=")

mile= float(km) / 1.609344

print ('Miles=',mile)
print ('Miles=',round(mile,3) ,'miles')
lambda_functions.py (root)
# Python Lambda Functions (Anonymous Functions)

# A lambda function is a small, anonymous, inline function defined using the `lambda` keyword.
# They are also known as "anonymous functions" because they don't have a formal name
# defined with `def` (unless you assign them to a variable, which is common).

# Syntax:
# lambda arguments: expression
# - `lambda`: The keyword that indicates you are defining a lambda function.
# - `arguments`: One or more arguments, separated by commas (just like regular function arguments).
# - `expression`: A single expression that is evaluated and returned.
#   Lambda functions cannot contain multiple expressions or complex statements. They are limited
#   to a single expression.

print("--- Lambda Functions ---")

# -------------------------------------------
# 1. Basic Lambda Function Syntax
# -------------------------------------------
print("\n--- 1. Basic Lambda Function Syntax ---")

# Example: A lambda function that adds two numbers.
add = lambda x, y: x + y
# - `lambda`: Keyword
# - `x, y`: Arguments
# - `x + y`: Expression (this value will be returned)

# Calling the lambda function (it's assigned to the variable 'add')
result = add(5, 3)
print(f"Result of add(5, 3): {result}") # Output: 8

# You can use lambda functions immediately without assigning them to a variable,
# though this is less common for direct calls and more for passing as arguments.
immediate_result = (lambda a, b: a * b)(4, 5)
print(f"Result of (lambda a, b: a * b)(4, 5): {immediate_result}") # Output: 20

# Comparison with a regular function defined using `def`:
def add_def(x, y):
    return x + y

result_def = add_def(5, 3)
print(f"Result of add_def(5, 3) (using def): {result_def}")
# Lambda functions are more concise for simple, single-expression functions.

# -------------------------------------------
# 2. Use Cases for Lambda Functions
# -------------------------------------------
# Lambda functions are most useful when you need a small, throwaway function for a short period,
# often as an argument to higher-order functions (functions that take other functions as arguments).

print("\n--- 2. Use Cases for Lambda Functions ---")

# --- a. With `sorted()` ---
# The `sorted()` function can take a `key` argument, which is a function that
# returns a value to be used for sorting. Lambdas are perfect for simple keys.
print("\n[Lambda] Using with sorted():")
points = [(1, 5), (3, 2), (5, 8), (2, 0)]
print(f"Original points: {points}")

# Sort points based on the second element (y-coordinate) of each tuple
# Using a lambda function as the key:
# For each item `p` in `points`, `lambda p: p[1]` returns `p[1]` (the y-coordinate).
sorted_by_y = sorted(points, key=lambda p: p[1])
print(f"Points sorted by y-coordinate (using lambda): {sorted_by_y}")

# For comparison, using a regular function:
def get_y_coordinate(point):
    return point[1]
sorted_by_y_def = sorted(points, key=get_y_coordinate)
print(f"Points sorted by y-coordinate (using def):    {sorted_by_y_def}")
# The lambda version is more compact for this simple key.

# Sort a list of dictionaries by a specific key's value
students = [
    {'name': 'Alice', 'grade': 85},
    {'name': 'Bob', 'grade': 92},
    {'name': 'Charlie', 'grade': 78}
]
print(f"\nOriginal students: {students}")
sorted_students_by_grade = sorted(students, key=lambda student: student['grade'])
print(f"Students sorted by grade (using lambda): {sorted_students_by_grade}")


# --- b. With `map()` ---
# The `map()` function applies a given function to each item of an iterable (e.g., list)
# and returns a map object (which can be converted to a list).
print("\n[Lambda] Using with map():")
numbers = [1, 2, 3, 4, 5]
print(f"Original numbers: {numbers}")

# Goal: Create a new list where each number is squared.
# Using lambda with map:
# `lambda x: x * x` is applied to each element `x` in `numbers`.
squared_numbers_map = map(lambda x: x * x, numbers)
squared_numbers_list = list(squared_numbers_map) # Convert map object to list
print(f"Squared numbers (using lambda with map): {squared_numbers_list}")

# For comparison, using a list comprehension (often preferred over map for this):
squared_numbers_comp = [x * x for x in numbers]
print(f"Squared numbers (using list comprehension): {squared_numbers_comp}")

# For comparison, using a regular function with map:
def square(x):
    return x * x
squared_numbers_map_def = map(square, numbers)
print(f"Squared numbers (using def with map):    {list(squared_numbers_map_def)}")
# Lambda is concise for simple transformations with map.

# --- c. With `filter()` ---
# The `filter()` function constructs an iterator from elements of an iterable
# for which a function returns true.
print("\n[Lambda] Using with filter():")
numbers_for_filter = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(f"Original numbers for filter: {numbers_for_filter}")

# Goal: Create a list of only the even numbers.
# Using lambda with filter:
# `lambda x: x % 2 == 0` returns True if x is even, False otherwise.
# filter() will include only those elements for which the lambda returns True.
even_numbers_filter = filter(lambda x: x % 2 == 0, numbers_for_filter)
even_numbers_list = list(even_numbers_filter) # Convert filter object to list
print(f"Even numbers (using lambda with filter): {even_numbers_list}")

# For comparison, using a list comprehension (often preferred over filter for this):
even_numbers_comp = [x for x in numbers_for_filter if x % 2 == 0]
print(f"Even numbers (using list comprehension): {even_numbers_comp}")

# For comparison, using a regular function with filter:
def is_even(x):
    return x % 2 == 0
even_numbers_filter_def = filter(is_even, numbers_for_filter)
print(f"Even numbers (using def with filter):    {list(even_numbers_filter_def)}")
# Lambda is concise for simple filtering conditions.


# -------------------------------------------
# 3. When to Use Lambda Functions
# -------------------------------------------
# - **Short, simple operations:** When the function logic is very simple and can be expressed in a single line.
# - **As arguments to higher-order functions:** This is the most common and idiomatic use (e.g., `map`, `filter`, `sorted`, UI event handlers).
# - **Readability for simple cases:** For simple tasks, a lambda can be more readable than defining a full function, as the logic is right where it's used.

# When NOT to use Lambda Functions:
# - **Complex logic:** If the function requires multiple lines of code, complex logic, or many statements, use a regular `def` function.
# - **When you need docstrings:** Lambda functions cannot have docstrings.
# - **When you need type hints for arguments/return value in a reusable way:** While you can assign a lambda to a variable and type hint that variable, `def` functions offer more standard and readable type hinting.
# - **If it makes the code harder to read:** If a lambda becomes too convoluted, a named `def` function is often clearer.

print("\n--- Lambda Functions Demonstration Complete ---")

# To run this file:
# 1. Save it as lambda_functions.py
# 2. Open a terminal or command prompt.
# 3. Navigate to the directory where you saved the file.
# 4. Run the script using the command: python lambda_functions.py
# The script will print the results of various lambda function applications.
list add.py (root)
people=['Mikran','Imran','Mawaan']
print(people)
new=input("Enter a name you want to add in the list:")
people.append(new)
print(people)
rem=input("Enter a name you want to remove in the list:")        
people.remove(rem)
print(people)
exn=input("Enter a name you want to exchange in the list:")
pos=int(input("Enter a position you want to exchange in the list:"))

people.insert(pos,exn)
print(people)

people.pop()
print(people)
list_comprehensions.py (root)
# Python List Comprehensions

# List comprehensions provide a concise and readable way to create lists.
# They are often more compact and faster than using traditional `for` loops
# and `append()` calls to build a list.

# The basic syntax is:
# new_list = [expression for item in iterable if condition]
# - expression: The expression to compute for each item (e.g., item * 2).
# - item: The variable representing each element from the iterable.
# - iterable: A sequence or collection to iterate over (e.g., a list, range, tuple).
# - condition (optional): A filter that includes the item in the new list only if the condition is True.

print("--- List Comprehensions ---")

# -------------------------------------------
# 1. Basic List Comprehension (from a range)
# -------------------------------------------
print("\n--- 1. Basic List Comprehension (from a range) ---")

# Goal: Create a list of squares from 0 to 9.

# Using a traditional for loop:
squares_loop = []
for x in range(10): # range(10) produces numbers 0, 1, ..., 9
    squares_loop.append(x * x)
print(f"Squares (using for loop):   {squares_loop}")

# Using a list comprehension:
# expression: x * x
# item: x
# iterable: range(10)
squares_comp = [x * x for x in range(10)]
print(f"Squares (using comprehension): {squares_comp}")
# Notice how the list comprehension is more compact.

# -------------------------------------------------
# 2. List Comprehension with Transformation
# -------------------------------------------------
print("\n--- 2. List Comprehension with Transformation ---")

# Goal: Create a list of uppercase words from a list of lowercase words.
words = ["hello", "world", "python", "is", "fun"]
print(f"Original words: {words}")

# Using a traditional for loop:
uppercase_words_loop = []
for word in words:
    uppercase_words_loop.append(word.upper()) # .upper() converts a string to uppercase
print(f"Uppercase (using for loop):   {uppercase_words_loop}")

# Using a list comprehension:
# expression: word.upper()
# item: word
# iterable: words
uppercase_words_comp = [word.upper() for word in words]
print(f"Uppercase (using comprehension): {uppercase_words_comp}")

# -------------------------------------------------
# 3. List Comprehension with a Conditional Clause (if)
# -------------------------------------------------
print("\n--- 3. List Comprehension with a Conditional Clause (if) ---")

# Goal: Create a list of even numbers from 0 to 19.
numbers = range(20) # Numbers from 0 to 19
print(f"Original numbers: {list(numbers)}") # Convert range to list for printing

# Using a traditional for loop:
even_numbers_loop = []
for num in numbers:
    if num % 2 == 0: # The condition: num is even if num modulo 2 is 0
        even_numbers_loop.append(num)
print(f"Even numbers (using for loop):   {even_numbers_loop}")

# Using a list comprehension with an if condition:
# expression: num
# item: num
# iterable: numbers
# condition: num % 2 == 0
even_numbers_comp = [num for num in numbers if num % 2 == 0]
print(f"Even numbers (using comprehension): {even_numbers_comp}")

# Goal: Get words longer than 3 characters from the 'words' list.
print(f"\nOriginal words: {words}")
long_words_comp = [word for word in words if len(word) > 3]
print(f"Long words (using comprehension): {long_words_comp}")


# -------------------------------------------------
# 4. List Comprehension with 'if-else' (Conditional Expression)
# -------------------------------------------------
print("\n--- 4. List Comprehension with 'if-else' ---")
# Note: The syntax for if-else within a list comprehension is different from the filtering 'if'.
# It's `expression_if_true if condition else expression_if_false` and comes *before* the `for` loop.

# Goal: Create a list where numbers are labeled 'even' or 'odd'.
numbers_to_label = range(1, 6) # 1, 2, 3, 4, 5
print(f"Numbers to label: {list(numbers_to_label)}")

# Using a traditional for loop:
labels_loop = []
for num in numbers_to_label:
    if num % 2 == 0:
        labels_loop.append(f"{num} is even")
    else:
        labels_loop.append(f"{num} is odd")
print(f"Labels (using for loop):   {labels_loop}")

# Using a list comprehension with an if-else expression:
# expression: f"{num} is even" if num % 2 == 0 else f"{num} is odd"
# item: num
# iterable: numbers_to_label
labels_comp = [f"{num} is even" if num % 2 == 0 else f"{num} is odd" for num in numbers_to_label]
print(f"Labels (using comprehension): {labels_comp}")


# -------------------------------------------------
# 5. Nested List Comprehensions (Use with caution, can be hard to read)
# -------------------------------------------------
print("\n--- 5. Nested List Comprehensions ---")

# Goal: Create a flattened list from a list of lists (a matrix).
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
print(f"Original matrix: {matrix}")

# Using traditional nested for loops:
flattened_loop = []
for row in matrix:
    for item in row:
        flattened_loop.append(item)
print(f"Flattened (using for loops):   {flattened_loop}")

# Using a nested list comprehension:
# The 'for' clauses are in the same order as in the traditional loop.
# expression: item
# outer loop: for row in matrix
# inner loop: for item in row
flattened_comp = [item for row in matrix for item in row]
print(f"Flattened (using comprehension): {flattened_comp}")

# Another example: Create pairs from two lists
list1 = ['A', 'B']
list2 = [1, 2]
print(f"\nList 1: {list1}, List 2: {list2}")

pairs_comp = [(l1, l2) for l1 in list1 for l2 in list2] # (l1, l2) creates a tuple for each pair
print(f"Pairs (using comprehension): {pairs_comp}")
# Output: [('A', 1), ('A', 2), ('B', 1), ('B', 2)]

# While powerful, deeply nested comprehensions can become difficult to understand.
# If it's too complex, a traditional for loop might be more readable.

# -------------------------------------------------
# Benefits of List Comprehensions:
# -------------------------------------------------
# 1. Conciseness and Readability: Often more compact and easier to read than equivalent loops,
#    once you are familiar with the syntax.
# 2. Performance: Can be faster than using `append()` in a loop, as they are optimized in CPython.
#    (Though for very complex expressions, this might not always hold true).
# 3. Expressiveness: They are a very Pythonic way to create lists.

print("\n--- List Comprehensions Demonstration Complete ---")

# To run this file:
# 1. Save it as list_comprehensions.py
# 2. Open a terminal or command prompt.
# 3. Navigate to the directory where you saved the file.
# 4. Run the script using the command: python list_comprehensions.py
# The script will print the lists created using both traditional loops and comprehensions.
matplotlib.py (root)
import matplotlib.pyplot as plt
 
x=[1,2,3,4]
y=[5,6,7,8]
 
 
plt.plot(x,y)
 
plt.show()
 
legend=['january','february','march','april']
plt.xticks(x,legend)
plt.show()
plt.bar(x,y)
plt.show()
plt.title("Montly Sales")
 
plt.ylabel("this is a chart")
 
plt.show()
plt.bar(x,y)
 
plt.show()
oop_concepts.py (root)
# Object-Oriented Programming (OOP) Concepts in Python

# OOP is a programming paradigm based on the concept of "objects",
# which can contain data in the form of fields (often known as attributes or properties)
# and code in the form of procedures (often known as methods).

# -----------------------------------
# 1. CLASSES
# -----------------------------------
# A class is a blueprint for creating objects. It defines a set of attributes and methods
# that the created objects will have.

print("--- 1. Classes and Objects ---")

class Dog:
    # This is a class attribute. It's shared by all instances (objects) of the class.
    species = "Canis familiaris"

    # This is the constructor method, also known as the initializer.
    # It's automatically called when you create a new object (instance) of the class.
    # 'self' refers to the instance being created.
    # 'name' and 'age' are instance attributes, specific to each object.
    def __init__(self, name, age):
        self.name = name  # Attribute specific to each Dog instance
        self.age = age    # Attribute specific to each Dog instance
        self._secret_trick = "Roll over" # Example of a "non-public" attribute convention
        self.__very_secret_mood = "Always happy" # Example of name mangling for "pseudo-private"

    # This is an instance method. It operates on an instance of the class ('self').
    def description(self):
        # f-strings (formatted string literals) are a convenient way to embed expressions inside string literals.
        return f"{self.name} is {self.age} years old."

    # Another instance method
    def speak(self, sound):
        return f"{self.name} says {sound}!"

    # Method to demonstrate accessing "private" attributes (for illustration)
    def reveal_secret_trick(self):
        return f"{self.name}'s secret trick is to {self._secret_trick}."

    def reveal_mood(self):
        # Accessing name-mangled attribute (Python renames it to _ClassName__attributeName)
        return f"{self.name}'s mood: {self._Dog__very_secret_mood}"


# -----------------------------------
# 2. OBJECTS (Instances)
# -----------------------------------
# An object is an instance of a class. When a class is defined, no memory is allocated
# until an object of that class is created.

# Creating (instantiating) objects of the Dog class
dog1 = Dog("Buddy", 3)  # Calls the __init__ method with name="Buddy", age=3
dog2 = Dog("Lucy", 5)   # Calls the __init__ method with name="Lucy", age=5

# Accessing instance attributes
print(f"{dog1.name} is an instance of Dog.")
print(f"{dog2.name} is also an instance of Dog.")

# Accessing class attributes
# Can be accessed via the class itself or an instance
print(f"All dogs belong to the species: {Dog.species}")
print(f"{dog1.name} is a {dog1.species}.")

# Calling instance methods
print(dog1.description())  # Output: Buddy is 3 years old.
print(dog2.speak("Woof"))  # Output: Lucy says Woof!

# -----------------------------------
# 3. INHERITANCE
# -----------------------------------
# Inheritance allows a new class (child/derived class) to inherit attributes and methods
# from an existing class (parent/base class). This promotes code reuse.

print("\n--- 3. Inheritance ---")

# Parent class (already defined as Dog)

# Child class inheriting from Dog
class GoldenRetriever(Dog):
    # The child class can have its own __init__ method.
    def __init__(self, name, age, favorite_toy):
        # 'super()' calls the __init__ method of the parent class (Dog).
        # This ensures that the parent's initialization logic (like setting name and age) is executed.
        super().__init__(name, age)
        self.favorite_toy = favorite_toy  # Attribute specific to GoldenRetriever

    # Child class can have its own methods
    def fetch(self, item):
        return f"{self.name} fetches the {item}. Their favorite toy is {self.favorite_toy}."

    # Child class can also override methods from the parent class
    def speak(self, sound="Bark"): # Overriding the speak method
        return f"{self.name} (a Golden Retriever) enthusiastically says {sound}!"

# Creating an instance of the child class
golden = GoldenRetriever("Charlie", 2, "tennis ball")

# Accessing attributes and methods from both parent and child class
print(golden.description())  # Inherited from Dog: Charlie is 2 years old.
print(golden.speak())        # Overridden in GoldenRetriever: Charlie (a Golden Retriever) enthusiastically says Bark!
print(golden.speak("Grrr"))  # Overridden in GoldenRetriever: Charlie (a Golden Retriever) enthusiastically says Grrr!
print(golden.fetch("stick")) # Defined in GoldenRetriever: Charlie fetches the stick. Their favorite toy is tennis ball.
print(f"{golden.name} is a {golden.species}") # Accessing class attribute inherited from Dog


# -----------------------------------
# 4. ENCAPSULATION
# -----------------------------------
# Encapsulation is the bundling of data (attributes) and methods that operate on the data
# into a single unit (a class). It also restricts direct access to some of an object's components.
# Python doesn't have strict private attributes like Java or C++, but uses conventions:
# - _non_public: Treated as a hint that it's for internal use.
# - __pseudo_private: Name mangling is applied (e.g., __mood becomes _Dog__mood).
#   This makes it harder to access accidentally from outside but not truly private.

print("\n--- 4. Encapsulation ---")

# dog1 is an instance of Dog class created earlier
print(f"Dog's name: {dog1.name}") # Public attribute, directly accessible

# Accessing "non-public" attribute (by convention, should be avoided directly from outside)
# print(f"Dog's secret trick (direct access): {dog1._secret_trick}") # Possible, but not recommended
print(dog1.reveal_secret_trick()) # Accessing via a public method is preferred

# Accessing "pseudo-private" attribute (name mangled)
# print(dog1.__very_secret_mood) # This would cause an AttributeError

# Accessing it via its mangled name (demonstrates it's not truly private)
# print(f"Dog's mood (mangled name access): {dog1._Dog__very_secret_mood}") # Possible, but not recommended
print(dog1.reveal_mood()) # Accessing via a public method is preferred

# The idea is to protect attributes from accidental modification and to manage access through methods.

# -----------------------------------
# 5. POLYMORPHISM
# -----------------------------------
# Polymorphism means "many forms". In OOP, it refers to the ability of different classes
# to be treated as objects of a common superclass.
# A common use is "duck typing": "If it walks like a duck and quacks like a duck, then it must be a duck."
# This means Python cares more about whether an object has a certain method or property,
# rather than its specific type.

# More commonly, polymorphism allows different classes to have methods with the same name,
# and these methods can behave differently for each class.

print("\n--- 5. Polymorphism ---")

class Cat(Dog): # Let's make Cat inherit from Dog for this example, though not biologically accurate
    def __init__(self, name, age, color):
        super().__init__(name, age) # Call Dog's init
        self.color = color

    # Cat has its own 'speak' method
    def speak(self, sound="Meow"): # Overriding the speak method
        return f"{self.name} (a cat) purrs: {sound}"

class Parrot: # Parrot does not inherit from Dog
    def __init__(self, name, can_talk=True):
        self.name = name
        self.can_talk = can_talk

    # Parrot also has a 'speak' method
    def speak(self, phrase="Squawk"):
        if self.can_talk:
            return f"{self.name} (a parrot) repeats: '{phrase}'"
        else:
            return f"{self.name} (a parrot) just squawks."

# Create instances of Dog, Cat, and Parrot
dog_poly = Dog("Rex", 4)
cat_poly = Cat("Whiskers", 3, "grey")
parrot_poly = Parrot("Polly")
silent_parrot = Parrot("Silent Bob", can_talk=False)

# Create a list of different animal objects
animals = [dog_poly, cat_poly, parrot_poly, silent_parrot]

# Iterate through the list and call the 'speak' method on each object.
# Even though each object is of a different class (or configured differently),
# they all respond to the 'speak()' call in their own way.
print("\nDemonstrating Polymorphism (common method name):")
for animal in animals:
    # Python doesn't strictly check if 'animal' is of a specific type that 'must' have .speak().
    # It just tries to call .speak(). If the method exists, it works. This is duck typing.
    if hasattr(animal, 'speak'): # Good practice to check if method exists
        print(animal.speak())
    else:
        print(f"{animal.name} can't speak.")

# Another example: a function that uses the .speak() method
def animal_communication(animal_object):
    # This function doesn't care what type of animal_object it is,
    # as long as it has a 'speak' method.
    print(f"Communicating with {animal_object.name}: {animal_object.speak('Hello!')}")

print("\nDemonstrating Polymorphism (duck typing with a function):")
animal_communication(dog_poly)
animal_communication(cat_poly)
animal_communication(parrot_poly)

print("\n--- OOP Concepts Demonstration Complete ---")

# To run this file, save it as oop_concepts.py and run 'python oop_concepts.py' in your terminal.
# You will see the output of the print statements demonstrating each concept.
plot.py (root)
import matplotlib.pyplot as plt
import numpy as np

plt.style.use('_mpl-gallery')

# make data
x = np.linspace(0, 10, 100)
y = 4 + 1 * np.sin(2 * x)
x2 = np.linspace(0, 10, 25)
y2 = 4 + 1 * np.sin(2 * x2)

# plot
fig, ax = plt.subplots()

ax.plot(x2, y2 + 2.5, 'x', markeredgewidth=2)
ax.plot(x, y, linewidth=2.0)
ax.plot(x2, y2 - 2.5, 'o-', linewidth=2)

ax.set(xlim=(0, 8), xticks=np.arange(1, 8),
       ylim=(0, 8), yticks=np.arange(1, 8))

plt.show()
requests.py (root)
#import requests
#r=requests.get("https://www.google.com")

#print(r.status_code)

#print(r.headers)

#r.text
#r.headers["Date"]
searching using loop.py (root)
# Initialize an empty list for names
people = []

# Collect 8 names from the user
for x in range(0, 8):
    person = input("Enter a name: ")
    people.append(person)

# Input the name to search for
getnam = input("Enter the name you want to search: ")

# Search for the name in the list
if getnam in people:
    print("Name found")
else:
    print("Name not found")
time and pyplot exercise.py (root)
import time as t
import sys
print(sys.path)
import matplotlib.pyplot as plt

# Setting the word for the typing test
word = "pakistan"

# Validating the difficulty level
while True:
    n = int(input("Enter difficulty level (1-10): "))
    if n < 1 or n > 10:
        print("Wrong level!")
        continue
    else:
        break

num = n  # To preserve the original difficulty level
print(f"Write the word '{word}' {n} times.")

times = []  # To store time taken for each attempt
mistake = 0  # To count mistakes

# Typing test loop
while n:
    start = t.time()  # Start the timer
    test = input("Enter: ")
    end = t.time()  # End the timer
    time_taken = end - start
    times.append(time_taken)

    # Decrement the remaining attempts
    n -= 1

    # Count mistakes
    if test != word:
        mistake += 1

# Generating attempt numbers for plotting
y = list(range(1, num + 1))  # Matches the number of attempts

# Plotting the results
plt.plot(y, times, marker='o', linestyle='-', color='b')
plt.xlabel("Attempt Number")
plt.ylabel("Time Taken (seconds)")
plt.title("Typing Test Evaluation")
plt.grid()

# Printing results
print(f"You have made {mistake} mistakes.")
print("Your test evaluation is loading...")

t.sleep(2)  # Adding a slight delay for effect
plt.show()
time.py (root)
import time as t

time_now=t.localtime()

print("transaction completed at:",str(time_now.tm_hour)+":"+str(time_now.tm_min)+":"+str(time_now.tm_sec))
print(str(time_now.tm_mday)+"-"+str(time_now.tm_mon)+"-"+str(time_now.tm_year))

t.sleep(2)

time_niw=t.time()

delivery_time=time_niw+(86400*7)

#print(t.localtime(delivery_time))

print("Delivery on "+str(time_now.tm_mday)+"-"+str(time_now.tm_mon)+"-"+str(time_now.tm_year))
while.py (root)
x = 0
people=[]

while x<5:
    person =input("Type the name of a person:")
    people.append(person)
    x += 1

print(people)

Custom Module Examples (custom_module_example/)

main.py (custom_module_example)
# This is main.py, a script that will use the custom module 'my_module.py'.
# It demonstrates how to import and use components (functions, classes, variables)
# from another Python file in the same directory (or a directory in Python's search path).

# --- What is a Module? ---
# A module is simply a Python file (.py extension) containing Python definitions and statements.
# Modules help you organize your code into logical units, making it:
# - More manageable: Break down large programs into smaller, well-defined pieces.
# - Reusable: Use the same functions or classes in multiple scripts without copying code.
# - Shareable: Distribute your code for others to use.
# 'my_module.py' in this directory is an example of a custom module we've created.

print("--- main.py execution started ---")

# When Python encounters an `import` statement, it looks for the specified module.
# If it's the first time this module is imported in the program, Python will:
# 1. Execute the module file (my_module.py in this case) from top to bottom.
#    This means any statements not inside a function or class, or the `if __name__ == "__main__":`
#    block in the module, will run. (You should see "my_module.py is being imported..." printed).
# 2. Make the module's objects (functions, classes, variables) available to main.py.


# --- 1. Importing the entire module ---
# `import my_module` imports everything from my_module.py under the namespace 'my_module'.
# To access its components, you need to prefix them with `my_module.`.
print("\n--- Method 1: import my_module ---")
import my_module # This executes my_module.py (if not already executed)

# Using the greet function from my_module
message1 = my_module.greet("Alice")
print(f"From my_module.greet: {message1}")

# Using the add function from my_module
sum1 = my_module.add(100, 200)
print(f"From my_module.add(100, 200): {sum1}")

# Accessing the global variable from my_module
print(f"Accessing my_module.MODULE_VERSION: {my_module.MODULE_VERSION}")

# Creating an instance of MyHelperClass from my_module
helper1 = my_module.MyHelperClass("Main Script User 1")
print(helper1.get_info())


# --- 2. Importing specific components using `from ... import ...` ---
# This imports specific names directly into the current script's namespace.
# You don't need to prefix them with the module name.
print("\n--- Method 2: from my_module import greet ---")
from my_module import greet # Only imports the 'greet' function

# Now 'greet' can be called directly
message2 = greet("Bob") # This 'greet' is my_module.greet
print(f"From imported greet: {message2}")

# Note: If main.py had its own 'greet' function, this import would shadow (replace) it
# if 'greet' was defined *before* this import, or be shadowed by it if defined *after*.

# Trying to access 'add' without prefixing or specific import will fail here:
# sum2 = add(5,5) # This would cause a NameError because 'add' is not directly in main.py's namespace yet


# --- 3. Importing a specific component with an alias using `as` ---
# This imports a specific name but gives it a different name (alias) in the current script.
# Useful to avoid naming conflicts or to use a shorter name.
print("\n--- Method 3: from my_module import add as custom_add ---")
from my_module import add as custom_add

sum3 = custom_add(7, 8)
print(f"Result of custom_add(7, 8): {sum3}")
# print(add(7,8)) # This would still cause a NameError as 'add' itself was not directly imported


# --- 4. Importing multiple specific components (including classes and variables) ---
print("\n--- Method 4: from my_module import MyHelperClass, MODULE_VERSION ---")
from my_module import MyHelperClass, MODULE_VERSION

# Now MyHelperClass and MODULE_VERSION can be used directly
helper2 = MyHelperClass("Main Script User 2")
print(helper2.get_info())
print(f"Directly imported MODULE_VERSION: {MODULE_VERSION}")


# --- Understanding `if __name__ == "__main__":` in `my_module.py` ---
# Each Python file has a special built-in variable called `__name__`.
# - When a Python script is run directly (e.g., `python my_module.py`), its `__name__` variable
#   is set to `"__main__"`.
# - When a Python script is imported as a module into another script (like we are doing here
#   with `my_module.py`), its `__name__` variable is set to the name of the module file
#   (i.e., `"my_module"` for `my_module.py`).

# The `if __name__ == "__main__":` block in `my_module.py` contains code that will
# ONLY run if `my_module.py` is executed directly. It will NOT run when `my_module.py`
# is imported by `main.py` (because in that case, `my_module.__name__` is "my_module", not "__main__").

# You should have seen the print statement "my_module.py is being imported or executed."
# from `my_module.py` when this script started (due to the first import).
# However, you should NOT have seen the print statement from inside the
# `if __name__ == "__main__":` block of `my_module.py`.
# To see that, you would need to run `python custom_module_example/my_module.py` from your terminal.

print("\n--- Demonstrating that my_module's __name__ is 'my_module' when imported ---")
# We can even access the __name__ variable of the imported module (though not typical)
if 'my_module' in dir(): # Check if my_module was imported using "import my_module"
    print(f"The __name__ variable of the imported my_module is: {my_module.__name__}")


# --- Best Practices for Importing ---
# - Be specific: `from module import specific_function` is often preferred over `import module`
#   if you only need a few things, as it makes it clearer where names come from.
# - Avoid `from module import *`: This imports all names from the module into the current
#   namespace. It can make it hard to tell where a function or variable came from and
#   can lead to naming conflicts. There are exceptions, but generally, it's discouraged for clarity.
# - Use aliases (`as`) for clarity or to resolve naming conflicts.
# - Place imports at the top of your script (standard convention).

print("\n--- main.py execution finished ---")

# To run this example:
# 1. Make sure both `main.py` and `my_module.py` are in the `custom_module_example` directory.
# 2. Open your terminal.
# 3. Navigate to the `custom_module_example` directory.
# 4. Run this script using the command: `python main.py`
#
# You can also run the module directly to see its `if __name__ == "__main__":` block execute:
# `python my_module.py`
my_module.py (custom_module_example)
# This is a Python module named 'my_module'.
# A module is simply a Python file with a .py extension that contains Python definitions and statements.
# Modules are used to organize code into logical units, making it more manageable, reusable, and shareable.

print("my_module.py is being imported or executed.")

# --- 1. Global Variable ---
# This is a variable defined at the module level.
# It can be imported and accessed by other scripts that import this module.
MODULE_VERSION = "1.0"

# --- 2. Functions ---
# Functions defined in a module can be imported and used in other scripts.

def greet(name):
    """
    A simple function that returns a greeting string.
    Args:
        name (str): The name of the person to greet.
    Returns:
        str: A greeting message.
    """
    return f"Hello, {name}! Welcome from my_module."

def add(a, b):
    """
    A simple function that adds two numbers.
    Args:
        a (int or float): The first number.
        b (int or float): The second number.
    Returns:
        int or float: The sum of a and b.
    """
    return a + b

# --- 3. Class ---
# Classes defined in a module can also be imported and used to create objects.

class MyHelperClass:
    """
    A simple example class within the module.
    """
    def __init__(self, owner_name="DefaultOwner"):
        """
        Constructor for MyHelperClass.
        Args:
            owner_name (str): The name of the owner of this helper instance.
        """
        self.owner_name = owner_name
        print(f"MyHelperClass instance created by {self.owner_name}.")

    def get_info(self):
        """
        A method that returns some information about the instance.
        Returns:
            str: Information string.
        """
        return f"This is a MyHelperClass object, owned by {self.owner_name}. Module version: {MODULE_VERSION}"

# --- 4. The `if __name__ == "__main__":` block ---
# This block of code is executed only when the module is run directly as a script
# (e.g., by typing `python my_module.py` in the terminal).
# It is NOT executed when the module is imported into another script.
# This is useful for including test code or a demonstration of the module's capabilities
# that should only run when the module itself is the main program.

if __name__ == "__main__":
    # This code runs ONLY if you execute `python custom_module_example/my_module.py`
    print("\n--- my_module.py executed directly ---")
    print("This part of the script runs because __name__ is currently '__main__'.")

    # Example usage of the module's components when run directly:
    print("\nDirect execution examples:")
    current_version = MODULE_VERSION
    print(f"Module Version (accessed directly): {current_version}")

    greeting_message = greet("Developer")
    print(greeting_message)

    sum_result = add(10, 5)
    print(f"Result of add(10, 5): {sum_result}")

    helper_instance = MyHelperClass("Direct Script User")
    print(helper_instance.get_info())

    print("\n--- End of my_module.py direct execution ---")

# When this file is imported by another script (like main.py):
# - The initial print statement ("my_module.py is being imported...") will run.
# - Definitions (MODULE_VERSION, greet, add, MyHelperClass) will be loaded.
# - The code inside `if __name__ == "__main__":` will NOT run.
#   In that case, __name__ for this file will be "my_module" (the name of the module).

Test Scripts (tests/)

__init__.py (tests)
# This file makes the 'tests' directory a Python package.
# It can be empty.
test_functions.py (tests)
import unittest
import sys
import os

# Add the parent directory (project root) to the Python path
# This allows us to import modules from the root directory (e.g., 'functions.py')
# when running tests from the 'tests' subdirectory.
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, project_root)

# Now we can import from functions.py
# If functions.py is not found, it might indicate the script is not being run from the correct context,
# or that functions.py is missing from the root of the project.
try:
    from functions import fahrtocelsius, say_hello_simple, say_hello_adv
except ImportError:
    print("Error: functions.py not found. Ensure it's in the project root and sys.path is correct.")
    # As a fallback for environments where path manipulation might be tricky,
    # one could define dummy functions here, but the goal is to test the actual functions.
    # For now, we'll let the ImportError halt if it occurs, as it's a setup issue.
    raise

class TestFunctions(unittest.TestCase):
    """
    Test cases for the functions defined in functions.py.
    """

    # --- Tests for fahrtocelsius ---
    def test_fahrtocelsius_freezing_point(self):
        """Test fahrtocelsius with the freezing point of water (32 F = 0 C)."""
        self.assertEqual(fahrtocelsius(32), 0)

    def test_fahrtocelsius_boiling_point(self):
        """Test fahrtocelsius with the boiling point of water (212 F = 100 C)."""
        self.assertEqual(fahrtocelsius(212), 100)

    def test_fahrtocelsius_negative_value(self):
        """Test fahrtocelsius with a common negative value (-40 F = -40 C)."""
        self.assertEqual(fahrtocelsius(-40), -40)

    def test_fahrtocelsius_other_value(self):
        """Test fahrtocelsius with another arbitrary value (e.g., 50 F = 10 C)."""
        self.assertEqual(fahrtocelsius(50), 10)

    # --- Tests for say_hello_simple ---
    # These tests assume say_hello_simple has been refactored to RETURN a string.
    def test_say_hello_simple_with_name(self):
        """Test say_hello_simple with a typical name."""
        expected_output = "Hello Alice, How are you"
        self.assertEqual(say_hello_simple("Alice"), expected_output)

    def test_say_hello_simple_empty_string(self):
        """Test say_hello_simple with an empty string as name."""
        expected_output = "Hello , How are you"
        self.assertEqual(say_hello_simple(""), expected_output)

    # --- Tests for say_hello_adv ---
    # These tests assume say_hello_adv has been refactored to RETURN a string.
    def test_say_hello_adv_one_argument(self):
        """Test say_hello_adv with only the first argument (person2 should use default)."""
        expected_output = "Hello Bob, How are you? And hello to mafia!"
        self.assertEqual(say_hello_adv("Bob"), expected_output)

    def test_say_hello_adv_two_arguments(self):
        """Test say_hello_adv with both arguments provided."""
        expected_output = "Hello Charlie, How are you? And hello to friend!"
        self.assertEqual(say_hello_adv("Charlie", "friend"), expected_output)

    def test_say_hello_adv_two_arguments_one_empty(self):
        """Test say_hello_adv with the second argument being an empty string."""
        expected_output = "Hello Dave, How are you? And hello to !"
        self.assertEqual(say_hello_adv("Dave", ""), expected_output)

if __name__ == '__main__':
    """
    This allows the test script to be run directly from the command line.
    `unittest.main()` will discover and run all tests in this file.
    """
    unittest.main()
test_oop_concepts.py (tests)
import unittest
import sys
import os

# Add the parent directory (project root) to the Python path
# This allows us to import modules from the root directory (e.g., 'oop_concepts.py')
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, project_root)

# Now we can import from oop_concepts.py
# We will test the Dog class and its related functionalities.
try:
    from oop_concepts import Dog, GoldenRetriever # Assuming these classes exist
except ImportError:
    print("Error: oop_concepts.py not found or classes missing. Ensure it's in the project root.")
    # Define dummy classes if the import fails, to allow test structure to be checked,
    # but ideally, the import should work.
    class Dog:
        species = "Canis familiaris"
        def __init__(self, name, age):
            self.name = name
            self.age = age
        def description(self): return ""
        def speak(self, sound): return ""
    class GoldenRetriever(Dog):
        def __init__(self, name, age, favorite_toy):
            super().__init__(name,age)
            self.favorite_toy = favorite_toy
        def fetch(self, item): return ""
    # In a real scenario, we would want the test to fail if the import fails.
    raise

class TestOopConcepts(unittest.TestCase):
    """
    Test cases for classes defined in oop_concepts.py.
    Focusing on the Dog and GoldenRetriever classes.
    """

    # --- Tests for Dog Class ---
    def test_dog_creation(self):
        """Test if a Dog object can be created successfully."""
        dog = Dog("Buddy", 3)
        self.assertIsNotNone(dog, "Dog object should not be None")
        self.assertIsInstance(dog, Dog, "Object should be an instance of Dog")

    def test_dog_attributes(self):
        """Test if the Dog object has the correct attributes after creation."""
        dog_name = "Rex"
        dog_age = 5
        dog = Dog(dog_name, dog_age)
        self.assertEqual(dog.name, dog_name, f"Dog name should be {dog_name}")
        self.assertEqual(dog.age, dog_age, f"Dog age should be {dog_age}")
        self.assertEqual(dog.species, "Canis familiaris", "Dog species class attribute should be 'Canis familiaris'")

    def test_dog_description_method(self):
        """Test the description method of the Dog class."""
        dog = Dog("Lucy", 4)
        expected_description = "Lucy is 4 years old."
        self.assertEqual(dog.description(), expected_description)

    def test_dog_speak_method(self):
        """Test the speak method of the Dog class."""
        dog = Dog("Max", 2)
        sound_to_make = "Woof"
        expected_output = f"Max says {sound_to_make}!"
        self.assertEqual(dog.speak(sound_to_make), expected_output)

    # --- Tests for GoldenRetriever Class (Inheritance) ---
    def test_goldenretriever_creation(self):
        """Test if a GoldenRetriever object can be created."""
        gr = GoldenRetriever("Charlie", 2, "ball")
        self.assertIsNotNone(gr, "GoldenRetriever object should not be None")
        self.assertIsInstance(gr, GoldenRetriever, "Object should be an instance of GoldenRetriever")
        self.assertIsInstance(gr, Dog, "GoldenRetriever object should also be an instance of Dog (inheritance)")

    def test_goldenretriever_attributes(self):
        """Test attributes of GoldenRetriever, including inherited ones."""
        gr_name = "Goldie"
        gr_age = 1
        gr_toy = "Frisbee"
        gr = GoldenRetriever(gr_name, gr_age, gr_toy)
        self.assertEqual(gr.name, gr_name)
        self.assertEqual(gr.age, gr_age)
        self.assertEqual(gr.favorite_toy, gr_toy, f"Favorite toy should be {gr_toy}")
        self.assertEqual(gr.species, "Canis familiaris", "Species should be inherited from Dog")

    def test_goldenretriever_fetch_method(self):
        """Test the fetch method specific to GoldenRetriever."""
        gr = GoldenRetriever("Sunny", 3, "squeaky toy")
        item_to_fetch = "stick"
        expected_output = f"Sunny fetches the {item_to_fetch}. Their favorite toy is squeaky toy."
        self.assertEqual(gr.fetch(item_to_fetch), expected_output)

    def test_goldenretriever_speak_method_overridden(self):
        """Test the overridden speak method in GoldenRetriever."""
        gr = GoldenRetriever("Rocky", 4, "rope")
        # Default sound for GoldenRetriever's overridden speak method
        expected_output_default = "Rocky (a Golden Retriever) enthusiastically says Bark!"
        self.assertEqual(gr.speak(), expected_output_default)
        # Specific sound
        sound_to_make = "Awooo"
        expected_output_specific = f"Rocky (a Golden Retriever) enthusiastically says {sound_to_make}!"
        self.assertEqual(gr.speak(sound_to_make), expected_output_specific)


if __name__ == '__main__':
    """
    Allows running the tests directly from the command line.
    """
    unittest.main()