Computer Programming 1 (Python) • Problem Solving & Mini-Projects

Program Design Process — I can design a small program by writing requirements, breaking it into functions, and testing it.

Learn to plan before you code so your programs are organized, reliable, and easy to debug.

A Python coding terminal with a program flow and laptop.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

In the earliest days of computing, programmers wrote code by directly entering machine instructions, one at a time, with almost no planning ahead. Programs were short and simple enough that a single person could hold the entire thing in their head. But as computers became more powerful and software grew more complex, this "just start coding" approach quickly fell apart. Bugs multiplied, projects ran late, and entire programs had to be rewritten from scratch. The need for a program design process — a structured way to plan, build, and verify software — became one of the most important ideas in all of computer science.

1968 • The Software Crisis

At a NATO conference, engineers coined the term software crisis to describe how large projects were failing due to poor planning. This moment launched the field of software engineering.

1970 • Waterfall Model Introduced

Winston Royce published a paper describing a sequential design process — requirements first, then design, then coding, then testing — known as the waterfall model. It became the standard for decades.

1991 • Python Is Released

Guido van Rossum released Python, a language designed for readability and rapid prototyping. Its clean syntax made it ideal for learning structured program design.

2001 • Agile Manifesto

Seventeen developers published the Agile Manifesto, promoting iterative design with frequent testing. The core idea — plan, build small pieces, test, repeat — is exactly the process you will learn in this lesson.

The central question this history raises is straightforward: how do you go from a vague idea — "I want a program that does X" — to working, tested code? The answer is a repeatable design process that every professional programmer uses, and that you can start practicing right now with Python.

SECTION 2

Core Principles of Program Design

The program design process can be broken into three major phases that repeat in a cycle. First, you define requirements — what the program should do. Next, you decompose the problem into smaller functions, each handling one specific task. Finally, you test each function and the program as a whole to make sure everything works correctly. These three phases form the backbone of good software development.

1

Requirements Gathering

Clearly state what the program's inputs, outputs, and expected behavior are. Write these down before touching any code. Think of requirements as a contract between you and the program.
2

Functional Decomposition

Break the problem into smaller, independent pieces. Each piece becomes a Python function with a clear name, defined inputs (parameters), and a known output (return value).
3

Incremental Testing

Test each function individually before combining them. Use known inputs and check that the output matches your expectations. Fix bugs in small pieces, not the whole program at once.
4

Iteration & Refinement

After testing, revisit your requirements and design. Real programs evolve — you may discover you missed a requirement or that a function should be split into two. This cycle is normal and expected.
✦

KEY TAKEAWAY

Think of designing a program like planning a road trip. Requirements are your destination and what you need to bring. Decomposition is breaking the drive into legs with rest stops. Testing is checking the map at each stop to make sure you are still on course. If you skip the planning and just start driving, you will waste time being lost.

SECTION 3

Visualizing the Design Cycle

The diagram below shows the program design process as a cycle. Notice that it is not a straight line — after testing, you loop back to refine requirements or adjust your functions. Each pass through the cycle makes the program more correct and more polished.

Program Design Cycle1. RequirementsDefine inputs, outputs, rules2. DecomposeBreak into functions3. Code & TestWrite & verify each function4. RefineIterate & improveArrows show the iterative flow: requirements → decompose → code & test → refine → repeat
The four boxes represent the major phases. The arrows form a loop, emphasizing that program design is iterative — you revisit earlier steps as you learn more about the problem.

In practice, a beginner may go around this cycle two or three times for a small program. Professional developers on large projects may iterate dozens of times. The key insight is that each pass through the cycle is productive — even when you find a mistake, that discovery helps you build a more robust program. This is why the loop in the diagram matters more than any single box.

SECTION 4

How the Design Process Works in Practice

Phase 1 — Writing Requirements

A requirement is a clear, specific statement about what your program must do. Good requirements answer three questions: What data goes in? What data comes out? What rules govern the transformation? You should write requirements in plain English (or whatever language you think in) before you write a single line of Python. For example, if you are building a tip calculator, your requirements might be: the user inputs a meal total and a tip percentage; the program outputs the tip amount and the grand total; the tip percentage must be between 0 and 100.

💡

Tip: Use the IPO Chart

An IPO chart (Input → Process → Output) is a simple table that forces you to think through requirements before coding. List every input, describe the processing step, and state the expected output.

Phase 2 — Decomposing into Functions

Functional decomposition means splitting a big problem into smaller, self-contained tasks, where each task becomes its own Python function. A well-designed function does one thing, has a descriptive name, accepts parameters for its inputs, and returns a result. For the tip calculator, you might write a function calculate_tip(meal_total, tip_percent) that returns the tip amount, and a second function calculate_grand_total(meal_total, tip) that returns the sum. A third function, get_user_input(), could handle prompting the user and validating the data.

Phase 3 — Testing

Testing means running your functions with known inputs and checking that the outputs match your expectations. The simplest technique is a manual test case: you call the function in a script or Python shell, pass in specific values, and compare the result to what you calculated by hand. For instance, calculate_tip(50.00, 20) should return 10.00. If it does not, you know there is a bug in that function and can fix it immediately. Testing each function individually before combining them is called unit testing, and it is far easier than debugging an entire program at once.

