Python: Project: Grade Mgmt (Pt.1)
This is the capstone project of the Python tutorial. Over three lessons, we will build a complete student grade management system from scratch. The project has three layers: data layer (JSON persistence), logic layer (grade processing), and presentation layer (command-line interface). This lesson builds the foundation — requirements and the data layer.
1. Project Overview
Student Grade Management System
├── Lesson 34: Data Layer — Requirements analysis + JSON/CSV persistence
├── Lesson 35: Logic Layer — Grade statistics + Sorting + Exception handling
└── Lesson 36: Presentation Layer — Menu interface + Search + Reports
2. Requirements Specification
1. Student Management
├── Add student (name, student ID, class)
└── Delete student (by student ID)
2. Grade Management
├── Enter grades (by student ID for multiple subjects)
├── Modify grades
└── Delete grade records
3. Statistics and Analysis
├── Individual report card
├── Class ranking
├── Subject averages
└── Score distribution
4. Data Persistence
├── Store data in JSON format to a file
└── Auto-load on startup
3. Data Structure Design
▶ Example: JSON Data Structure
# Data stored in JSON format
{
"students": {
"2024001": {
"name": "Zhang San",
"class_name": "Class 1",
"scores": {
"Chinese": 85,
"Math": 92,
"English": 78
}
},
"2024002": {
"name": "Li Si",
"class_name": "Class 1",
"scores": {
"Chinese": 90,
"Math": 88,
"English": 95
}
}
},
"subjects": ["Chinese", "Math", "English"],
"classes": ["Class 1", "Class 2"]
}
Output:
# Executed successfully
Tip: Why use student ID as the key? Using student ID (string) as the dictionary key makes lookups O(1) — whether there are 10 or 10,000 students, the lookup speed is the same.
4. Data Layer Implementation
▶ Example: Data Layer Implementation and Testing
import json
import os
DATA_FILE = "grade_system.json"
# ====== Default Data Structure ======
DEFAULT_DATA = {
"students": {},
"subjects": ["Chinese", "Math", "English"],
"classes": []
}
# ====== Data Layer ======
def load_data():
"""Load data from JSON file"""
if not os.path.exists(DATA_FILE):
save_data(DEFAULT_DATA.copy())
return DEFAULT_DATA.copy()
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def save_data(data):
"""Save data to JSON file"""
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# ====== Student Management ======
def add_student(student_id, name, class_name):
"""Add a student"""
data = load_data()
if student_id in data["students"]:
return False, f"Student ID {student_id} already exists!"
data["students"][student_id] = {
"name": name,
"class_name": class_name,
"scores": {}
}
# Update class list
if class_name not in data["classes"]:
data["classes"].append(class_name)
save_data(data)
return True, f"Added student: {name} ({student_id})"
def delete_student(student_id):
"""Delete a student"""
data = load_data()
if student_id not in data["students"]:
return False, f"Student ID {student_id} does not exist!"
name = data["students"][student_id]["name"]
del data["students"][student_id]
save_data(data)
return True, f"Deleted student: {name}"
def get_student(student_id):
"""Get a single student's info"""
data = load_data()
return data["students"].get(student_id)
def get_all_students():
"""Get all students"""
data = load_data()
return data["students"]
# ====== Grade Management ======
def add_score(student_id, subject, score):
"""Enter a grade"""
data = load_data()
if student_id not in data["students"]:
return False, f"Student ID {student_id} does not exist!"
if subject not in data["subjects"]:
return False, f"Subject {subject} does not exist!"
if not isinstance(score, (int, float)) or score < 0 or score > 100:
return False, "Score must be between 0 and 100!"
data["students"][student_id]["scores"][subject] = score
save_data(data)
return True, f"Entered {data['students'][student_id]['name']}'s {subject} score: {score}"
def update_score(student_id, subject, score):
"""Update a grade (same function as add_score)"""
return add_score(student_id, subject, score)
def delete_score(student_id, subject):
"""Delete a grade for a subject"""
data = load_data()
if student_id not in data["students"]:
return False, f"Student ID {student_id} does not exist!"
if subject not in data["students"][student_id]["scores"]:
return False, f"{data['students'][student_id]['name']} has no {subject} score"
del data["students"][student_id]["scores"][subject]
save_data(data)
return True, f"Deleted {data['students'][student_id]['name']}'s {subject} score"
# ====== Test Code ======
if __name__ == "__main__":
print("=== Testing Data Layer ===")
print(add_student("2024001", "Zhang San", "Class 1"))
print(add_student("2024002", "Li Si", "Class 1"))
print(add_student("2024003", "Wang Wu", "Class 2"))
print(add_score("2024001", "Chinese", 85))
print(add_score("2024001", "Math", 92))
print(add_score("2024002", "Chinese", 90))
student = get_student("2024001")
print(f"\nZhang San's info: {student}")
print(f"\nTotal students: {len(get_all_students())}")
Output:
=== Testing Data Layer ===
5. Design Highlights
▶ Example: Student Data Operations (Difficulty ⭐)
students = {}
def add_student(students, sid, name, class_name):
if sid in students:
return f"Student ID {sid} already exists!"
students[sid] = {"name": name, "class_name": class_name, "scores": {}}
return f"Added: {name} ({sid})"
def add_score(students, sid, subject, score):
if sid not in students:
return f"Student ID {sid} does not exist!"
if score < 0 or score > 100:
return "Score must be between 0 and 100!"
students[sid]["scores"][subject] = score
return f"Entered {students[sid]['name']}'s {subject}: {score}"
def get_average(students, sid):
if sid not in students or not students[sid]["scores"]:
return None
scores = students[sid]["scores"].values()
return sum(scores) / len(scores)
print(add_student(students, "001", "Alice", "Class 1"))
print(add_student(students, "002", "Bob", "Class 1"))
print(add_score(students, "001", "Chinese", 85))
print(add_score(students, "001", "Math", 92))
print(add_score(students, "002", "Chinese", 78))
avg = get_average(students, "001")
print(f"\nAlice's average: {avg:.1f}")
print(f"Bob's Chinese score: {students['002']['scores']['Chinese']}")
print(f"Total students: {len(students)}")
Output:
Added: Alice (001)
Added: Bob (002)
Entered Alice's Chinese: 85
Entered Alice's Math: 92
Entered Bob's Chinese: 78
Alice's average: 88.5
Bob's Chinese score: 78
Total students: 2
5. Design Highlights
- Uniform function return: Each function returns a
(bool, str)tuple —Truemeans success,Falsemeans failure, and the string is the message. - Encapsulated repeated operations: The
load_data()andsave_data()functions are reused across all data operations. - Input validation: Score range checking and student ID existence checks are done in the data layer, so upper-layer callers don't need to repeat them.
- Ready for expansion: Lessons 35 and 36 will add functionality on top of this data layer.
❓ FAQ
os.path.join() to construct paths and avoid hardcoding.(success, message) tuple instead of raising exceptions?success, which is lighter than try/except. It's suitable for small CLI tools; for larger projects, custom exception classes are recommended.📖 Summary
- The project uses a three-layer architecture: Data Layer -> Logic Layer -> Presentation Layer; this lesson completes the data layer
- Data is persisted in JSON format; student IDs serve as dictionary keys for O(1) lookup
- Scores range from 0 to 100, validated on entry
- All operation functions return
(success, message)tuples for easy caller handling
📝 Exercises
-
Beginner (Difficulty: Star): Run the data layer code above, add 2-3 students with their grades, and verify the JSON file is generated correctly.
-
Intermediate (Difficulty: Star-Star): Add an
update_student(student_id, name=None, class_name=None)function to the data layer that only updates the provided fields (leaving others unchanged). -
Advanced (Difficulty: Star-Star-Star): Add batch entry functionality to
add_score— accept a dictionary like{"Chinese": 85, "Math": 92}to enter multiple grades at once. Requirement: if any score is invalid, roll back the entire batch operation (don't save any data).