The Story: A Weekend Lost to Marksheets
Mrs. Sunita Deshmukh is the class teacher of Class 10 B in a government high school in Nashik. After every term exam, the subject teachers give her the marks of all 40 students. Then her weekend disappears. With a calculator and a register, she adds five subjects for each student, works out the percentage, writes the grade, finds the topper of the class and of each subject, decides the ranks, and counts how many students passed. One wrong addition means rewriting a whole page.
The marks already arrive in a spreadsheet. A spreadsheet can be saved as a CSV file (comma separated values), and Python reads CSV files easily. In this project you will build Mrs. Deshmukh a Marks Report Generator: a program that reads the marks file and, in less than a second, writes a neat text report and a results file she can open in a spreadsheet.
You will use skills you already know: files, the csv module, functions, comprehensions and classes. You will also meet two useful tools from the standard library: dataclasses and statistics.
How to follow this project: every step below is a complete program. Press ▶ Try it yourself on each one. Each step adds one feature to the step before it, so you can always see exactly what changed.
The Plan
| Step | Feature | Main skill |
|---|---|---|
| 1 | Read the marks file | csv.DictReader |
| 2 | Total and percentage for each student | @dataclass, @property |
| 3 | Grades and pass or fail | if chains, all() |
| 4 | Ranks (with ties) and the class topper | sorted() with a key |
| 5 | Subject averages and subject toppers | statistics.mean |
| 6 | A neat text report | f-string alignment |
| 7 | A results CSV and the complete program | csv.DictWriter, main() |
The sample file has 8 students instead of 40, so the output fits on your screen. The program works the same way for 40 or 400.
Step 1: Read the Marks File
Each run of the playground starts in an empty folder, so the program first writes a small marks.csv. On Mrs. Deshmukh’s computer this file already exists, and she would delete those lines.
import csv
from pathlib import Path
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
with open("marks.csv", newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
print("Subjects:", subjects)
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
print(row["roll"], row["name"], marks)
SAMPLE_CSVis a multi-line string. The first line is the header: the column names. Each later line is one student.Path("marks.csv").write_text(...)creates the file in one line. Always passencoding="utf-8"so names with any letters are saved correctly.open(..., newline="")is what thecsvmodule asks for. It lets the module handle line endings itself, which matters on Windows.csv.DictReader(file)reads each line as a dictionary whose keys come from the header, such asrow["name"].reader.fieldnames[2:]takes the header names afterrollandname. These are the subjects. Because we read them from the file, adding a sixth subject needs no code change.- The dictionary comprehension builds
{"English": 78, ...}. We callint()because the CSV module always gives text, never numbers.
Step 2: Total and Percentage for Each Student
A loose dictionary for each student would work, but a small class keeps the data and the calculations together. Python’s @dataclass decorator writes the boring parts of a class for us.
import csv
from dataclasses import dataclass
from pathlib import Path
MAX_MARKS = 100 # maximum marks in each subject
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
@dataclass
class Student:
roll: int
name: str
marks: dict
@property
def total(self):
return sum(self.marks.values())
@property
def percentage(self):
return round(self.total / (len(self.marks) * MAX_MARKS) * 100, 1)
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
subjects, students = load_students("marks.csv")
for student in students:
print(f"{student.name:<16}{student.total:>5}{student.percentage:>7}%")
@dataclassreads the three linesroll: int,name: strandmarks: dictand creates the__init__method for you. SoStudent(1, "Aarav Shinde", marks)just works. It also adds a readableprint()output.@propertyturns a method into something you read like a value:student.total, without brackets. The total is calculated from the marks every time, so it can never be out of date.percentagedivides the total by the maximum possible marks (5 subjects × 100) and rounds to one decimal place.load_students()is Step 1 moved into a function. It returns two values, the subject list and the student list, and we unpack them withsubjects, students = ....- In
{student.name:<16}, the part after the colon is a format spec.<16means “left-align in 16 characters” and>5means “right-align in 5”. This lines up the columns.
Step 3: Grades and Pass or Fail
The school gives a grade from the percentage, and a student passes only if they score at least 33 in every subject. A high total does not hide a failed subject.
import csv
from dataclasses import dataclass
from pathlib import Path
MAX_MARKS = 100 # maximum marks in each subject
PASS_MARK = 33 # minimum marks to pass a subject
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
@property
def total(self):
return sum(self.marks.values())
@property
def percentage(self):
return round(self.total / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
@property
def passed(self):
return all(mark >= PASS_MARK for mark in self.marks.values())
@property
def result(self):
return "Pass" if self.passed else "Fail"
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
subjects, students = load_students("marks.csv")
for student in students:
print(f"{student.name:<16}{student.percentage:>6}% {student.grade:<4}{student.result}")
passed = [student for student in students if student.passed]
print(f"Passed: {len(passed)} Failed: {len(students) - len(passed)}")
PASS_MARK = 33is a constant: a name in capital letters for a value that does not change. If the rule changes, you edit one line.grade_for()checks from the highest grade down. Eachreturnends the function, so a student with 85% gets"A"and never reaches the later checks. Change the cut-offs to match your own school.all(mark >= PASS_MARK for mark in ...)isTrueonly if every mark passes. Chetan scored 52 in Hindi but only 30 in Maths, so he fails.resultuses a one-lineif(a conditional expression) to give the words"Pass"or"Fail".- The list comprehension collects the students who passed, and
len()counts them.
Step 4: Ranks, Ties and the Class Topper
Divya and Gauri both have 429 marks. It would be unfair to call one of them second and the other third. The usual fair method is: equal totals share a rank, and the next rank is skipped. So the ranks go 1, 2, 2, 4.
import csv
from dataclasses import dataclass
from pathlib import Path
MAX_MARKS = 100 # maximum marks in each subject
PASS_MARK = 33 # minimum marks to pass a subject
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
rank: int = 0
@property
def total(self):
return sum(self.marks.values())
@property
def percentage(self):
return round(self.total / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
@property
def passed(self):
return all(mark >= PASS_MARK for mark in self.marks.values())
@property
def result(self):
return "Pass" if self.passed else "Fail"
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
def assign_ranks(students):
"""Sort by total, highest first. Equal totals get the same rank."""
ordered = sorted(students, key=lambda s: s.total, reverse=True)
for position, student in enumerate(ordered, start=1):
previous = ordered[position - 2] if position > 1 else None
if previous and student.total == previous.total:
student.rank = previous.rank
else:
student.rank = position
return ordered
subjects, students = load_students("marks.csv")
ranked = assign_ranks(students)
for student in ranked:
print(f"{student.rank:>2}. {student.name:<16}{student.total:>5} {student.grade}")
toppers = [student for student in ranked if student.rank == 1]
for topper in toppers:
print(f"Class topper: {topper.name} with {topper.total} marks ({topper.percentage}%)")
rank: int = 0adds a new field with a default value, so we can still create aStudentwith three values and set the rank later.sorted(students, key=lambda s: s.total, reverse=True)returns a new list ordered by total, highest first. Thelambdais a tiny function that tellssorted()what to compare.enumerate(ordered, start=1)gives each student a position: 1, 2, 3 and so on.ordered[position - 2]is the student just above. Positions start at 1 but list indexes start at 0, so the student above position 3 is at index 1.- If the totals match, the student copies the rank of the student above. Otherwise the rank is the position. That is how Aarav becomes 4th, not 3rd.
- There can be more than one topper, so we collect every student with rank 1 in a list.
Step 5: Subject Averages and Subject Toppers
The headmaster always asks two questions: “Which subject is weak in this class?” and “Who topped Maths?”. The statistics module answers the first one.
import csv
from dataclasses import dataclass
from pathlib import Path
from statistics import mean
MAX_MARKS = 100 # maximum marks in each subject
PASS_MARK = 33 # minimum marks to pass a subject
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
rank: int = 0
@property
def total(self):
return sum(self.marks.values())
@property
def percentage(self):
return round(self.total / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
@property
def passed(self):
return all(mark >= PASS_MARK for mark in self.marks.values())
@property
def result(self):
return "Pass" if self.passed else "Fail"
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
def assign_ranks(students):
"""Sort by total, highest first. Equal totals get the same rank."""
ordered = sorted(students, key=lambda s: s.total, reverse=True)
for position, student in enumerate(ordered, start=1):
previous = ordered[position - 2] if position > 1 else None
if previous and student.total == previous.total:
student.rank = previous.rank
else:
student.rank = position
return ordered
def subject_summary(subjects, students):
"""Return (subject, average, highest, topper names) for every subject."""
rows = []
for subject in subjects:
scores = [student.marks[subject] for student in students]
highest = max(scores)
toppers = [s.name for s in students if s.marks[subject] == highest]
rows.append((subject, mean(scores), highest, toppers))
return rows
subjects, students = load_students("marks.csv")
ranked = assign_ranks(students)
print(f"{'Subject':<10}{'Average':>8}{'Highest':>9} Topper(s)")
for subject, average, highest, toppers in subject_summary(subjects, ranked):
print(f"{subject:<10}{average:>8.1f}{highest:>9} {', '.join(toppers)}")
from statistics import meanbrings in a function that returns the average of a list. You could writesum(scores) / len(scores), butmeansays what you mean. The same module hasmedianandstdevtoo.scoresis a list of every student’s marks in one subject, andmax(scores)is the highest.- Two students can share the highest marks. Bhavna and Farhan both scored 90 in Social, so
toppersis a list of names, not one name. - The function returns a list of tuples. The loop unpacks each tuple into four names in one line.
:>8.1fmeans right-align in 8 characters with one decimal place.', '.join(toppers)turns the list of names into one string.
The output shows at once that Maths has the lowest average in this class, so it may need extra classes before the next exam.
Step 6: Write a Neat Text Report
Now we collect everything into one report that Mrs. Deshmukh can print or send to the headmaster. Building a list of lines and joining them at the end is cleaner than many separate print() calls, and it lets us save the same text to a file.
import csv
from dataclasses import dataclass
from pathlib import Path
from statistics import mean
MAX_MARKS = 100 # maximum marks in each subject
PASS_MARK = 33 # minimum marks to pass a subject
SCHOOL = "Government High School, Nashik"
CLASS_NAME = "Class 10 B | Term 1 Result"
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
rank: int = 0
@property
def total(self):
return sum(self.marks.values())
@property
def percentage(self):
return round(self.total / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
@property
def passed(self):
return all(mark >= PASS_MARK for mark in self.marks.values())
@property
def result(self):
return "Pass" if self.passed else "Fail"
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
def assign_ranks(students):
"""Sort by total, highest first. Equal totals get the same rank."""
ordered = sorted(students, key=lambda s: s.total, reverse=True)
for position, student in enumerate(ordered, start=1):
previous = ordered[position - 2] if position > 1 else None
if previous and student.total == previous.total:
student.rank = previous.rank
else:
student.rank = position
return ordered
def subject_summary(subjects, students):
"""Return (subject, average, highest, topper names) for every subject."""
rows = []
for subject in subjects:
scores = [student.marks[subject] for student in students]
highest = max(scores)
toppers = [s.name for s in students if s.marks[subject] == highest]
rows.append((subject, mean(scores), highest, toppers))
return rows
def build_report(subjects, ranked):
"""Return the whole report as one string."""
width = 62
lines = [SCHOOL, CLASS_NAME, "=" * width, ""]
lines.append(f"{'Rank':<6}{'Roll':<6}{'Name':<17}{'Total':>6}{'%':>7} {'Grade':<7}Result")
lines.append("-" * width)
for s in ranked:
lines.append(f"{s.rank:<6}{s.roll:<6}{s.name:<17}{s.total:>6}{s.percentage:>7.1f} {s.grade:<7}{s.result}")
passed = sum(1 for s in ranked if s.passed)
lines += ["", "CLASS SUMMARY", "-" * width]
lines.append(f"Students: {len(ranked)} Passed: {passed} Failed: {len(ranked) - passed}")
lines.append(f"Pass percentage: {passed / len(ranked) * 100:.1f}%")
for s in ranked:
if s.rank == 1:
lines.append(f"Class topper: {s.name} ({s.total} marks, {s.percentage}%)")
lines += ["", f"{'Subject':<10}{'Average':>8}{'Highest':>9} Topper(s)", "-" * width]
for subject, average, highest, toppers in subject_summary(subjects, ranked):
lines.append(f"{subject:<10}{average:>8.1f}{highest:>9} {', '.join(toppers)}")
return "\n".join(lines)
subjects, students = load_students("marks.csv")
ranked = assign_ranks(students)
report = build_report(subjects, ranked)
Path("report.txt").write_text(report, encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"))
SCHOOLandCLASS_NAMEare constants at the top, so the heading changes in one place each term."=" * widthrepeats a character to draw a line exactly as wide as the table.lines.append()adds one line, andlines += [...]adds several lines at once.sum(1 for s in ranked if s.passed)counts the students who passed without building a list."\n".join(lines)joins all the lines with a new line character between them.- We write the report to
report.txtand then read it back, to prove the file really contains it.
Step 7: The Complete Program with a Results CSV
The office also wants the results as a spreadsheet, sorted by rank. csv.DictWriter writes dictionaries as rows. This is the complete program. On your computer, delete the sample data lines, put the real marks.csv next to the program and run it.
import csv
from dataclasses import dataclass
from pathlib import Path
from statistics import mean
MAX_MARKS = 100 # maximum marks in each subject
PASS_MARK = 33 # minimum marks to pass a subject
SCHOOL = "Government High School, Nashik"
CLASS_NAME = "Class 10 B | Term 1 Result"
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
rank: int = 0
@property
def total(self):
return sum(self.marks.values())
@property
def percentage(self):
return round(self.total / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
@property
def passed(self):
return all(mark >= PASS_MARK for mark in self.marks.values())
@property
def result(self):
return "Pass" if self.passed else "Fail"
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
def assign_ranks(students):
"""Sort by total, highest first. Equal totals get the same rank."""
ordered = sorted(students, key=lambda s: s.total, reverse=True)
for position, student in enumerate(ordered, start=1):
previous = ordered[position - 2] if position > 1 else None
if previous and student.total == previous.total:
student.rank = previous.rank
else:
student.rank = position
return ordered
def subject_summary(subjects, students):
"""Return (subject, average, highest, topper names) for every subject."""
rows = []
for subject in subjects:
scores = [student.marks[subject] for student in students]
highest = max(scores)
toppers = [s.name for s in students if s.marks[subject] == highest]
rows.append((subject, mean(scores), highest, toppers))
return rows
def build_report(subjects, ranked):
"""Return the whole report as one string."""
width = 62
lines = [SCHOOL, CLASS_NAME, "=" * width, ""]
lines.append(f"{'Rank':<6}{'Roll':<6}{'Name':<17}{'Total':>6}{'%':>7} {'Grade':<7}Result")
lines.append("-" * width)
for s in ranked:
lines.append(f"{s.rank:<6}{s.roll:<6}{s.name:<17}{s.total:>6}{s.percentage:>7.1f} {s.grade:<7}{s.result}")
passed = sum(1 for s in ranked if s.passed)
lines += ["", "CLASS SUMMARY", "-" * width]
lines.append(f"Students: {len(ranked)} Passed: {passed} Failed: {len(ranked) - passed}")
lines.append(f"Pass percentage: {passed / len(ranked) * 100:.1f}%")
for s in ranked:
if s.rank == 1:
lines.append(f"Class topper: {s.name} ({s.total} marks, {s.percentage}%)")
lines += ["", f"{'Subject':<10}{'Average':>8}{'Highest':>9} Topper(s)", "-" * width]
for subject, average, highest, toppers in subject_summary(subjects, ranked):
lines.append(f"{subject:<10}{average:>8.1f}{highest:>9} {', '.join(toppers)}")
return "\n".join(lines)
def write_results_csv(path, subjects, ranked):
"""Save one row per student, in rank order, for a spreadsheet."""
fieldnames = ["rank", "roll", "name", *subjects, "total", "percentage", "grade", "result"]
with open(path, "w", newline="", encoding="utf-8") as file:
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
for s in ranked:
writer.writerow({
"rank": s.rank, "roll": s.roll, "name": s.name, **s.marks,
"total": s.total, "percentage": s.percentage,
"grade": s.grade, "result": s.result,
})
def main():
subjects, students = load_students("marks.csv")
ranked = assign_ranks(students)
Path("report.txt").write_text(build_report(subjects, ranked), encoding="utf-8")
write_results_csv("results.csv", subjects, ranked)
print(Path("report.txt").read_text(encoding="utf-8"))
print()
print("results.csv:")
print(Path("results.csv").read_text(encoding="utf-8"))
if __name__ == "__main__":
main()
fieldnamesis the list of columns in order.*subjectsunpacks the subject names into the middle of the list.open(path, "w", newline="")opens the file for writing. Againnewline=""stops blank lines between rows on Windows.writer.writeheader()writes the column names, andwriter.writerow()writes one student.**s.marksunpacks the marks dictionary into the row, so every subject becomes its own column.main()holds the steps of the program in order, andif __name__ == "__main__":runs it only when you run this file directly, not when another file imports it. This is the standard shape of a real Python script.
Run it with python marks_report.py on Windows or python3 marks_report.py on Mac and Linux. Mrs. Deshmukh’s weekend of calculations is now one command.
Ideas to Take It Further
- Absent students: accept
ABin the marks file (see Common Mistakes below) and show “Absent” in the report. - Different maximum marks: read the maximum for each subject from a second header line, for subjects with 80 marks of theory.
- Rank only passed students: many schools give ranks only to students who pass every subject. Filter the list before calling
assign_ranks(). - One file per student: write a small report card text file for each student, named by roll number.
- Compare terms: read Term 1 and Term 2 files and show who improved the most.
Common Mistakes
- Forgetting that CSV values are text.
row["Maths"]is"96", not96. Adding text joins it:"78" + "85"is"7885". Always convert withint()when you read the file. - A blank cell or “AB” crashes the program. Real marks files have absent students.
int("AB")raises aValueError:
row = {"name": "Kiran Bhosale", "Maths": "AB"} # AB means absent
marks = int(row["Maths"]) # ERROR: ValueError
Fix: convert each cell with a small function that handles these cases first:
def to_mark(text):
"""Turn a cell into a number. Blank or AB (absent) counts as 0."""
text = text.strip().upper()
if text in ("", "AB"):
return 0
return int(text)
print(to_mark(" 78 ")) # 78
print(to_mark("AB")) # 0
print(to_mark("")) # 0
- Giving tied students different ranks. Using the position from
enumerate()alone ranks students with equal totals one after the other:
totals = {"Divya": 429, "Gauri": 429, "Aarav": 421}
ordered = sorted(totals, key=totals.get, reverse=True)
for position, name in enumerate(ordered, start=1):
print(position, name)
# 1 Divya
# 2 Gauri <- unfair: Gauri has the same total as Divya
# 3 Aarav
Fix: compare each total with the one above it, as assign_ranks() does in Step 4.
- Deciding pass or fail from the percentage only. A student with 60% overall but 25 in Science has still failed Science. Check every subject with
all(). - Rounding too early. Round only for display or at the last step. If you round each subject average before adding them, small errors add up.
Practice Task: Show the Grade Distribution
The headmaster wants to see how many students got each grade, as a quick bar chart made of stars:
- Complete
grade_counts()so it returns a dictionary with all six grades (A+,A,B,C,D,E), each with a count. Grades nobody got must show 0. - Print one line per grade: the grade, a bar of
*characters (one per student) and the count. - Hint: start with a dictionary comprehension that sets every grade to 0.
import csv
from dataclasses import dataclass
from pathlib import Path
MAX_MARKS = 100
PASS_MARK = 33
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
@property
def percentage(self):
return round(sum(self.marks.values()) / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
def grade_counts(students):
"""Return a dict like {"A+": 1, "A": 3, ...} with every grade, even if 0."""
# TODO 1: start a dict with all six grades set to 0
# TODO 2: add 1 for each student's grade
return {}
subjects, students = load_students("marks.csv")
# TODO 3: print one line per grade with a bar of * characters
print(grade_counts(students))
Show one possible answer (try it yourself first!)
import csv
from dataclasses import dataclass
from pathlib import Path
MAX_MARKS = 100
PASS_MARK = 33
# Sample data: on your computer you would already have this file
SAMPLE_CSV = """roll,name,English,Hindi,Maths,Science,Social
1,Aarav Shinde,78,85,96,88,74
2,Bhavna Kale,91,88,93,90,90
3,Chetan More,45,52,30,41,48
4,Divya Pawar,82,79,88,95,85
5,Farhan Shaikh,67,72,58,64,90
6,Gauri Jadhav,85,90,84,86,84
7,Imran Khan,55,61,47,52,58
8,Jyoti Salunkhe,38,44,35,29,40
"""
Path("marks.csv").write_text(SAMPLE_CSV, encoding="utf-8")
def grade_for(percentage):
"""Turn a percentage into a grade."""
if percentage >= 90:
return "A+"
if percentage >= 75:
return "A"
if percentage >= 60:
return "B"
if percentage >= 45:
return "C"
if percentage >= PASS_MARK:
return "D"
return "E"
@dataclass
class Student:
roll: int
name: str
marks: dict
@property
def percentage(self):
return round(sum(self.marks.values()) / (len(self.marks) * MAX_MARKS) * 100, 1)
@property
def grade(self):
return grade_for(self.percentage)
def load_students(path):
"""Read the marks file and return (subjects, students)."""
students = []
with open(path, newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
subjects = reader.fieldnames[2:]
for row in reader:
marks = {subject: int(row[subject]) for subject in subjects}
students.append(Student(int(row["roll"]), row["name"], marks))
return subjects, students
def grade_counts(students):
"""Return a dict like {"A+": 1, "A": 3, ...} with every grade, even if 0."""
counts = {grade: 0 for grade in ["A+", "A", "B", "C", "D", "E"]}
for student in students:
counts[student.grade] += 1
return counts
subjects, students = load_students("marks.csv")
print("Grade distribution")
for grade, count in grade_counts(students).items():
print(f"{grade:<3}{'*' * count:<10}{count}")
Key Takeaways
csv.DictReaderreads each row as a dictionary, and every value is text, so convert marks withint().- A
@dataclasswith@propertymethods keeps each student’s data and calculations together, and the totals are never out of date. - Sort with
sorted(..., key=..., reverse=True), then compare each total with the one above to give tied students the same rank. statistics.meangives averages, and a list (not a single name) handles shared toppers.- Build reports as a list of lines with aligned f-strings, then write them with
write_text()andcsv.DictWriter.