← Computer Programming I

Built-in Functions

  • Built-in functions are pre-written code provided by Python that perform common tasks
  • They save time and effort - no need to reinvent the wheel for common operations
  • We’ve already used several: print(), input(), int(), float(), str(), range()
  • Python has dozens of built-in functions that make programming easier
  • Understanding built-in functions helps you recognize patterns for creating your own functions

Syntax:

# General syntax
result = function_name(arguments)

# Common built-in functions for numbers
max(a, b, c, ...)      # Returns the largest value
min(a, b, c, ...)      # Returns the smallest value
abs(x)                 # Returns absolute value of x
round(x)               # Rounds x to nearest integer
round(x, n)            # Rounds x to n decimal places
pow(base, exp)         # Returns base raised to power exp
len(sequence)          # Returns length of sequence
sum(sequence)          # Returns sum of sequence
Problem

Problem Statement: Create a grade analysis program that processes three test scores. The program should:

  • Start with three predefined test scores: score1 = 85, score2 = 92, score3 = 78
  • Calculate and display the highest score among the three
  • Calculate and display the lowest score among the three
  • Calculate the average score and round it to 2 decimal places
  • Find the absolute difference between the highest and lowest scores
  • Calculate the total points if each score is raised to the power of 2 (for weighted calculation)
  • Display all results in a clear format

Complete Code
# Three test scores
score1 = 85
score2 = 92
score3 = 78

# Calculate statistics using built-in functions
highest = max(score1, score2, score3)
lowest = min(score1, score2, score3)
average = (score1 + score2 + score3) / 3
rounded_average = round(average, 2)
score_range = abs(highest - lowest)

# Weighted calculation using pow
weighted1 = pow(score1, 2)
weighted2 = pow(score2, 2)
weighted3 = pow(score3, 2)
total_weighted = weighted1 + weighted2 + weighted3

print("Grade Analysis:")
print(f"Scores: {score1}, {score2}, {score3}")
print(f"Highest: {highest}")
print(f"Lowest: {lowest}")
print(f"Average: {rounded_average}")
print(f"Range: {score_range}")
print(f"Weighted total (squared): {total_weighted}")

Key Points:

  • max() and min() can take multiple arguments
  • round() with second argument controls decimal places
  • abs() is useful for finding distances/differences
  • pow() is equivalent to ** operator but as a function
  • Built-in functions can be combined for complex calculations

Defining Your Own Functions

  • Functions are reusable blocks of code that perform specific tasks
  • They help organize code, reduce repetition, and make programs easier to understand
  • Functions can take inputs (parameters) and produce outputs (return values)
  • The DRY principle: “Don’t Repeat Yourself” - if you write similar code twice, consider making it a function
  • Functions create a layer of abstraction - use complex operations without knowing implementation details

Syntax:

def function_name(parameter1, parameter2):
    # function body
    # code that does something
    return result  # optional
  • def keyword starts function definition
  • Function name follows Python naming conventions (lowercase with underscores)
  • Parameters in parentheses (can be empty)
  • Colon ends the header line
  • Function body is indented
  • return statement sends value back to caller (optional)
Problem

Problem Statement: Create a temperature conversion system that can convert between Celsius and Fahrenheit. The system should:

  • Define a function to convert Celsius to Fahrenheit using the formula: F = (C × 9/5) + 32
  • Define a function to convert Fahrenheit to Celsius using the formula: C = (F - 32) × 5/9
  • Test both functions with sample temperatures: 0°C, 100°C, 32°F, and 212°F
  • Display the conversions showing both the original and converted values

Complete Code
def celsius_to_fahrenheit(celsius):
    fahrenheit = (celsius * 9/5) + 32
    return fahrenheit

def fahrenheit_to_celsius(fahrenheit):
    celsius = (fahrenheit - 32) * 5/9
    return celsius

# Test conversions
c1 = 0
f1 = celsius_to_fahrenheit(c1)
print(f"{c1}°C = {f1}°F")

