Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 25 min read 👁 5 views 📅 Oct 22, 2026

Project: Build a Student Marks Report Generator

Build a Python program that reads a class marks CSV and calculates totals, percentages, grades, ranks with ties, pass and fail counts, subject averages and toppers, then writes a neat text report and a results CSV.

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

StepFeatureMain skill
1Read the marks filecsv.DictReader
2Total and percentage for each student@dataclass, @property
3Grades and pass or failif chains, all()
4Ranks (with ties) and the class toppersorted() with a key
5Subject averages and subject toppersstatistics.mean
6A neat text reportf-string alignment
7A results CSV and the complete programcsv.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_CSV is 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 pass encoding="utf-8" so names with any letters are saved correctly.
  • open(..., newline="") is what the csv module 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 as row["name"].
  • reader.fieldnames[2:] takes the header names after roll and name. 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 call int() 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}%")
  • @dataclass reads the three lines roll: int, name: str and marks: dict and creates the __init__ method for you. So Student(1, "Aarav Shinde", marks) just works. It also adds a readable print() output.
  • @property turns 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.
  • percentage divides 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 with subjects, students = ....
  • In {student.name:<16}, the part after the colon is a format spec. <16 means “left-align in 16 characters” and >5 means “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 = 33 is 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. Each return ends 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 ...) is True only if every mark passes. Chetan scored 52 in Hindi but only 30 in Maths, so he fails.
  • result uses a one-line if (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 = 0 adds a new field with a default value, so we can still create a Student with 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. The lambda is a tiny function that tells sorted() 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 mean brings in a function that returns the average of a list. You could write sum(scores) / len(scores), but mean says what you mean. The same module has median and stdev too.
  • scores is a list of every student’s marks in one subject, and max(scores) is the highest.
  • Two students can share the highest marks. Bhavna and Farhan both scored 90 in Social, so toppers is 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.1f means 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"))
  • SCHOOL and CLASS_NAME are constants at the top, so the heading changes in one place each term.
  • "=" * width repeats a character to draw a line exactly as wide as the table.
  • lines.append() adds one line, and lines += [...] 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.txt and 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()
  • fieldnames is the list of columns in order. *subjects unpacks the subject names into the middle of the list.
  • open(path, "w", newline="") opens the file for writing. Again newline="" stops blank lines between rows on Windows.
  • writer.writeheader() writes the column names, and writer.writerow() writes one student.
  • **s.marks unpacks the marks dictionary into the row, so every subject becomes its own column.
  • main() holds the steps of the program in order, and if __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 AB in 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", not 96. Adding text joins it: "78" + "85" is "7885". Always convert with int() when you read the file.
  • A blank cell or “AB” crashes the program. Real marks files have absent students. int("AB") raises a ValueError:
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.DictReader reads each row as a dictionary, and every value is text, so convert marks with int().
  • A @dataclass with @property methods 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.mean gives 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() and csv.DictWriter.

❓ Frequently Asked Questions

Not with the standard library alone. In Excel or any spreadsheet app, use Save As and choose CSV, then run the program on that file. The openpyxl package can read .xlsx files directly if you install it with pip.
Both work. A dataclass gives each student fixed fields with clear names, and its properties calculate the total, percentage and grade in one place, so the rest of the code stays short.
Edit the numbers in grade_for(). Keep the checks in order from the highest grade to the lowest, because the first matching return wins.
Yes. Nothing in the program depends on the number of students or subjects, because both come from the file. It will finish in well under a second.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