Python Student Management System

In the last lesson, Expense Tracker, you built an app that saves data to a file. So you can add records and store them. But real apps do more than add. They let you view, change, and remove records too. That full set of actions has a name: CRUD, which stands for Create, Read, Update, Delete. Almost every app you use is CRUD underneath. In this project we build a student management system that does all four, so you learn the pattern behind real software.

🤔 What is CRUD, and why does it matter?

Think about any app that stores things: contacts, notes, a shopping list. You can always do the same four things to the data.

  • Create: add a new record, like a new student.
  • Read: view the records, all of them or one.
  • Update: change an existing record, like fixing a mark.
  • Delete: remove a record you no longer need.

These four actions are the backbone of almost every program that manages data. Once you can build CRUD, you can build a huge range of real apps, because they are all variations on the same idea. Our student system will do all four, and it will save to a JSON file so the data stays around after you close the program.

🧱 Step 1: Design first, code later

Before we write any function, let’s think about the design. That just means deciding how we’ll organize the data. A good design makes the rest of the project easy. A messy one makes everything harder. So let’s slow down here for a minute.

We have two clear pieces in this app:

  • A single student. Each student has a name, a roll number, and marks. These three things belong together. They describe one person.
  • The whole group of students. We need to hold many students and find the right one fast.

For the single student, we’ll use a class. A class is a blueprint that bundles related data together under one name. So instead of three loose variables floating around, one Student object carries the name, the roll number, and the marks together. That’s cleaner. When you pass a student around, you pass one thing, not three.

For the whole group, we’ll use a collection. We’ll keep all the students in a dictionary where the key is the roll number and the value is the Student object. A dictionary keyed by roll number means we can jump straight to one student, like students[12], without searching. For update and delete, that direct lookup is a big help.

Here is the Student class. It’s small, but it sets up the whole project:

class Student:
def __init__(self, roll, name, marks):
self.roll = roll
self.name = name
self.marks = marks

Read it line by line:

  • class Student: starts the blueprint. Every student we make will follow this shape.
  • __init__ is the setup method. Python runs it automatically the moment you create a student. The word “init” is short for initialize, which just means “set up at the start”.
  • self is the student object being built right now. self.roll = roll saves the roll number onto that object, and the same for name and marks.

So why a class plus a dictionary? Because each one does a different job. The class keeps one student’s details tidy and together. The dictionary keeps all the students in one place and lets us find any of them by roll number instantly. Together they give us a clean, simple design that scales up without getting messy.

💾 Step 2: Saving and loading with JSON

We want the data to survive after the program closes. So we save it to a file on disk. When the program starts again, we load it back. The format we’ll use is JSON, which is a simple text format for storing data like dictionaries and lists.

There’s one small catch. JSON can store plain dictionaries and lists, but it cannot store our Student objects directly. So before saving, we turn each Student into a plain dictionary. After loading, we turn each plain dictionary back into a Student.

This shows the save and load helpers:

import json
def save(students):
plain = {}
for roll, student in students.items():
plain[roll] = {"name": student.name, "marks": student.marks}
with open("students.json", "w") as f:
json.dump(plain, f)
def load():
try:
with open("students.json", "r") as f:
plain = json.load(f)
students = {}
for roll, info in plain.items():
students[int(roll)] = Student(int(roll), info["name"], info["marks"])
return students
except FileNotFoundError:
return {}

Walk through what each part does:

  • save builds a plain dictionary first. For each student it stores just the name and marks. Then json.dump writes that plain dictionary to students.json.
  • load reads the file with json.load, which gives back the plain dictionary. Then it rebuilds a real Student for each entry and puts them in the students dictionary.
  • int(roll) matters here. JSON always stores keys as text, so a roll number comes back as "12", not 12. We convert it back to a number so our lookups stay consistent.
  • except FileNotFoundError handles the very first run. There’s no file yet, so we just start with an empty dictionary {}.

Why does this matter? Because without saving, every student you add disappears the moment you close the program. With saving, the program has a memory. That one feature is the difference between a toy and something you’d actually use.

🛠️ Step 3: The four CRUD functions, one by one

Now the heart of the app. One function for each CRUD action. Each one takes the students dictionary and works on it. Let’s go through them slowly, because each one finds the right student a slightly different way.

Add a student (Create)

Adding means creating a new Student and putting it in the dictionary under its roll number.

def add_student(students, roll, name, marks):
students[roll] = Student(roll, name, marks)
print("Added!")