⚠

Edge Cases Matter

Always test with at least three types of input: a normal value, a boundary value (like 0 or the maximum), and an invalid value (like a negative number). These are called edge cases, and they are where most bugs hide.

SECTION 5

Anatomy of a Well-Decomposed Program

To see how decomposition works at the code level, let's examine a small program that converts temperatures. The user enters a temperature in Fahrenheit, and the program displays the equivalent in Celsius and Kelvin. Below is a diagram showing how the program's functions connect to each other.

Function Map — Temperature Convertermain()Orchestrates the programget_fahrenheit()Prompts user, returns floatto_celsius(f)Returns (f − 32) × 5/9to_kelvin(c)Returns c + 273.15display_results(f, c, k)Prints formatted output to screenEach box is one Python function. main() calls the others in sequence.main → get_fahrenheit → to_celsius → to_kelvin → display_results
The main() function acts as the conductor, calling each helper function in the right order. Each helper has a single responsibility, making it easy to test independently.
FunctionInput (Parameters)Output (Return Value)Responsibility
get_fahrenheit()None (reads from user)floatPrompt user and validate input
to_celsius(f)f — a floatfloatConvert Fahrenheit to Celsius
to_kelvin(c)c — a floatfloatConvert Celsius to Kelvin
display_results(f, c, k)Three floatsNone (prints to screen)Format and print the results
main()NoneNoneCall all functions in the correct order

Function breakdown for the temperature converter

Notice how each function's responsibility fits in one short phrase. If you cannot describe a function's job in one sentence, it is probably doing too much and should be split. This is a practical rule of thumb for functional decomposition.

SECTION 6

Worked Example — Temperature Converter

Let's walk through the entire design process for the temperature converter program, from requirements to tested code.

Designing a Temperature Converter

  1. 1

    Step 1 — Write Requirements

    We state our requirements clearly. Input: a temperature in Fahrenheit (a number). Output: the same temperature expressed in Celsius and Kelvin, formatted to two decimal places. Rules: the program should reject non-numeric input gracefully.
    Requirements documented before any code is written.
  2. 2

    Step 2 — Identify Functions

    We identify four distinct tasks: getting input, converting F→C, converting C→K, and displaying output. Each becomes its own function. We also write a main() function to tie them together. The conversion formulas are: Celsius = (F − 32) × 5 ÷ 9 and Kelvin = C + 273.15.
    Five functions identified: get_fahrenheit, to_celsius, to_kelvin, display_results, main
  3. 3

    Step 3 — Write the Code

    Here is the complete Python code: def get_fahrenheit(): while True: try: f = float(input("Enter temperature in °F: ")) return f except ValueError: print("Please enter a valid number.") def to_celsius(f): return (f - 32) * 5 / 9 def to_kelvin(c): return c + 273.15 def display_results(f, c, k): print(f"{f:.2f} °F = {c:.2f} °C = {k:.2f} K") def main(): f = get_fahrenheit() c = to_celsius(f) k = to_kelvin(c) display_results(f, c, k) main() Each function is short, clear, and does one thing.
    Code written with five focused functions.
  4. 4

    Step 4 — Test Individual Functions

    We test each conversion function manually. Calling to_celsius(212) should return 100.0 because 212°F is the boiling point of water. Calling to_kelvin(100.0) should return 373.15. We also test with 32°F (freezing point), which should give 0°C and 273.15 K. Finally, we try −40°F, which is the same in both Fahrenheit and Celsius: to_celsius(-40) returns -40.0.
    All tests pass — each function returns the expected value.
  5. 5

    Step 5 — Run the Full Program & Refine

    We run the whole program and enter 72 (a typical room temperature). The output is "72.00 °F = 22.22 °C = 295.37 K", which looks correct. We also type "hello" to check our error handling — the program asks again for valid input instead of crashing. If anything were wrong, we would go back to the relevant function and fix just that piece.
    Program complete, tested, and working.

SECTION 7

Strengths & Common Pitfalls

Using a structured design process has clear advantages, but beginners often encounter predictable pitfalls. Understanding both sides helps you apply the process more effectively.

StrengthsCommon Pitfalls
Bugs are isolated to one function, making debugging faster.Writing functions that are too large and try to do multiple things at once.
Requirements prevent wasted effort — you know what to build before you start.Writing vague requirements like "the program should work correctly" instead of specifying inputs and outputs.
Functions can be reused in other programs without rewriting code.Skipping testing because the code "looks right" — untested code almost always has hidden bugs.
Code is easier for others (and your future self) to read and understand.Naming functions poorly (e.g., func1() instead of calculate_tip()).
Team members can work on different functions in parallel.Refusing to iterate — treating the first draft as final instead of revisiting and improving.
✦

KEY TAKEAWAY

The design process does not slow you down — it speeds you up. Spending 10 minutes planning can save you an hour of frustrated debugging. Think of it like measuring lumber before cutting: you can always trim more, but you cannot un-cut a board.

SECTION 8

Connection to Professional Software Development