c2 = 100
f2 = celsius_to_fahrenheit(c2)
print(f"{c2}°C = {f2}°F")

f3 = 32
c3 = fahrenheit_to_celsius(f3)
print(f"{f3}°F = {c3}°C")

f4 = 212
c4 = fahrenheit_to_celsius(f4)
print(f"{f4}°F = {c4}°C")

Key Points:

  • Function names should describe what the function does
  • Each function should do one thing well
  • Functions make code reusable - call them multiple times with different values
  • The return value can be stored in a variable or used directly

Expected Output:

0°C = 32.0°F
100°C = 212.0°F
32°F = 0.0°C
212°F = 100.0°C

Passing Arguments and Multiple Parameters

  • Functions can accept multiple pieces of information through parameters
  • Arguments are the actual values passed when calling the function
  • Parameters are the variable names in the function definition
  • Order matters when passing arguments (positional arguments)
  • Functions can have zero, one, or many parameters
  • Each parameter acts as a local variable inside the function

Syntax:

def function_name(param1, param2, param3):
    # use param1, param2, param3
    return result

# Calling with arguments
result = function_name(arg1, arg2, arg3)
Problem

Problem Statement: Create a geometric calculator for rectangles and triangular prisms. The calculator should:

  • Define a function that calculates the area of a rectangle given width and height
  • Define a function that calculates the perimeter of a rectangle given width and height
  • Define a function that calculates the volume of a triangular prism given base, height of triangle, and length of prism
  • Test all functions with these measurements:
    • Rectangle: width=5, height=8
    • Triangular prism: base=6, triangle_height=4, prism_length=10
  • Display all calculated values with appropriate labels

Complete Code
def rectangle_area(width, height):
    area = width * height
    return area

def rectangle_perimeter(width, height):
    perimeter = 2 * (width + height)
    return perimeter

def triangular_prism_volume(base, triangle_height, prism_length):
    # Area of triangle times length
    triangle_area = 0.5 * base * triangle_height
    volume = triangle_area * prism_length
    return volume

# Rectangle calculations
w = 5
h = 8
area = rectangle_area(w, h)
perimeter = rectangle_perimeter(w, h)
print(f"Rectangle {w}x{h}:")
print(f"  Area: {area}")
print(f"  Perimeter: {perimeter}")

# Triangular prism calculation
b = 6
th = 4
pl = 10
volume = triangular_prism_volume(b, th, pl)
print(f"\nTriangular Prism (base={b}, height={th}, length={pl}):")
print(f"  Volume: {volume}")

Key Points:

  • Parameter names should be descriptive
  • Order of arguments must match order of parameters
  • Functions can perform complex calculations and return single results
  • Same parameters (width, height) can be used for different calculations

Expected Output:

Rectangle 5x8:
  Area: 40
  Perimeter: 26

Triangular Prism (base=6, height=4, length=10):
  Volume: 120.0

Return Values and Functions Without Return

  • return statement sends a value back to where the function was called
  • Functions can return any data type: numbers, strings, booleans, etc.
  • A function stops executing when it reaches a return statement
  • Functions without return statements return None by default
  • Some functions perform actions (like printing) instead of computing values
  • You can have multiple return statements (in different branches of logic)

Syntax:

def computing_function():
    result = # some calculation
    return result

def action_function():
    # do something (like print)
    # no return statement
Problem

Problem Statement: Create a grade classification system that processes numerical scores. The system should:

  • Define a function that determines the letter grade (A, B, C, D, F) for a score:
    • A: 90-100
    • B: 80-89
    • C: 70-79
    • D: 60-69
    • F: below 60
  • Define a function that checks if a score is passing (60 or above) and returns True/False
  • Define a function that prints a formatted grade report (doesn’t return anything)
  • Test with individual scores: 95, 82, 73, 65, 58
  • For each score, show the letter grade, pass/fail status, and print the report