Here’s the flow:

  • Student(roll, name, marks) creates a fresh student object.
  • students[roll] = ... stores it under its roll number key. So the roll number is how we’ll find it later.

View all students (Read)

Viewing means looping through the dictionary and printing each student.

def view_students(students):
if not students:
print("No students yet.")
return
for roll, student in students.items():
print(f"Roll {roll}: {student.name} - Marks {student.marks}")

Read it:

  • if not students checks for an empty dictionary first. If there’s nothing to show, we say so and stop. The return exits the function early.
  • .items() gives us both the key and the value together on each loop. So roll is the roll number and student is the Student object.
  • We reach into the object with student.name and student.marks to print the details.

Search by roll number (Read one)

Searching means finding one exact student by their roll number.

def search_student(students, roll):
if roll in students:
student = students[roll]
print(f"Roll {roll}: {student.name} - Marks {student.marks}")
else:
print("No student with that roll number.")

The flow here:

  • if roll in students asks the dictionary “do you have this key?” This is the instant lookup we designed for.
  • If yes, students[roll] grabs that one student directly. No searching through a list.
  • If no, we print a friendly message instead of crashing.

Update marks (Update)

Updating means finding the student, then changing their marks.

def update_marks(students, roll, marks):
if roll in students:
students[roll].marks = marks
print("Updated.")
else:
print("No student with that roll number.")

Read it:

  • Again we check if roll in students first. We only change something that’s really there.
  • students[roll].marks = marks reaches into that student object and sets the new marks. Notice we change just one field and leave the name alone.

Delete a student (Delete)

Deleting means finding the student, then removing them from the dictionary.

def delete_student(students, roll):
if roll in students:
del students[roll]
print("Deleted.")
else:
print("No student with that roll number.")

The flow:

  • The same existence check guards us first.
  • del students[roll] removes that key and its student from the dictionary. The whole record is gone.

See the pattern? Search, update, and delete all start the same way, with if roll in students. That one check is how each finds the right student safely. Add is the only one that doesn’t need it, because it’s putting a new record in, not touching an existing one.

🔁 Step 4: The menu loop

A program that runs once and quits isn’t very useful. We want the user to do many actions in a row. So we use a while True loop, which is a loop that keeps going until we tell it to stop with break. Each time around, we show the menu, read the user’s choice, and call the right function.

def main():
students = load()
print("Student Management System")
while True:
print("\n1) Add 2) View 3) Search 4) Update marks 5) Delete 6) Quit")
choice = input("Choose an option: ")
if choice == "1":
roll = get_roll("Roll number: ")
name = input("Name: ")
marks = get_marks("Marks: ")
if roll in students:
print("That roll number already exists.")
else:
add_student(students, roll, name, marks)
save(students)
elif choice == "2":
view_students(students)
elif choice == "3":
roll = get_roll("Roll number to search: ")
search_student(students, roll)
elif choice == "4":
roll = get_roll("Roll number to update: ")
marks = get_marks("New marks: ")
update_marks(students, roll, marks)
save(students)
elif choice == "5":
roll = get_roll("Roll number to delete: ")
delete_student(students, roll)
save(students)
elif choice == "6":
print("Goodbye!")
break
else:
print("Please pick 1 to 6.")

Let’s trace the flow:

  • students = load() runs once at the start. So the program comes up already remembering yesterday’s data.
  • while True: starts the loop. It repeats forever until something calls break.
  • We print the menu, then input("Choose an option: ") waits for the user to type a number and press Enter.
  • Then a chain of if and elif checks the choice and calls the matching function. After any change to the data, we call save(students) right away so nothing is lost.
  • For Add, we first check if roll in students to block a duplicate roll number, which we’ll talk about next.
  • Choice "6" runs break, which exits the loop and ends the program. The final else catches anything that isn’t 1 to 6, so a typo never crashes us.

Notice the menu options are checked as text, like choice == "1", not choice == 1. That’s because input() always gives back a string. We’ll come back to this idea in the validation step.

🛡️ Step 5: Input validation

Users type all kinds of things. Letters where you expected a number. A roll number that already exists. A roll number that isn’t there. If we don’t handle these, the program crashes or stores bad data. Validation means checking the input is sensible before we use it. This is one of the most important habits in real software, so let’s give it room.

There are three cases to handle here.