The requirements-decomposition-testing process you are learning now is a simplified version of what professional software engineers do every day. As projects grow larger, the same core ideas scale up with additional tools and techniques. The table below shows how each phase of your process maps to its professional counterpart.

What You Do NowProfessional VersionKey Concept
Write requirements in plain EnglishProduct managers write formal specs with user storiesRequirements engineering
Break code into functionsArchitects design modules, classes, and APIsSoftware architecture
Test functions manually with known inputsAutomated test suites run thousands of tests in secondsTest-driven development (TDD)
Iterate and refineAgile sprints with two-week cycles and retrospectivesAgile methodology

In future courses, you will learn about object-oriented design, where functions are grouped into classes that model real-world objects. You will also encounter version control systems like Git that track every change to your code. But all of these advanced tools rest on the same foundation you are building now: think first, decompose, and test relentlessly.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words why writing requirements before coding saves time. Give at least one specific example of what could go wrong without requirements.
Show answer
Requirements force you to define inputs, outputs, and rules before you start coding. Without them, you might build a feature the wrong way and only realize it after hours of work. For example, imagine building a grade calculator and forgetting to specify whether the grading scale is A–F or pass/fail. You could write the entire program and then discover you need to redesign it from scratch. Requirements act like a blueprint — they prevent misunderstandings and wasted effort.

Question 2: BASIC CALCULATION

You are designing a program that calculates the area of a rectangle. List the requirements (input, output, rules), and then write the names and parameters of the functions you would create. You do not need to write the actual code.
Show answer
Requirements: Input: length and width as positive numbers. Output: the area (length × width). Rules: both values must be greater than zero. Functions: get_dimensions() prompts the user and returns length and width. calculate_area(length, width) returns the product. display_area(area) prints the result. main() calls them in sequence. Each function has a single, clear job.

Question 3: INTERMEDIATE

A student writes one large function that asks for a name, calculates a test average from three scores, assigns a letter grade, and prints the result — all in 25 lines. Explain what is wrong with this design and how you would refactor it into separate functions.
Show answer
The function violates the principle of single responsibility — it does four different things. If the letter grade logic has a bug, you have to dig through all 25 lines to find it. Refactoring would mean creating four functions: get_scores() to collect the three scores, calculate_average(s1, s2, s3) to return the mean, assign_grade(average) to return the letter grade, and display_report(name, average, grade) to print the output. Each function can now be tested independently, and a bug in grading does not affect input collection.

Question 4: APPLIED

You need to build a tip calculator for a restaurant. The user enters the meal cost and a tip percentage (15, 18, or 20). The program should display the tip amount and the total bill. Write the complete Python program using the design process: list requirements, define functions, write code, and include at least two test cases as comments in your code.
Show answer
Requirements: Input: meal cost (positive float), tip percentage (15, 18, or 20). Output: tip amount and total bill, both formatted to two decimals. Rules: reject invalid percentages. Code: def get_meal_cost(): return float(input("Meal cost: $")) def get_tip_percent(): while True: p = int(input("Tip % (15, 18, or 20): ")) if p in [15, 18, 20]: return p print("Choose 15, 18, or 20.") def calculate_tip(cost, percent): return cost * percent / 100 def main(): cost = get_meal_cost() pct = get_tip_percent() tip = calculate_tip(cost, pct) total = cost + tip print(f"Tip: ${tip:.2f} Total: ${total:.2f}") # Test: calculate_tip(50, 20) → 10.00 # Test: calculate_tip(100, 15) → 15.00 main() The test cases confirm that the core math function works correctly before we rely on it in the full program.

Question 5: CRITICAL THINKING

Imagine you designed and tested a quiz program that asks five true/false questions and reports the score. After sharing it with a friend, they tell you they want ten questions and multiple-choice options instead of true/false. How does the design process help you handle this change? Which functions would you need to modify, and which could stay the same? Explain your reasoning.
Show answer
Because the program was decomposed into functions, the change is manageable rather than catastrophic. The function that asks a single question would need to be updated to handle multiple-choice instead of true/false — its internal logic changes, but its interface (take a question, return whether the answer is correct) can remain similar. The function that stores or generates questions would need to be expanded from five to ten items and include answer choices. However, the function that calculates the score (correct answers divided by total) can stay the same because it does not care about the question format — it just counts right answers. The display function might need a small tweak to show "/10" instead of "/5." This illustrates the power of decomposition: changes are localized to the relevant functions, and well-designed functions with clear inputs and outputs act as stable boundaries that absorb change gracefully.

SUMMARY

Lesson Summary

Summary

The program design process is a repeatable cycle that turns a vague idea into working, reliable software. You begin by writing requirements that define the program's inputs, outputs, and rules. Next, you use functional decomposition to break the problem into small, named Python functions, each with a single responsibility. Then you test each function individually using known inputs and expected outputs before combining them into the full program.

This process is iterative — after testing, you revisit earlier phases to refine your design. Good function names, edge case testing, and the habit of planning before coding are skills that transfer directly to professional software development, including Agile methodology and test-driven development. Master this cycle now, and every future programming project will feel more manageable.

Varsity Tutors • Computer Programming 1 (Python) • Program Design Process