Complete Code
def get_letter_grade(score):
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    else:
        return "F"

def is_passing(score):
    return score >= 60  # Returns True or False

def print_grade_report(score):
    # This function performs action but doesn't return value
    letter = get_letter_grade(score)
    status = "PASS" if is_passing(score) else "FAIL"
    print(f"Score: {score} | Grade: {letter} | Status: {status}")
    # No return statement

# Test individual scores
score1 = 95
letter1 = get_letter_grade(score1)
pass1 = is_passing(score1)
print(f"Processing score {score1}:")
print(f"  Letter grade: {letter1}")
print(f"  Passing: {pass1}")
print_grade_report(score1)
print()

score2 = 82
letter2 = get_letter_grade(score2)
pass2 = is_passing(score2)
print(f"Processing score {score2}:")
print(f"  Letter grade: {letter2}")
print(f"  Passing: {pass2}")
print_grade_report(score2)
print()

score3 = 73
letter3 = get_letter_grade(score3)
pass3 = is_passing(score3)
print(f"Processing score {score3}:")
print(f"  Letter grade: {letter3}")
print(f"  Passing: {pass3}")
print_grade_report(score3)
print()

score4 = 65
letter4 = get_letter_grade(score4)
pass4 = is_passing(score4)
print(f"Processing score {score4}:")
print(f"  Letter grade: {letter4}")
print(f"  Passing: {pass4}")
print_grade_report(score4)
print()

score5 = 58
letter5 = get_letter_grade(score5)
pass5 = is_passing(score5)
print(f"Processing score {score5}:")
print(f"  Letter grade: {letter5}")
print(f"  Passing: {pass5}")
print_grade_report(score5)

Key Points:

  • Functions can call other functions
  • Boolean functions often use direct return of comparison
  • Not all functions need to return values - some perform actions
  • Multiple return statements allow different outputs based on conditions
  • Early return can simplify logic (no need for else after return)

Expected Output:

Processing score 95:
  Letter grade: A
  Passing: True
Score: 95 | Grade: A | Status: PASS

Processing score 82:
  Letter grade: B
  Passing: True
Score: 82 | Grade: B | Status: PASS

Processing score 73:
  Letter grade: C
  Passing: True
Score: 73 | Grade: C | Status: PASS

Processing score 65:
  Letter grade: D
  Passing: True
Score: 65 | Grade: D | Status: PASS

Processing score 58:
  Letter grade: F
  Passing: False
Score: 58 | Grade: F | Status: FAIL

Variable Scope (Local and Global)

  • Scope determines where a variable can be accessed in your code
  • Local variables exist only inside the function where they’re created
  • Global variables exist outside all functions and can be accessed anywhere
  • Parameters are local variables
  • Variables created inside a function are destroyed when the function ends
  • Each function call creates a new set of local variables
  • Best practice: minimize use of global variables, pass data through parameters

Syntax:

global_var = 10  # Global scope

def function():
    local_var = 5  # Local scope
    # Can access both global_var and local_var here
    
# Can only access global_var here, not local_var
Problem

Problem Statement: Create a points tracking system for a game that demonstrates scope. The system should:

  • Have a global variable for the game’s high score (initially 0).
  • Define a helper function calculate_round_points that calculates points for a round based on: hits (10 points each) and time_bonus (5 points per second under 30). This function should return the calculated score.
  • Define a function show_statistics that displays the current game statistics for a given round.
  • Define a primary function play_round that orchestrates a full round of the game. It should:
    • Accept the round number, hits, and time taken as parameters.
    • Call calculate_round_points to get the score for the round.
    • Update the global high_score if the new score is higher.
    • Call the show_statistics function internally to display the results immediately after the round is processed.
  • Simulate three rounds by calling play_round with these values:
    • Round 1: 5 hits, completed in 25 seconds
    • Round 2: 8 hits, completed in 20 seconds
    • Round 3: 3 hits, completed in 35 seconds
  • After all rounds are played, display the final high score.