Case 1: a non-number where a number is expected. The roll number and marks should be numbers. But input() gives back text. If we just call int("abc"), Python raises a ValueError and the program crashes. So we wrap the conversion in try/except, which lets us catch the error and ask again instead of crashing.

def get_roll(prompt):
while True:
value = input(prompt)
try:
return int(value)
except ValueError:
print("Please enter a whole number.")
def get_marks(prompt):
while True:
value = input(prompt)
try:
marks = int(value)
except ValueError:
print("Please enter a whole number.")
continue
if 0 <= marks <= 100:
return marks
print("Marks must be between 0 and 100.")

Read what’s happening:

  • try: return int(value) tries to turn the text into a number. If it works, we return it and we’re done.
  • except ValueError: runs only when the conversion fails, like when someone types "abc". We print a message, and because we’re inside a while True loop, we ask again.
  • get_marks does the same number check, then also checks the range. Marks below 0 or above 100 don’t make sense, so we reject them. continue jumps back to the top of the loop to ask again.

Case 2: a duplicate roll number. Each student needs a unique roll number, because that’s our key. If two students had roll number 12, the second would overwrite the first. We already handle this in the menu, with if roll in students before adding. If the roll is taken, we say so and don’t add.

Case 3: a roll number that does not exist. Search, update, and delete all face this. The user might type a roll that was never added. Each of those functions already checks if roll in students first and prints a friendly message instead of raising a KeyError. So a wrong roll number is handled, not a crash.

Here’s a quick look at the program politely handling bad input:

Output

Choose an option: 1
Roll number: twelve
Please enter a whole number.
Roll number: 12
Name: Riya
Marks: 150
Marks must be between 0 and 100.
Marks: 88
Added!

See how it never crashes? It just keeps asking until the input makes sense. That’s what good validation feels like to a user.

🖥️ Step 6: The complete program

Here is the whole system in one piece, with the class, the JSON save and load, the validation helpers, the CRUD functions, and the menu loop. Save it as students.py and run it.

import json
class Student:
def __init__(self, roll, name, marks):
self.roll = roll
self.name = name
self.marks = marks
def save(students):
plain = {}
for roll, student in students.items():
plain[roll] = {"name": student.name, "marks": student.marks}
with open("students.json", "w") as f:
json.dump(plain, f)
def load():
try:
with open("students.json", "r") as f:
plain = json.load(f)
students = {}
for roll, info in plain.items():
students[int(roll)] = Student(int(roll), info["name"], info["marks"])
return students
except FileNotFoundError:
return {}
def get_roll(prompt):
while True:
value = input(prompt)
try:
return int(value)
except ValueError:
print("Please enter a whole number.")
def get_marks(prompt):
while True:
value = input(prompt)
try:
marks = int(value)
except ValueError:
print("Please enter a whole number.")
continue
if 0 <= marks <= 100:
return marks
print("Marks must be between 0 and 100.")
def add_student(students, roll, name, marks):
students[roll] = Student(roll, name, marks)
print("Added!")
def view_students(students):
if not students:
print("No students yet.")
return
for roll, student in students.items():
print(f"Roll {roll}: {student.name} - Marks {student.marks}")
def search_student(students, roll):
if roll in students:
student = students[roll]
print(f"Roll {roll}: {student.name} - Marks {student.marks}")
else:
print("No student with that roll number.")
def update_marks(students, roll, marks):
if roll in students:
students[roll].marks = marks
print("Updated.")
else:
print("No student with that roll number.")
def delete_student(students, roll):
if roll in students:
del students[roll]
print("Deleted.")
else:
print("No student with that roll number.")
def main():
students = load()
print("Student Management System")
while True:
print("\n1) Add 2) View 3) Search 4) Update marks 5) Delete 6) Quit")
choice = input("Choose an option: ")
if choice == "1":
roll = get_roll("Roll number: ")
name = input("Name: ")
marks = get_marks("Marks: ")
if roll in students:
print("That roll number already exists.")
else:
add_student(students, roll, name, marks)
save(students)
elif choice == "2":
view_students(students)
elif choice == "3":
roll = get_roll("Roll number to search: ")
search_student(students, roll)
elif choice == "4":
roll = get_roll("Roll number to update: ")
marks = get_marks("New marks: ")
update_marks(students, roll, marks)
save(students)
elif choice == "5":
roll = get_roll("Roll number to delete: ")
delete_student(students, roll)
save(students)
elif choice == "6":
print("Goodbye!")
break
else:
print("Please pick 1 to 6.")
main()

Here is a sample run showing all the actions:

