Dev Diary 1: Improving My Python Grade Statistics Program
Dev Diary 1: Improving My Python Grade Statistics Program
As a beginner Python programmer, one of the most exciting parts of learning has been building something that feels useful. My original project was a simple program to collect student exam and exercise data, then calculate grades and print out some basic statistics. The idea was solid, but the implementation was clunky and limited in many ways.
In this dev diary, I want to walk through the process of taking that rough first version and gradually refining it into a more robust, readable, and user-friendly script. I’ll share the changes I made, why I made them, and what I learned along the way. Hopefully, this will help other beginners understand how small improvements can add up to make a big difference.
Original Version
Here’s a look at my original script:
def results() -> list: points_exercises = [] while True: user_input = input("Exam points and exercises completed: ") if user_input == "": break points_exercises.append(user_input.split()) return points_exercises def calculate_grade(points_exercises : list): points = 0 length = len(points_exercises) grade_sum = 0 for entry in points_exercises: exam_points = int(entry[0]) exercises_completed = int(entry[1]) points = convert_exercise_points(exercises_completed) grade = calculate_course_grade(exam_points, points) grade_sum += grade points_average = calculate_average(grade_sum, length) print("Statistics:") print(f"Points average: {points_average}") calculate_grade_distribution(points_exercises, length) def convert_exercise_points(points : int) -> int: exercise_points = points // 10 return exercise_points def calculate_course_grade(exam_points : int, points : int) -> int: grade = exam_points + points return grade def calculate_average(grade_sum : int, length : int) -> float: points_average = grade_sum / length return points_average def calculate_pass_percentage(grade : int, length: int) -> float: return 0 def calculate_grade_distribution(points_exercises: list, length): grade_0 = grade_1 = grade_2 = grade_3 = grade_4 = grade_5 = 0 for entry in points_exercises: exam_points = int(entry[0]) exercises_completed = int(entry[1]) points = convert_exercise_points(exercises_completed) if exam_points < 10: grade = 0 else: grade = calculate_course_grade(exam_points, points) if grade <= 14: grade_0 += 1 elif grade <= 17: grade_1 += 1 elif grade <= 20: grade_2 += 1 elif grade <= 23: grade_3 += 1 elif grade <= 27: grade_4 += 1 else: # grade <= 30 grade_5 += 1 pass_percentage = (1 - (grade_0 / length)) * 100 print(f"Pass percentage: {pass_percentage:.1f}") print("Grade distribution:") print(f" 5: {'*' * grade_5}") print(f" 4: {'*' * grade_4}") print(f" 3: {'*' * grade_3}") print(f" 2: {'*' * grade_2}") print(f" 1: {'*' * grade_1}") print(f" 0: {'*' * grade_0}") def main(): points_exercises = results() calculate_grade(points_exercises) main()
The core functionality was there, but it had several issues:
- Unclear input format: The user had to type exam points and exercises in a single line, like
25 80
. This was confusing. - Poor error handling: No feedback if the user typed letters or invalid numbers.
- Redundant logic: Some variables were calculated multiple times inside loops.
- No separation of concerns: Everything was tightly coupled together.
- No individual student breakdown.
- No comments or docstrings.
Major Improvements
1. Renamed and Reorganized Functions
Instead of vague names like results()
and calculate_grade()
, I used clearer function names that explain exactly what they do. For example:
Before:
def results() -> list:
After:
def get_user_input() -> list:
This made the code easier to read and understand at a glance.
2. Improved User Input
I changed the input method to be step-by-step, asking for the exam score and the exercises completed one at a time. This also let me validate the input better.
Before:
user_input = input("Exam points and exercises completed: ") points_exercises.append(user_input.split())
After:
user_input1 = input(f"({i + 1}) Enter the exam points: ") user_input2 = input(f"({i + 1}) Enter the exercises completed: ")
I also added checks to make sure scores are between 0-30 for exams and 0-100 for exercises.
3. Error Handling
I added try...except
blocks so the program won’t crash if the user types something invalid.
After:
try: exam_points = int(exam_str) exercises_completed = int(exercises_str) if 0 <= exam_points <= 30 and 0 <= exercises_completed <= 100: data.append((exam_points, exercises_completed)) else: print("Invalid input. Exam (0-30), Exercises (0-100).") except ValueError: print("Invalid input. Please enter two integers.")
4. Separation of Logic
Originally, the grade calculation was scattered and tangled. I split it into small, focused functions:
convert_exercise_points()
calculate_total_points()
determine_grade()
calculate_statistics()
Each of these has one job and is easier to test and understand.
5. Added Individual Student Output
The original version gave only totals. Now each student’s data and grade are shown.
After:
print("\nIndividual results:") for i, (exam_points, exercises_completed) in enumerate(data, start=1): total_points = calculate_total_points(exam_points, exercises_completed) grade = determine_grade(total_points) print(f" Student {i}: Exam {exam_points} + Exercises {convert_exercise_points(exercises_completed)} = Total {total_points} → Grade {grade}")
6. Grade Distribution & Pass Percentage
This was mostly working in the original code, but I cleaned it up and made sure it reflected the actual grade values from 0 to 5.
7. Fail-Safe for Empty Input
Previously, if you didn’t input anything, the program would break or show bad data. I added checks to handle this gracefully.
After:
def calculate_statistics(data: list[tuple[int, int]]) -> None: if len(data) == 0: print("No data to analyze.") return
8. Cleaner Main Function and Guard Clause
Adding if __name__ == "__main__":
protects the script so it doesn’t run automatically if imported somewhere else.
After:
def main(): data = get_user_input() if data: calculate_statistics(data) else: print("No data entered.") if __name__ == "__main__": main()
9. Added Comments and Docstrings
Comments were added to explain each step, which is especially helpful for future me (or other beginners looking at the code).
Example:
# Convert exercises to exercise points: 1 point per 10 exercises return exercises_completed // 10
Final Version Summary
By the end, I had a program that:
- Collects user input clearly and safely
- Validates inputs
- Calculates grades and exercise points logically
- Prints clear, detailed results
- Is broken down into clean, focused functions
- Handles errors and edge cases
- Is documented with helpful comments
This is what the top of my final version looks like now:
def get_user_input() -> list: """Collect exam and exercise inputs from the user.""" # ... def convert_exercise_points(exercises_completed: int) -> int: # Convert exercises to exercise points return exercises_completed // 10 # ...
What I Learned
- Naming matters. Clear function names make your code readable.
- Breaking code into small functions makes debugging and testing much easier.
- Validating input is essential in any program that accepts user input.
- Python exceptions (try/except) are powerful and necessary.
- Planning and organizing code pays off in the long run.
My Thoughts
As a beginner, this refactoring project taught me more than any single tutorial. It forced me to think critically about design, user experience, and code clarity. I can now see how much room there is to grow, and how even small improvements can lead to a much better product.
Next, I might explore saving results to a file or adding a simple GUI with tkinter
. But for now, I’m proud of how far this project has come.
Thanks for reading, and happy coding!
Final Version
The final version can be found at my GitHub repository.