From Functions to Modules

EE1021 Programming for Engineers · Lecture 3 Lab · 2026–2027

Basri Erdogan

Lab Outcomes

By the end of the lab, you should be able to:

  1. Write and call a function that returns a useful value
  2. Test a function with normal and boundary inputs
  3. Use decisions and loops inside functions
  4. Import selected functions from the standard math module
  5. Place reusable functions in sensor_tools.py and call them from main.py

Working Method

For every task:

  1. Predict or outline before typing
  2. Implement the smallest working function
  3. Call it with at least two inputs
  4. Compare the output with your prediction
  5. Correct and explain one mistake you found

Keep functions focused: one function should have one clear job.

Stage 1: Diagnose Data Flow

Why Does result Become None?

def double_value(value):
    print(value * 2)

result = double_value(5)
print("result:", result)

Before running:

  1. Predict both displayed lines.
  2. Identify which line displays 10.
  3. Explain why result is not 10.

The function displays 10, then returns None. Replace print(value * 2) with return value * 2 when the caller needs the result.

Task 1: Repair and Test

Open starter/return_vs_print_bug.py.

  • Change the function so it returns the doubled value.
  • Keep the final print() in the main code.
  • Test with 5, 0, and -3.
  • Add one comment explaining the difference between displaying and returning.

Expected results from the repaired function:

Argument Returned value
5 10
0 0
-3 -6

Stage 2: Build Small Functions

Task 2: Calibrate One Reading

Create a function with this contract:

def calibrate_voltage(raw_voltage, offset=0.05):
    """Return raw_voltage corrected by offset."""
    # your code

Test these calls:

print(calibrate_voltage(4.92))
print(calibrate_voltage(4.92, 0.10))
print(calibrate_voltage(0.0, -0.02))

Record the three expected values before running the program.

Check the Default Argument

def calibrate_voltage(raw_voltage, offset=0.05):
    return raw_voltage + offset

Answer in a comment:

  • Which call uses the default?
  • Which call overrides it?
  • Why should raw_voltage not have a default?

Do not create separate functions for standard and custom offsets. One parameterized function should handle both cases.

Task 3: Return a Classification

Implement:

def voltage_status(voltage):
    """Return Invalid, Low, Normal, or High."""
    # Invalid: voltage < 0
    # Low:     0 <= voltage < 4.75
    # Normal:  4.75 <= voltage <= 5.25
    # High:    voltage > 5.25

Your function must return exactly one string and must not call input().

Test the Boundaries

Use a separate call for every test.

print(voltage_status(-0.1))
print(voltage_status(4.74))
print(voltage_status(4.75))
print(voltage_status(5.25))
print(voltage_status(5.26))

Create a short test table in a comment:

Input Expected Actual

Correct the function if any expected and actual values differ.

Stage 3: Put Lecture 2 Patterns in Functions

Task 4: Package an Accumulator

Implement:

def sum_multiples(limit, divisor):
    """Return the sum of positive multiples up to limit."""
    total = 0
    # add a for loop
    return total

Required tests:

print(sum_multiples(12, 3))   # 30
print(sum_multiples(10, 2))   # 30
print(sum_multiples(4, 5))    # 0

Explain why the last call returns 0.

Check-Off A: Functions in One File

Show the instructor or teaching assistant:

  • calibrate_voltage() with a default argument
  • voltage_status() passing all five boundary tests
  • sum_multiples() passing all three tests
  • one sentence explaining why input() belongs in the main code rather than these calculation functions

Do not continue until your functions return the expected values.

Stage 4: Reuse Modules

Task 5: Use the Standard math Module

For a series circuit, calculate impedance magnitude:

|Z| = square root of (resistance² + reactance²)

import math

def impedance_magnitude(resistance, reactance):
    # return the magnitude using math.sqrt
    pass

Test:

print(impedance_magnitude(3, 4))   # 5.0
print(impedance_magnitude(5, 12))  # 13.0

Two Files, Two Responsibilities

sensor_tools.py

  • owns reusable function definitions
  • does not ask the user for input
  • does not run the overall workflow

main.py

  • imports sensor_tools
  • reads or defines the raw value
  • calls the functions
  • displays the final report

Download or open sensor_tools.py and main.py. Keep them in the same folder.

Task 6: Complete sensor_tools.py

def calibrate_voltage(raw_voltage, offset=0.05):
    """Return raw_voltage corrected by offset."""
    pass

def voltage_status(voltage):
    """Return Invalid, Low, Normal, or High."""
    pass

Replace every pass. Keep the function names and parameters unchanged so that main.py can call them.

Complete the Main Program

main.py

import sensor_tools

raw_voltage = 4.72

calibrated = sensor_tools.calibrate_voltage(raw_voltage)
status = sensor_tools.voltage_status(calibrated)

print("Raw:", raw_voltage)
print("Calibrated:", calibrated)
print("Status:", status)

Run main.py, then test at least three raw readings that produce different statuses.

Final Check-Off

Demonstrate a working two-file program and explain:

  1. Which file defines the reusable functions?
  2. Why do calls use the prefix sensor_tools.?
  3. What value does calibrate_voltage() return?
  4. Which boundary inputs did you test?
  5. What would happen if sensor_tools.py were in a different folder?

Submit or retain the completed files according to the instructor’s directions.

Extension: Debug a Mathematical Expression

The intended expression is:

√2 × sin(π/4)

import math

wrong = math.sqrt(2) * 0.25 * math.pi
correct = math.sqrt(2) * math.sin(0.25 * math.pi)

Explain why the two expressions differ and verify that correct is approximately 1.0.