Final Project: Student Grade Management (Part 2)
In Lesson 34, we completed the data layer — CRUD operations for students and grades. In this lesson, we build the logic layer on top of the data layer: statistical calculations, ranking, score distribution analysis, and other core business logic. Lesson 36 will add the presentation layer (menu interface) to complete the system.
Logic Layer Implementation
This layer's code depends on the data layer functions from Lesson 34 (load_data, get_all_students, etc.).
Example: Grade Statistics Logic Layer
# ====== Logic Layer: Grade Statistics and Analysis ======
import statistics
from grade_project_part1 import load_data, get_all_students
def calculate_student_average(student_id):
"""Calculate a single student's average score"""
data = load_data()
student = data["students"].get(student_id)
if not student:
return None
scores = student["scores"].values()
if not scores:
return None
return sum(scores) / len(scores)
def get_student_report(student_id):
"""Generate an individual report card"""
data = load_data()
student = data["students"].get(student_id)
if not student:
return None
report = {
"name": student["name"],
"class_name": student["class_name"],
"scores": dict(student["scores"]),
}
scores = list(student["scores"].values())
if scores:
report["average"] = round(sum(scores) / len(scores), 1)
report["max_score"] = max(scores)
report["min_score"] = min(scores)
report["total"] = sum(scores)
else:
report["average"] = None
report["max_score"] = None
report["min_score"] = None
report["total"] = 0
return report
def get_class_ranking(class_name=None):
"""Get class ranking (by total score descending)"""
data = load_data()
students = data["students"]
# Collect all students (or a specific class)
results = []
for sid, info in students.items():
if class_name and info["class_name"] != class_name:
continue
scores = list(info["scores"].values())
total = sum(scores) if scores else 0
avg = round(total / len(scores), 1) if scores else 0
results.append({
"student_id": sid,
"name": info["name"],
"class_name": info["class_name"],
"total": total,
"average": avg,
"score_count": len(scores)
})
# Sort by total descending
results.sort(key=lambda x: x["total"], reverse=True)
# Add ranking
for i, r in enumerate(results, 1):
r["rank"] = i
return results
def get_subject_averages():
"""Calculate average scores for each subject"""
data = load_data()
subjects = data["subjects"]
students = data["students"]
result = {}
for subject in subjects:
scores = []
for info in students.values():
if subject in info["scores"]:
scores.append(info["scores"][subject])
if scores:
result[subject] = {
"average": round(sum(scores) / len(scores), 1),
"max": max(scores),
"min": min(scores),
"count": len(scores)
}
else:
result[subject] = None
return result
def get_score_distribution(subject=None):
"""Calculate score distribution across ranges"""
data = load_data()
students = data["students"]
ranges = [
("90-100", 90, 101),
("80-89", 80, 90),
("70-79", 70, 80),
("60-69", 60, 70),
("0-59", 0, 60),
]
if subject:
# Count distribution for a specific subject
result = {label: 0 for label, _, _ in ranges}
for info in students.values():
if subject in info["scores"]:
score = info["scores"][subject]
for label, low, high in ranges:
if low <= score < high:
result[label] += 1
break
return result
else:
# Count distribution across all subjects combined
result = {label: 0 for label, _, _ in ranges}
for info in students.values():
for score in info["scores"].values():
for label, low, high in ranges:
if low <= score < high:
result[label] += 1
break
return result
def get_students_by_class():
"""Group students by class"""
data = load_data()
students = data["students"]
classes = {}
for sid, info in students.items():
cls = info["class_name"]
if cls not in classes:
classes[cls] = []
classes[cls].append({
"student_id": sid,
"name": info["name"],
"score_count": len(info["scores"])
})
return classes
Logic Layer Testing
Example: Logic Layer Functional Test
if __name__ == "__main__":
print("=== Individual Report Card ===")
report = get_student_report("2024001")
if report:
print(f"Name: {report['name']}")
print(f"Class: {report['class_name']}")
for subject, score in report["scores"].items():
print(f" {subject}: {score}")
print(f"Total: {report['total']}")
print(f"Average: {report['average']}")
print(f"Max: {report['max_score']}")
print(f"Min: {report['min_score']}")
print("\n=== Class Ranking ===")
ranking = get_class_ranking()
for r in ranking[:5]:
print(f"#{r['rank']}: {r['name']} ({r['class_name']}) Total {r['total']}")
print("\n=== Subject Averages ===")
averages = get_subject_averages()
for subject, info in averages.items():
if info:
print(f"{subject}: Avg {info['average']}, Max {info['max']}, Min {info['min']}")
print("\n=== Score Distribution ===")
dist = get_score_distribution()
for label, count in dist.items():
bar = "#" * count
print(f"{label}: {bar} {count} students")
Note: The
importabove assumes the data layer file is namedgrade_project_part1.py. Modify the import statement if your filename differs. In the final complete system, all code will be merged into one file.
Exception Handling Design
The logic layer handles the following exceptional cases:
| Scenario | Return Value | Description |
|---|---|---|
| Student doesn't exist | None or empty result |
Caller checks the return value |
| No grade data | Statistics value is None |
Caller displays "No grades yet" |
| Empty data | Empty list/empty dict | Won't crash, displays friendly message |
| Invalid data | Exception caught | Protected by try-except |
FAQ
key=lambda in sorting?key parameter specifies the sorting criterion. lambda item: item[1] means "take the second element of each item as the sorting key." This is equivalent to def get_score(item): return item[1] — lambda is just shorthand.None instead of 0 make things harder for the caller?None distinguishes between "no score exists" and "the score is actually 0" — a meaningful difference. The caller can use if result is not None: to check, which is safer than mistakenly treating "no data" as "0 points."Summary
- The logic layer sits on top of the data layer, focusing on "computation" rather than "storage"
- Individual report card: displays per-subject scores, total, average, max/min
- Class ranking: sorted by total score descending, with auto-generated rank numbers
- Subject averages: iterate all students' scores, aggregate by subject
- Score distribution: count students in each score range
- Exception handling: return
Noneinstead of erroring when student/score doesn't exist
Exercises
-
Beginner (Difficulty: Star): Call
get_student_report()to view a complete report card for one student and verify the output is correct. -
Intermediate (Difficulty: Star-Star): Add a
get_top_n(n)function to the logic layer that returns the top N students by total score across the entire school. -
Advanced (Difficulty: Star-Star-Star): Add a
get_subject_pass_rate(subject, pass_score=60)function to the logic layer that calculates the pass rate for a given subject (students with score >= pass_score / total students). Then calculate and compare pass rates across all subjects.