Complete Code
high_score = 0  # Global variable

def calculate_round_points(hits, time_taken):
    # Local variables
    hit_points = hits * 10
    
    if time_taken < 30:
        time_bonus = (30 - time_taken) * 5
    else:
        time_bonus = 0
    
    total_points = hit_points + time_bonus
    return total_points

def show_statistics(round_num, current_points):
    # Reading global variable (no 'global' keyword needed for reading)
    print(f"Round {round_num} Results:")
    print(f"  Points this round: {current_points}")
    print(f"  Current high score: {high_score}")
    print()

def play_round(round_num, hits, time_taken):
    global high_score  # Need this to modify the global variable
    
    # Call helper function to get points
    points = calculate_round_points(hits, time_taken)
    
    # Check and update the global high score
    if points > high_score:
        high_score = points
        print(f"NEW HIGH SCORE: {high_score}")
    
    # Call another helper function to display results
    show_statistics(round_num, points)

# Simulate game rounds with single function calls
play_round(1, 5, 25)
play_round(2, 8, 20)
play_round(3, 3, 35)

# Display the final state of the game
print(f"Final High Score after all rounds: {high_score}")

Key Points:

  • Need global keyword to modify global variables inside functions.
  • Don’t need global keyword just to read global variables (as seen in show_statistics).
  • Function Composition: A primary function (play_round) can orchestrate a series of actions by calling other helper functions (calculate_round_points, show_statistics) to perform specific sub-tasks.
  • This design makes the main part of the script cleaner and easier to read. The complexity is encapsulated within the play_round function.
  • The play_round function is an “action” function; its main purpose is to perform a sequence of operations rather than returning a value.
  • Global variables persist between function calls, allowing the high_score to be maintained across multiple rounds.

Expected Output:

NEW HIGH SCORE: 75
Round 1 Results:
  Points this round: 75
  Current high score: 75

NEW HIGH SCORE: 130
Round 2 Results:
  Points this round: 130
  Current high score: 130

Round 3 Results:
  Points this round: 30
  Current high score: 130

Final High Score: 130

Consolidation Problems

Problem 1: Prime Number Analyzer

Problem

Problem Statement: Create a program that analyzes prime numbers within a range. Your program should:

  • Define a function is_prime(n) that returns True if n is prime, False otherwise
    • A prime number is only divisible by 1 and itself
    • Numbers less than 2 are not prime
    • Check divisibility from 2 up to n-1
  • Define a function count_primes_between(start, end) that counts how many prime numbers exist between start and end (inclusive)
    • Use the is_prime function to check each number
    • Return the total count
  • Define a function nth_prime(n) that finds the nth prime number
    • The 1st prime is 2, 2nd prime is 3, 3rd prime is 5, etc.
    • Keep checking numbers until you find n primes
  • Define a function is_twin_prime(n) that checks if n and n+2 are both prime
    • Twin primes are pairs like (3,5), (11,13), (17,19)
  • Test your functions:
    • Count primes between 1 and 50
    • Find the 10th prime number
    • Check if 11, 15, and 29 form twin prime pairs with n+2
    • Count primes between 100 and 150
  • Display all results clearly

Solution
def is_prime(n):
    if n < 2:
        return False
    
    for i in range(2, n):
        if n % i == 0:
            return False
    return True

def count_primes_between(start, end):
    count = 0
    for num in range(start, end + 1):
        if is_prime(num):
            count += 1
    return count

def nth_prime(n):
    if n < 1:
        return -1  # Invalid input
    
    count = 0
    num = 2
    while count < n:
        if is_prime(num):
            count += 1
            if count == n:
                return num
        num += 1
    return -1

def is_twin_prime(n):
    return is_prime(n) and is_prime(n + 2)

# Test the functions
print("Prime Number Analysis")
print("-" * 40)

# Count primes between 1 and 50
count1 = count_primes_between(1, 50)
print(f"Prime numbers between 1 and 50: {count1}")