Output

Student Management System
1) Add 2) View 3) Search 4) Update marks 5) Delete 6) Quit
Choose an option: 1
Roll number: 12
Name: Alex
Marks: 91
Added!
1) Add 2) View 3) Search 4) Update marks 5) Delete 6) Quit
Choose an option: 2
Roll 12: Alex - Marks 91
1) Add 2) View 3) Search 4) Update marks 5) Delete 6) Quit
Choose an option: 4
Roll number to update: 12
New marks: 95
Updated.
1) Add 2) View 3) Search 4) Update marks 5) Delete 6) Quit
Choose an option: 6
Goodbye!

You now have a complete CRUD app. It creates, reads, updates, and deletes student records, validates input, and saves everything to a file so it remembers between runs. This same skeleton powers contact apps, to-do lists, and far bigger systems.

🚀 Step 7: Extend it further

The project works, but there’s plenty of room to grow it. Here are a few ideas to try on your own.

  • Compute an average and a letter grade. Loop over the students, add up the marks, and divide by the count for a class average. For one student, turn marks into a grade with simple if checks, like 90 and up is “A”.
  • Sort the list by marks. Use sorted(students.values(), key=lambda s: s.marks, reverse=True) to show a ranked list, top scorer first.
  • Export to CSV. Open a .csv file and write one line per student, like roll,name,marks. CSV files open straight in Excel or Google Sheets, so it’s handy for sharing.

Each one is a small step that builds on the same design. Try them and the project starts feeling like real software.

🛠️ Practice Challenge

Try this, then check the answer.

Add an “average marks” option to the menu (option 7) that prints the average marks of all students. Handle the empty case, where there are no students yet.

⚠️ Common Mistakes

A few traps in this project:

  • Not checking if the roll number exists before searching, updating, or deleting. Without the if roll in students guard, a wrong roll number crashes the program with a KeyError.
  • Forgetting that JSON keys come back as text. After json.load, a roll number is "12", not 12. If you don’t convert with int(roll), your lookups quietly fail to match.
  • Not wrapping int(input(...)) in try/except. The moment a user types a letter, int("abc") raises a ValueError and the whole program stops.
  • Forgetting to save after a change. Update and delete must call save too, not just add. Otherwise the change is lost on close.
  • Allowing duplicate roll numbers. Adding a roll that already exists overwrites the first student silently. The if roll in students check before adding stops that.

✅ Best Practices

Habits this project teaches:

  • Bundle one record’s data in a class, so a Student carries its name, roll, and marks together as one tidy object.
  • Store records in a dictionary keyed by a unique ID, so lookup, update, and delete are instant.
  • Validate every input with try/except and range checks before you use it, so bad data never gets in and the program never crashes on a typo.
  • Guard search, update, and delete with an existence check, so a bad roll number gives a friendly message, not a KeyError.
  • Save after every change that alters the data, so nothing is lost.
  • Keep one function per CRUD action. Clear, single-purpose functions make the program easy to extend.

🧩 What You’ve Learned

✅ CRUD means Create, Read, Update, Delete, the four actions behind almost every data app.

✅ A Student class bundles one student’s name, roll number, and marks together, and a dictionary keyed by roll number holds them all for instant lookup.

✅ Search, update, and delete all check if roll in students first, to avoid a KeyError on a missing roll number.

try/except ValueError lets you catch bad input, like letters where a number is expected, and ask again instead of crashing.

✅ Saving to JSON after every change gives the whole system a reliable memory, but remember JSON keys come back as text, so convert them with int.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does CRUD stand for?

    Why: CRUD is Create, Read, Update, Delete, the four core actions for managing data that nearly every app performs.

  2. 2

    Why store students in a dictionary keyed by roll number instead of a list?

    Why: A dictionary keyed by roll number lets you reach one record directly, like students[12]. With a list you would have to search through it.

  3. 3

    Why wrap int(input(...)) in try/except ValueError?

    Why: If the user types letters, int() raises a ValueError. The try/except catches it so we can ask again instead of crashing.

  4. 4

    After json.load, what type are the dictionary keys, and why does it matter?

    Why: JSON stores keys as text, so a roll number returns as a string like "12". Converting with int keeps lookups consistent.

🚀 What’s Next?

You can now build a full CRUD app with a class, validation, and saved data, which is the backbone of real software. Next we connect to the wider world: a weather application that fetches live data from the internet using an API.

Weather Application

Share & Connect