import gradio as gr import io import json import random import contextlib import traceback from datetime import datetime APP_TITLE = "PCEP Python Learning Academy" PASS_MARK = 70 LESSONS = { "1. Python Fundamentals": { "block": "Computer Programming and Python Fundamentals", "weight": "18%", "content": """ ### Learning goals - Understand what programming is. - Understand interpreter, syntax, comments, indentation, variables, and literals. - Write your first Python statements. ### Key explanation Python is an interpreted programming language. You write instructions, and the Python interpreter runs them line by line. Python uses indentation to show code structure. This is important for `if`, `for`, `while`, and functions. ### Example ```python # This is a comment print("Hello, Python!") age = 20 print(age) ``` ### Common exam points - Python is case-sensitive. - Comments start with `#`. - Indentation is part of Python syntax. - Variable names should be meaningful and follow Python naming rules. """, "starter_code": "print('Hello, PCEP learner!')\nname = 'Student'\nprint('Welcome', name)", }, "2. Variables and Data Types": { "block": "Computer Programming and Python Fundamentals", "weight": "18%", "content": """ ### Learning goals - Use variables to store values. - Recognize integers, floats, strings, and Booleans. - Convert values using `int()`, `float()`, and `str()`. ### Example ```python age = 25 # int price = 9.99 # float name = "Ali" # str passed = True # bool print(type(age)) print(int("10") + 5) ``` ### Common exam points - `10` is an integer. - `10.0` is a float. - `"10"` is a string. - `True` and `False` are Boolean values. """, "starter_code": "x = '10'\ny = 5\nprint(int(x) + y)", }, "3. Operators": { "block": "Computer Programming and Python Fundamentals", "weight": "18%", "content": """ ### Learning goals - Use arithmetic, comparison, logical, assignment, and string operators. - Understand operator priority. ### Example ```python print(2 + 3) print(2 ** 3) print(7 // 2) print(7 % 2) print(5 > 3 and 2 < 4) print("Py" + "thon") ``` ### Common exam points - `**` means exponentiation. - `//` means floor division. - `%` gives the remainder. - `and`, `or`, and `not` are Boolean operators. """, "starter_code": "a = 7\nb = 2\nprint(a // b)\nprint(a % b)", }, "4. Input and Output": { "block": "Computer Programming and Python Fundamentals", "weight": "18%", "content": """ ### Learning goals - Use `print()` for output. - Use `input()` for user input. - Convert input values when needed. ### Example ```python name = input("Enter your name: ") age = int(input("Enter your age: ")) print("Hello", name) print("Next year you will be", age + 1) ``` ### Common exam points - `input()` always returns a string. - Use `int()` or `float()` before doing numeric calculation. - `print()` supports `sep=` and `end=`. """, "starter_code": "print('Python', 'PCEP', sep=' - ')\nprint('Hello', end=' ')\nprint('World')", }, "5. Conditional Statements": { "block": "Control Flow – Conditional Blocks and Loops", "weight": "29%", "content": """ ### Learning goals - Use `if`, `elif`, and `else`. - Build Boolean expressions. - Understand nested conditions. ### Example ```python score = 75 if score >= 70: print("Pass") elif score >= 50: print("Borderline") else: print("Fail") ``` ### Common exam points - Conditions must evaluate to `True` or `False`. - `elif` means else-if. - Indentation is required inside each block. """, "starter_code": "score = 75\nif score >= 70:\n print('Pass')\nelse:\n print('Try again')", }, "6. Loops": { "block": "Control Flow – Conditional Blocks and Loops", "weight": "29%", "content": """ ### Learning goals - Use `while` loops and `for` loops. - Use `range()`. - Understand `break` and `continue`. ### Example ```python for i in range(5): print(i) count = 0 while count < 3: print(count) count += 1 ``` ### Common exam points - `range(5)` produces 0, 1, 2, 3, 4. - `break` exits a loop. - `continue` skips to the next iteration. """, "starter_code": "for i in range(1, 6):\n print(i)", }, "7. Lists, Tuples, Dictionaries, and Strings": { "block": "Data Collections – Tuples, Dictionaries, Lists, and Strings", "weight": "25%", "content": """ ### Learning goals - Use lists, tuples, dictionaries, and strings. - Use indexing and slicing. - Understand mutability. ### Example ```python items = ["pen", "book", "bag"] print(items[0]) items.append("pencil") person = {"name": "Ali", "age": 20} print(person["name"]) word = "Python" print(word[0]) print(word[1:4]) ``` ### Common exam points - Lists are mutable. - Tuples are immutable. - Dictionary values are accessed using keys. - Strings are immutable. """, "starter_code": "numbers = [3, 1, 2]\nnumbers.append(4)\nprint(sorted(numbers))\nprint(numbers[0:2])", }, "8. Functions": { "block": "Functions and Exceptions", "weight": "28%", "content": """ ### Learning goals - Define and call functions. - Use parameters and return values. - Understand `None`. ### Example ```python def add(a, b): return a + b result = add(3, 4) print(result) ``` ### Common exam points - A function is defined with `def`. - `return` sends a value back to the caller. - A function without `return` returns `None`. """, "starter_code": "def greet(name):\n return 'Hello ' + name\n\nprint(greet('PCEP'))", }, "9. Exceptions": { "block": "Functions and Exceptions", "weight": "28%", "content": """ ### Learning goals - Understand runtime errors. - Use `try` and `except`. - Handle common exceptions safely. ### Example ```python try: number = int("abc") except ValueError: print("Invalid number") ``` ### Common exam points - `ValueError` happens when a value has the wrong format. - `ZeroDivisionError` happens when dividing by zero. - Exceptions prevent the whole program from crashing if handled properly. """, "starter_code": "try:\n print(10 / 0)\nexcept ZeroDivisionError:\n print('Cannot divide by zero')", }, "10. Mock Exam": { "block": "All PCEP blocks", "weight": "100%", "content": """ ### Learning goals - Review all major PCEP topics. - Practise exam-style thinking. - Identify weak areas before the actual exam. Use the **Mock Exam** tab to attempt a 30-question quiz. """, "starter_code": "print('Ready for the mock exam!')", }, } QUESTION_BANK = [ {"topic":"Fundamentals","q":"Which symbol starts a single-line comment in Python?","choices":["//","#","--","/*"],"answer":"#","explain":"Python single-line comments start with #."}, {"topic":"Fundamentals","q":"Python uses indentation mainly to show what?","choices":["Code block structure","File size","Program speed","Package version"],"answer":"Code block structure","explain":"Indentation defines blocks such as if, loops, and functions."}, {"topic":"Variables","q":"What is the type of 3.14?","choices":["int","float","str","bool"],"answer":"float","explain":"Numbers with decimal points are usually floats."}, {"topic":"Variables","q":"What does input() return?","choices":["int","float","str","bool"],"answer":"str","explain":"input() always returns a string unless you convert it."}, {"topic":"Operators","q":"What is the result of 7 // 2?","choices":["3","3.5","1","4"],"answer":"3","explain":"// performs floor division."}, {"topic":"Operators","q":"What is the result of 2 ** 3?","choices":["5","6","8","9"],"answer":"8","explain":"** means exponentiation."}, {"topic":"Operators","q":"Which operator gives the remainder?","choices":["/","//","%","**"],"answer":"%","explain":"% is the modulo operator."}, {"topic":"Input/Output","q":"Which function displays output?","choices":["show()","print()","display()","output()"],"answer":"print()","explain":"print() displays output to the console."}, {"topic":"Conditionals","q":"Which keyword checks another condition after if fails?","choices":["elseif","elif","else if","then"],"answer":"elif","explain":"Python uses elif for else-if logic."}, {"topic":"Conditionals","q":"What is the result of 5 > 3?","choices":["True","False","5","3"],"answer":"True","explain":"5 is greater than 3."}, {"topic":"Loops","q":"What values are produced by range(3)?","choices":["1,2,3","0,1,2","0,1,2,3","3 only"],"answer":"0,1,2","explain":"range(3) starts at 0 and stops before 3."}, {"topic":"Loops","q":"Which keyword exits a loop immediately?","choices":["stop","exit","break","continue"],"answer":"break","explain":"break exits the nearest loop."}, {"topic":"Loops","q":"Which keyword skips the current loop iteration?","choices":["skip","continue","break","pass"],"answer":"continue","explain":"continue jumps to the next iteration."}, {"topic":"Lists","q":"Which method adds an item to the end of a list?","choices":["add()","append()","push()","insert_end()"],"answer":"append()","explain":"append() adds an item to the end of a list."}, {"topic":"Lists","q":"What is numbers[0] for numbers = [10, 20, 30]?","choices":["10","20","30","Error"],"answer":"10","explain":"Python indexing starts at 0."}, {"topic":"Tuples","q":"Which collection is immutable?","choices":["list","tuple","dictionary","set item"],"answer":"tuple","explain":"Tuples cannot be changed after creation."}, {"topic":"Dictionaries","q":"How do you access the value for key 'name' in d?","choices":["d.name","d['name']","d(name)","d->name"],"answer":"d['name']","explain":"Dictionary values are accessed using keys in square brackets."}, {"topic":"Strings","q":"What is 'Python'[0]?","choices":["P","y","Python","Error"],"answer":"P","explain":"The first character is at index 0."}, {"topic":"Strings","q":"Are Python strings mutable?","choices":["Yes","No","Only short strings","Only numeric strings"],"answer":"No","explain":"Strings are immutable."}, {"topic":"Functions","q":"Which keyword defines a function?","choices":["function","def","fun","define"],"answer":"def","explain":"Python uses def to define functions."}, {"topic":"Functions","q":"What does a function return if it has no return statement?","choices":["0","False","None","Empty string"],"answer":"None","explain":"A function without return returns None."}, {"topic":"Functions","q":"Which keyword sends a result back from a function?","choices":["send","return","yield only","break"],"answer":"return","explain":"return gives a result back to the caller."}, {"topic":"Exceptions","q":"Which exception often occurs when int('abc') is executed?","choices":["TypeError","ValueError","ZeroDivisionError","NameError"],"answer":"ValueError","explain":"'abc' cannot be converted to an integer."}, {"topic":"Exceptions","q":"Which exception occurs for 10 / 0?","choices":["ValueError","ZeroDivisionError","IndexError","KeyError"],"answer":"ZeroDivisionError","explain":"Division by zero raises ZeroDivisionError."}, {"topic":"Operators","q":"What is True and False?","choices":["True","False","None","Error"],"answer":"False","explain":"and returns True only when both sides are True."}, {"topic":"Operators","q":"What is not True?","choices":["True","False","None","Error"],"answer":"False","explain":"not reverses the Boolean value."}, {"topic":"Lists","q":"What does len([1, 2, 3]) return?","choices":["2","3","4","Error"],"answer":"3","explain":"len() returns the number of items."}, {"topic":"Dictionaries","q":"Which method returns dictionary keys?","choices":["keys()","items()","values()","index()"],"answer":"keys()","explain":"keys() returns the dictionary keys."}, {"topic":"Strings","q":"What is 'abc' + 'def'?","choices":["abcdef","abc def","Error","6"],"answer":"abcdef","explain":"+ concatenates strings."}, {"topic":"Variables","q":"Which is a valid Python variable name?","choices":["2name","first_name","first-name","class"],"answer":"first_name","explain":"Variable names cannot start with a number, contain hyphen, or use keywords."}, {"topic":"Fundamentals","q":"Which statement is case-sensitive in Python?","choices":["Name and name are different","Name and name are same","Only functions are case-sensitive","Only strings are case-sensitive"],"answer":"Name and name are different","explain":"Python identifiers are case-sensitive."}, {"topic":"Input/Output","q":"What does print('A', 'B', sep='-') display?","choices":["A B","A-B","AB","A, B"],"answer":"A-B","explain":"sep='-' places a hyphen between output items."}, ] progress = {"completed_lessons": [], "quiz_scores": {}, "exam_history": []} current_exam = [] def get_lesson(topic): data = LESSONS[topic] return f""" # {topic} **PCEP block:** {data['block']} **Approximate exam weight:** {data['weight']} {data['content']} """, data["starter_code"] def mark_complete(topic): if topic not in progress["completed_lessons"]: progress["completed_lessons"].append(topic) return dashboard() def run_code(code): safe_builtins = { "print": print, "range": range, "len": len, "int": int, "float": float, "str": str, "bool": bool, "list": list, "tuple": tuple, "dict": dict, "set": set, "sum": sum, "min": min, "max": max, "sorted": sorted, "type": type, "enumerate": enumerate, "abs": abs, "round": round, "True": True, "False": False, "None": None } env = {"__builtins__": safe_builtins} output = io.StringIO() try: with contextlib.redirect_stdout(output): exec(code, env, env) result = output.getvalue() return result if result else "Code ran successfully with no printed output." except Exception: return "Error:\n" + traceback.format_exc(limit=2) def get_practice_question(topic_filter): pool = QUESTION_BANK if topic_filter == "All" else [q for q in QUESTION_BANK if q["topic"] == topic_filter] if not pool: pool = QUESTION_BANK q = random.choice(pool) return q["q"], gr.update(choices=q["choices"], value=None), json.dumps(q) def check_practice_answer(choice, q_json): if not q_json: return "Please generate a question first." q = json.loads(q_json) if choice == q["answer"]: progress["quiz_scores"].setdefault(q["topic"], []).append(1) return f"✅ Correct. {q['explain']}" progress["quiz_scores"].setdefault(q["topic"], []).append(0) return f"❌ Incorrect. Correct answer: **{q['answer']}**. {q['explain']}" def start_exam(): global current_exam current_exam = random.sample(QUESTION_BANK, min(30, len(QUESTION_BANK))) outputs = [] for i, q in enumerate(current_exam): outputs.append(gr.update(label=f"Q{i+1}. {q['q']}", choices=q["choices"], value=None, visible=True)) return outputs + ["Exam started. Answer all questions, then click Submit Exam."] def submit_exam(*answers): if not current_exam: return "Please start an exam first.", dashboard() correct = 0 topic_total = {} topic_correct = {} review = [] for i, q in enumerate(current_exam): ans = answers[i] topic_total[q["topic"]] = topic_total.get(q["topic"], 0) + 1 if ans == q["answer"]: correct += 1 topic_correct[q["topic"]] = topic_correct.get(q["topic"], 0) + 1 else: review.append(f"Q{i+1}: {q['q']}\nYour answer: {ans}\nCorrect answer: {q['answer']}\nExplanation: {q['explain']}") score = round(correct / len(current_exam) * 100, 1) status = "PASS" if score >= PASS_MARK else "REVIEW NEEDED" progress["exam_history"].append({"date": datetime.now().strftime("%Y-%m-%d %H:%M"), "score": score}) weak = [] for topic, total in topic_total.items(): pct = round(topic_correct.get(topic, 0) / total * 100) if pct < 70: weak.append(f"{topic}: {pct}%") feedback = f"# Exam Result\nScore: **{score}%** ({correct}/{len(current_exam)})\n\nStatus: **{status}**\n\n" if weak: feedback += "## Weak areas to review\n" + "\n".join([f"- {w}" for w in weak]) + "\n\n" if review: feedback += "## Wrong-answer review\n" + "\n\n".join(review[:10]) else: feedback += "Excellent work. No wrong answers in this attempt." return feedback, dashboard() def dashboard(): total_lessons = len(LESSONS) completed = len(progress["completed_lessons"]) completion = round(completed / total_lessons * 100) quiz_attempts = sum(len(v) for v in progress["quiz_scores"].values()) quiz_correct = sum(sum(v) for v in progress["quiz_scores"].values()) quiz_pct = round(quiz_correct / quiz_attempts * 100, 1) if quiz_attempts else 0 best_exam = max([x["score"] for x in progress["exam_history"]], default=0) lines = [ "# Progress Dashboard", f"Lessons completed: **{completed}/{total_lessons}** ({completion}%)", f"Practice quiz accuracy: **{quiz_pct}%** ({quiz_correct}/{quiz_attempts})", f"Best mock exam score: **{best_exam}%**", "", "## Completed lessons", ] lines += [f"- {x}" for x in progress["completed_lessons"]] or ["- None yet"] lines.append("\n## Recommended next step") if completion < 100: lines.append("Complete all lessons, then attempt the mock exam.") elif best_exam < PASS_MARK: lines.append("Review weak areas and retake the mock exam.") else: lines.append("Good progress. Continue practising timed questions before the real exam.") return "\n".join(lines) def export_progress(): return json.dumps(progress, indent=2) with gr.Blocks(title=APP_TITLE, theme=gr.themes.Soft()) as demo: gr.Markdown(f"# {APP_TITLE}\nBeginner-friendly Python learning and PCEP exam preparation platform.") with gr.Tab("Learn"): topic = gr.Dropdown(list(LESSONS.keys()), value="1. Python Fundamentals", label="Choose lesson") lesson_md = gr.Markdown() complete_btn = gr.Button("Mark lesson as completed") dash_from_lesson = gr.Markdown() topic.change(get_lesson, topic, [lesson_md, gr.State()]) demo.load(get_lesson, topic, [lesson_md, gr.State()]) complete_btn.click(mark_complete, topic, dash_from_lesson) with gr.Tab("Code Playground"): code_topic = gr.Dropdown(list(LESSONS.keys()), value="1. Python Fundamentals", label="Load starter code") code_box = gr.Code(label="Python Code", language="python", lines=12) run_btn = gr.Button("Run Code") code_output = gr.Textbox(label="Output", lines=10) code_topic.change(lambda t: LESSONS[t]["starter_code"], code_topic, code_box) demo.load(lambda: LESSONS["1. Python Fundamentals"]["starter_code"], None, code_box) run_btn.click(run_code, code_box, code_output) with gr.Tab("Practice Quiz"): practice_topic = gr.Dropdown(["All"] + sorted(set(q["topic"] for q in QUESTION_BANK)), value="All", label="Question topic") q_text = gr.Markdown("Click Generate Question to begin.") q_choice = gr.Radio(label="Choose your answer", choices=[]) q_state = gr.State("") gen_btn = gr.Button("Generate Question") check_btn = gr.Button("Check Answer") feedback = gr.Markdown() gen_btn.click(get_practice_question, practice_topic, [q_text, q_choice, q_state]) check_btn.click(check_practice_answer, [q_choice, q_state], feedback) with gr.Tab("Mock Exam"): gr.Markdown("Start a randomized 30-question PCEP-style mock exam. Suggested time limit: 45 minutes.") start_btn = gr.Button("Start Mock Exam") exam_status = gr.Markdown() exam_radios = [] for i in range(30): exam_radios.append(gr.Radio(label=f"Q{i+1}", choices=[], visible=True)) submit_btn = gr.Button("Submit Exam") exam_result = gr.Markdown() exam_dash = gr.Markdown() start_btn.click(start_exam, None, exam_radios + [exam_status]) submit_btn.click(submit_exam, exam_radios, [exam_result, exam_dash]) with gr.Tab("Dashboard"): dash = gr.Markdown() refresh_btn = gr.Button("Refresh Dashboard") export_btn = gr.Button("Export Progress JSON") export_box = gr.Textbox(label="Progress JSON", lines=12) demo.load(dashboard, None, dash) refresh_btn.click(dashboard, None, dash) export_btn.click(export_progress, None, export_box) if __name__ == "__main__": demo.launch()