# Find 10th prime
prime10 = nth_prime(10)
print(f"The 10th prime number is: {prime10}")

# Check twin primes
num1 = 11
twin1 = is_twin_prime(num1)
print(f"Is {num1} part of a twin prime pair? {twin1}")
if twin1:
    print(f"  Twin pair: ({num1}, {num1+2})")

num2 = 15
twin2 = is_twin_prime(num2)
print(f"Is {num2} part of a twin prime pair? {twin2}")

num3 = 29
twin3 = is_twin_prime(num3)
print(f"Is {num3} part of a twin prime pair? {twin3}")
if twin3:
    print(f"  Twin pair: ({num3}, {num3+2})")

# Count primes between 100 and 150
count2 = count_primes_between(100, 150)
print(f"\nPrime numbers between 100 and 150: {count2}")

Expected Output/Behavior:

Prime Number Analysis
----------------------------------------
Prime numbers between 1 and 50: 15
The 10th prime number is: 29
Is 11 part of a twin prime pair? True
  Twin pair: (11, 13)
Is 15 part of a twin prime pair? False
Is 29 part of a twin prime pair? True
  Twin pair: (29, 31)

Prime numbers between 100 and 150: 10
  • Key Points:
    • Efficiency: checking up to square root of n would be faster
    • Building functions on top of other functions creates powerful abstractions
    • The is_prime function is reused multiple times
    • Twin primes become rarer as numbers get larger

Problem 2: Password Strength Validator

Problem

Problem Statement: Create a password validation system that checks password strength. Your program should:

  • Define a function count_uppercase(password) that counts uppercase letters in the password
  • Define a function count_lowercase(password) that counts lowercase letters
  • Define a function count_digits(password) that counts numeric digits
  • Define a function has_special_char(password) that returns True if password contains at least one special character from: !@#$%^&*
  • Define a function calculate_strength(password) that returns a strength score (0-5):
    • 1 point if password has at least 2 uppercase letters
    • 1 point if password has at least 2 lowercase letters
    • 1 point if password has at least 2 digits
    • 1 point if password has special character
    • 1 point if password length >= 8
  • Define a function get_strength_label(score) that returns:
    • “Very Weak” for score 0-1
    • “Weak” for score 2
    • “Medium” for score 3
    • “Strong” for score 4
    • “Very Strong” for score 5
  • Test with these passwords (store each in a separate variable):
    • password1 = “abc”
    • password2 = “Hello”
    • password3 = “HeLLo123”
    • password4 = “Pass@123”
    • password5 = “MyP@ssW0rd!”
  • For each password, show the strength score and label

Solution
def count_uppercase(password):
    count = 0
    for char in password:
        if char >= 'A' and char <= 'Z':
            count += 1
    return count

def count_lowercase(password):
    count = 0
    for char in password:
        if char >= 'a' and char <= 'z':
            count += 1
    return count

def count_digits(password):
    count = 0
    for char in password:
        if char >= '0' and char <= '9':
            count += 1
    return count

def has_special_char(password):
    special_chars = "!@#$%^&*"
    for char in password:
        if char in special_chars:
            return True
    return False

def calculate_strength(password):
    score = 0
    
    if count_uppercase(password) >= 2:
        score += 1
    if count_lowercase(password) >= 2:
        score += 1
    if count_digits(password) >= 2:
        score += 1
    if has_special_char(password):
        score += 1
    if len(password) >= 8:
        score += 1
    
    return score

def get_strength_label(score):
    if score <= 1:
        return "Very Weak"
    elif score == 2:
        return "Weak"
    elif score == 3:
        return "Medium"
    elif score == 4:
        return "Strong"
    else:
        return "Very Strong"

# Test passwords
print("Password Strength Analysis")
print("-" * 40)

password1 = "abc"
score1 = calculate_strength(password1)
label1 = get_strength_label(score1)
print(f"Password: {password1}")
print(f"  Uppercase: {count_uppercase(password1)}, Lowercase: {count_lowercase(password1)}, Digits: {count_digits(password1)}")
print(f"  Strength Score: {score1}/5")
print(f"  Strength Label: {label1}")
print()

password2 = "Hello"
score2 = calculate_strength(password2)
label2 = get_strength_label(score2)
print(f"Password: {password2}")
print(f"  Uppercase: {count_uppercase(password2)}, Lowercase: {count_lowercase(password2)}, Digits: {count_digits(password2)}")
print(f"  Strength Score: {score2}/5")
print(f"  Strength Label: {label2}")
print()

password3 = "HeLLo123"
score3 = calculate_strength(password3)
label3 = get_strength_label(score3)
print(f"Password: {password3}")
print(f"  Uppercase: {count_uppercase(password3)}, Lowercase: {count_lowercase(password3)}, Digits: {count_digits(password3)}")
print(f"  Strength Score: {score3}/5")
print(f"  Strength Label: {label3}")
print()

password4 = "Pass@123"
score4 = calculate_strength(password4)
label4 = get_strength_label(score4)
print(f"Password: {password4}")
print(f"  Uppercase: {count_uppercase(password4)}, Lowercase: {count_lowercase(password4)}, Digits: {count_digits(password4)}")
print(f"  Strength Score: {score4}/5")
print(f"  Strength Label: {label4}")
print()

password5 = "MyP@ssW0rd!"
score5 = calculate_strength(password5)
label5 = get_strength_label(score5)
print(f"Password: {password5}")
print(f"  Uppercase: {count_uppercase(password5)}, Lowercase: {count_lowercase(password5)}, Digits: {count_digits(password5)}")
print(f"  Strength Score: {score5}/5")
print(f"  Strength Label: {label5}")

Expected Output/Behavior:

Password Strength Analysis
----------------------------------------
Password: abc
  Uppercase: 0, Lowercase: 3, Digits: 0
  Strength Score: 1/5
  Strength Label: Very Weak

Password: Hello
  Uppercase: 1, Lowercase: 4, Digits: 0
  Strength Score: 1/5
  Strength Label: Very Weak

Password: HeLLo123
  Uppercase: 3, Lowercase: 2, Digits: 3
  Strength Score: 4/5
  Strength Label: Strong

Password: Pass@123
  Uppercase: 1, Lowercase: 3, Digits: 3
  Strength Score: 4/5
  Strength Label: Strong

Password: MyP@ssW0rd!
  Uppercase: 3, Lowercase: 5, Digits: 1
  Strength Score: 4/5
  Strength Label: Strong
  • Key Points:
    • Character comparison using ASCII values
    • Modular design makes it easy to add new criteria
    • Each function has a single responsibility
    • Could enhance by checking for consecutive characters or common patterns

Problem 3: Mathematical Function Evaluator

Problem

Problem Statement: Create a program that evaluates mathematical functions at different points. Your program should:

  • Define a function quadratic(x, a, b, c) that evaluates ax² + bx + c
  • Define a function cubic(x, a, b, c, d) that evaluates ax³ + bx² + cx + d
  • Define a function find_quadratic_roots(a, b, c) that finds the roots of ax² + bx + c = 0
    • Use the quadratic formula: x = (-b ± √(b² - 4ac)) / 2a
    • Calculate discriminant = b² - 4ac
    • If discriminant < 0, return “No real roots”
    • If discriminant = 0, return the one root
    • If discriminant > 0, calculate and return both roots
  • Define a function evaluate_at_points(func_type, x1, x2, x3, a, b, c) that:
    • If func_type is “quadratic”, evaluate quadratic function at x1, x2, x3
    • If func_type is “linear”, evaluate ax + b at x1, x2, x3 (use c=0)
    • Print the results for each point
  • Test your functions:
    • Evaluate quadratic 2x² + 3x - 5 at x = -2, 0, 2
    • Evaluate cubic x³ - 2x² + x - 1 at x = -1, 1, 3
    • Find roots of x² - 5x + 6 = 0
    • Find roots of x² + 2x + 5 = 0
    • Evaluate linear function 3x + 7 at x = -3, 0, 4

Solution
def quadratic(x, a, b, c):
    result = a * x * x + b * x + c
    return result

def cubic(x, a, b, c, d):
    result = a * x * x * x + b * x * x + c * x + d
    return result

def find_quadratic_roots(a, b, c):
    # Calculate discriminant
    discriminant = b * b - 4 * a * c
    
    if discriminant < 0:
        return "No real roots"
    elif discriminant == 0:
        root = -b / (2 * a)
        return root
    else:
        # Two roots
        sqrt_discriminant = pow(discriminant, 0.5)
        root1 = (-b + sqrt_discriminant) / (2 * a)
        root2 = (-b - sqrt_discriminant) / (2 * a)
        return root1, root2

def evaluate_at_points(func_type, x1, x2, x3, a, b, c):
    if func_type == "quadratic":
        y1 = quadratic(x1, a, b, c)
        y2 = quadratic(x2, a, b, c)
        y3 = quadratic(x3, a, b, c)
        print(f"Quadratic {a}x² + {b}x + {c}:")
    elif func_type == "linear":
        y1 = a * x1 + b
        y2 = a * x2 + b
        y3 = a * x3 + b
        print(f"Linear {a}x + {b}:")
    
    print(f"  f({x1}) = {y1}")
    print(f"  f({x2}) = {y2}")
    print(f"  f({x3}) = {y3}")

# Test the functions
print("Mathematical Function Evaluator")
print("-" * 40)

# Evaluate quadratic 2x² + 3x - 5
print("Evaluating 2x² + 3x - 5:")
y1 = quadratic(-2, 2, 3, -5)
y2 = quadratic(0, 2, 3, -5)
y3 = quadratic(2, 2, 3, -5)
print(f"  f(-2) = {y1}")
print(f"  f(0) = {y2}")
print(f"  f(2) = {y3}")
print()

# Evaluate cubic x³ - 2x² + x - 1
print("Evaluating x³ - 2x² + x - 1:")
c1 = cubic(-1, 1, -2, 1, -1)
c2 = cubic(1, 1, -2, 1, -1)
c3 = cubic(3, 1, -2, 1, -1)
print(f"  f(-1) = {c1}")
print(f"  f(1) = {c2}")
print(f"  f(3) = {c3}")
print()

# Find roots
print("Finding roots of x² - 5x + 6 = 0:")
roots1 = find_quadratic_roots(1, -5, 6)
if roots1 != "No real roots":
    if isinstance(roots1, tuple):
        print(f"  Root 1: {roots1[0]}")
        print(f"  Root 2: {roots1[1]}")
    else:
        print(f"  Single root: {roots1}")
print()

print("Finding roots of x² + 2x + 5 = 0:")
roots2 = find_quadratic_roots(1, 2, 5)
print(f"  Result: {roots2}")
print()

# Evaluate linear function
evaluate_at_points("linear", -3, 0, 4, 3, 7, 0)

Expected Output/Behavior:

Mathematical Function Evaluator
----------------------------------------
Evaluating 2x² + 3x - 5:
  f(-2) = 3
  f(0) = -5
  f(2) = 9

Evaluating x³ - 2x² + x - 1:
  f(-1) = -5
  f(1) = -1
  f(3) = 11

Finding roots of x² - 5x + 6 = 0:
  Root 1: 3.0
  Root 2: 2.0

Finding roots of x² + 2x + 5 = 0:
  Result: No real roots

Linear 3x + 7:
  f(-3) = -2
  f(0) = 7
  f(4) = 19
  • Key Points:
    • Using pow(x, 0.5) for square root since we haven’t covered math module
    • Tuple return for multiple values
    • Building complex calculations from simpler functions
    • Type checking with isinstance (preview of advanced concepts)