Skip to main content
7 Steps · Brief to Gradescope

How to Attack a Python Assignment, End to End

This is the workflow strong Python teaching assistants drill into their students at office hours. Students who follow it ship working code on time. Students who skip steps get stuck in hour three with 40 lines of broken code and no plan to recover. The 7 steps below run from reading the brief on day one to clicking submit on Gradescope on day five.

Used by working Python developers 7 steps, every assignment Tested on 200+ coursework jobs
Step 1 of 7

Read the brief twice, mark every requirement

The single highest-return move in coursework is annotating the brief before any code. Print the assignment PDF. Highlight 4 things in 4 colors: every input, every output, every constraint, every rubric line. Add a fifth pass for the deadline and late-submission window.

What students skip and lose points on

The rubric. 1 in 3 students reads only the prose section and submits code that passes functional tests but loses 20 points on docstring, type hint, or filename formatting. The rubric is where 20 to 40% of the grade hides on a typical CSE 163, CS50P, or DATA 100 assignment.

Read the brief a second time after a 10-minute break. New constraints surface on the second pass roughly 60% of the time. The breaks matters more than the highlighter.

The 5 things you mark

  • Every input the program receives (CSV path, stdin line, dict, function arg)
  • Every output the program returns (number, list, JSON file, plot, stdout text)
  • Every constraint (no sorted(), runtime cap, library bans)
  • Every rubric line and its point value
  • The deadline plus the late-penalty schedule
Worked example: brief annotation
ASSIGNMENT 3: Sorted Merge (CSE 163, 100 points)

Write a function merge_sorted(a: list[int], b: list[int]) -> list[int]   # [INPUT 1, INPUT 2]
that returns a single sorted list combining both inputs.                  # [OUTPUT]

Constraints:
- Do NOT use sorted() or list.sort().                                     # [CONSTRAINT 1: no built-in sort]
- Inputs may be empty. Inputs may contain duplicates.                     # [CONSTRAINT 2: edge cases]
- Inputs are guaranteed to each be sorted ascending on entry.             # [CONSTRAINT 3: precondition]

Rubric:
- Correctness on 12 hidden test cases (60 pts)                            # [RUBRIC: tests drive 60%]
- O(n + m) time complexity (20 pts)                                       # [RUBRIC: complexity tested]
- Type hints + docstring on the function (10 pts)                         # [RUBRIC: easy points]
- Files: merge.py only. No subfolders. No notebooks.                      # [RUBRIC: filename strict]

Due: Friday 11:59 PM Pacific via Gradescope.                              # [DEADLINE]
Late penalty: 10% per 24h, max 48h late.                                  # [LATE WINDOW]
Step 2 of 7

Scope inputs and outputs in writing

Before any code, write the function signature on paper. Type the input shape and the output shape in plain English. A CSV with 14 columns and 800 rows is a different problem from a stdin line of 14 space-separated integers, even though both are "input". Naming the shape forces the design.

Input shapes you describe in 1 sentence

  • "A CSV at data/students.csv with 4 columns: id, name, grade, gpa."
  • "A single stdin line of 6 space-separated integers between 0 and 99."
  • "A Python dict mapping str book titles to int page counts."
  • "A JSON file with a top-level array of order objects."

Output shapes you describe in 1 sentence

  • "A single int printed to stdout, the count of passing students."
  • "A sorted list[int] returned from the function."
  • "A pandas DataFrame written to out.csv with no index column."
  • "A matplotlib figure saved to scatter.png at 300 dpi."
Function signature, written before the body
# merge.py: first line written for this assignment

def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """Return a single ascending-sorted list combining a and b in O(n + m).

    Inputs are each sorted ascending on entry. Duplicates allowed.
    Inputs may be empty. Inputs are not mutated.
    """
    ...

The 3 ellipses are the body you have not written yet. The signature plus docstring already earns the easy 10 rubric points on most CSE 163 and CS50P assignments. You earned them in 90 seconds, before any logic.

Step 3 of 7

Sketch pseudocode in plain English

Every step the program takes, written in 4 to 12 lines of plain English. No Python syntax. No semicolons. The pseudocode is the document you write the code FROM. Skipping this step is the most common reason students rewrite their solution at hour four with 60% of the time gone.

Pseudocode for the sorted-merge brief
function merge_sorted(a, b):
  create empty result list
  set pointer i = 0   (position in a)
  set pointer j = 0   (position in b)
  while i < length(a) AND j < length(b):
    if a[i] is smaller or equal to b[j]:
      append a[i] to result
      increment i
    else:
      append b[j] to result
      increment j
  append any remaining items from a (from index i to end)
  append any remaining items from b (from index j to end)
  return result

What good pseudocode looks like

Each line maps to 1 to 3 lines of Python. Variable names are real (i, j, result), not abstract (foo, bar). Conditions read like English ("if a[i] is smaller or equal to b[j]") so a non-coder could follow the logic. Total length is 6 to 12 lines for a typical single-function assignment.

The translation discipline

One pseudocode line, one Python block. If a pseudocode line takes 8 lines of Python to implement, the pseudocode was too coarse. Break it into 2 lines and try again. This keeps the code easy to debug because each block has a known goal.

Step 4 of 7

Write tests first, even if the course does not require them

Tests are the green/red signal that tells you whether the last 20 minutes of typing helped or hurt. Write 3 to 5 pytest cases from the example I/O in the brief BEFORE the implementation. Run them: they fail with NameError or AssertionError. Now you have a target. This is TDD-lite for coursework and it cuts debug time roughly in half.

tests/test_merge.py
# tests/test_merge.py
# Requires: pip install pytest
import pytest
from merge import merge_sorted


def test_basic_interleave():
    assert merge_sorted([1, 3, 5], [2, 4, 6]) == [1, 2, 3, 4, 5, 6]


def test_empty_left():
    assert merge_sorted([], [2, 4, 6]) == [2, 4, 6]


def test_empty_right():
    assert merge_sorted([1, 3, 5], []) == [1, 3, 5]


def test_both_empty():
    assert merge_sorted([], []) == []


def test_duplicates_across_lists():
    assert merge_sorted([1, 2, 2, 3], [2, 2, 4]) == [1, 2, 2, 2, 2, 3, 4]


@pytest.mark.parametrize("a, b, expected", [
    ([0], [0], [0, 0]),
    ([-3, -1], [-2, 0], [-3, -2, -1, 0]),
    ([1, 1, 1], [1, 1], [1, 1, 1, 1, 1]),
])
def test_parametrized_cases(a, b, expected):
    assert merge_sorted(a, b) == expected

How many tests is enough

5 from the brief examples, then 3 to 5 edge cases the brief implies. For a sorted-merge, edges include both inputs empty, one input empty, duplicates across both inputs, all negatives, and the largest-allowed input. Total of 8 to 10 tests covers a typical single-function coursework problem at 80 to 90% line coverage.

Running pytest in 2 commands

  • pip install pytest
  • python -m pytest tests/ -v
  • Add -k test_basic to run one test by name
  • Add --tb=short for shorter failure output
Step 5 of 7

Implement from pseudocode, one block at a time

Translate each pseudocode line into Python, then run the tests. A test goes green: move on. A test goes red: stop and debug with breakpoint() right before the line that broke. Do not write 80 lines and run pytest at the end. That pattern turns a 10-minute bug into a 2-hour bug.

merge.py: implementation from the pseudocode
# merge.py
from __future__ import annotations


def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """Merge two ascending-sorted integer lists into one sorted list in O(n + m)."""
    result: list[int] = []
    i, j = 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            result.append(a[i])
            i += 1
        else:
            result.append(b[j])
            j += 1
    # Drain whichever list still has items
    if i < len(a):
        result.extend(a[i:])
    if j < len(b):
        result.extend(b[j:])
    return result
Debug session with breakpoint()
# Add a breakpoint where the test starts failing
def merge_sorted(a, b):
    result = []
    i, j = 0, 0
    while i < len(a) and j < len(b):
        breakpoint()  # Drops into pdb on the first iteration
        if a[i] <= b[j]:
            result.append(a[i])
            i += 1
        else:
            result.append(b[j])
            j += 1
    # ...

# In the pdb prompt:
#   (Pdb) p a, b, i, j     -> inspect current state
#   (Pdb) p result         -> see what is built so far
#   (Pdb) n                -> step to the next line
#   (Pdb) c                -> continue to the next breakpoint

For Python 3.10 and newer, breakpoint() drops into pdb with no import. See the debugging guide for the full pdb command reference and the 4-step root-cause method.

Step 6 of 7

Verify against the rubric, line by line

Open the rubric and check each line against your code. Read every line of your code aloud: if you cannot explain why a line exists, rewrite it or delete it. Add the edge tests the brief implied. The marginal cost is 20 minutes. The marginal gain is 15 to 25 points on a 100-point assignment.

tests/test_merge_edges.py: cases the brief implied
# tests/test_merge_edges.py
from merge import merge_sorted


def test_single_element_each():
    assert merge_sorted([7], [3]) == [3, 7]


def test_all_negative():
    assert merge_sorted([-10, -5, -1], [-9, -6, -2]) == [-10, -9, -6, -5, -2, -1]


def test_one_much_longer():
    long = list(range(0, 1000, 2))           # 0, 2, 4, ... 998
    short = [1, 999]
    out = merge_sorted(long, short)
    assert out == sorted(long + short)
    assert len(out) == 502


def test_does_not_mutate_inputs():
    a = [1, 3, 5]
    b = [2, 4, 6]
    merge_sorted(a, b)
    assert a == [1, 3, 5]
    assert b == [2, 4, 6]

The read-aloud test

Read every line of merge.py aloud. Each line gets one sentence: what it does and why. "The while loop continues until either pointer reaches the end of its list, because we cannot compare past the end." Lines you cannot explain are lines a grader will mark down.

5 edges to always test

  • Empty input (both empty, each empty separately)
  • Single element on each side
  • Duplicates within and across inputs
  • Negative numbers, zero, and large numbers
  • Inputs are not mutated after the call
Step 7 of 7

Submission prep, 45 minutes before the deadline

Filename exactly as the brief states (case-sensitive on Gradescope and Linux). Required files only. No __pycache__, no .pyc, no .venv, no notebooks unless the brief asks for them. Final test pass. Submit. Screenshot the confirmation page.

Final submission bundle
assignment-03/
├── merge.py
├── README.md
└── requirements.txt

# Files you do NOT submit (already in .gitignore):
#   __pycache__/
#   .pytest_cache/
#   .venv/
#   tests/         (unless the brief asks for them)
#   *.pyc
Submission commands
# Final test pass right before upload
python -m pytest tests/ -v

# Build requirements.txt from your venv
pip freeze > requirements.txt

# Confirm the exact filename matches the brief (case-sensitive on Gradescope)
ls -la merge.py

# Zip only the required files
zip submit.zip merge.py README.md requirements.txt

.gitignore for clean bundles

Add __pycache__/, .pytest_cache/, .venv/, *.pyc, and editor folders (.vscode/, .idea/) to your .gitignore before the first commit. See the setup guide for the full file.

Confirm and screenshot

Gradescope shows a green confirmation panel with a timestamp and the auto-test result. Screenshot it. If the submission portal goes down 4 minutes before the deadline (it happens 2 to 3 times per term), that screenshot is your only proof you submitted on time.

5-day plan

Timeline for a 5-day Python assignment

The night-before pattern produces submitted-but-broken code 8 times out of 10. The 5-day spread below leaves buffer for the inevitable bug at hour two of day three.

Day 1 (Monday)

60 to 90 minutes

Print the brief. Annotate every input, output, constraint, rubric line. Write pseudocode in plain English. No code yet.

Day 2 (Tuesday)

90 to 120 minutes

Write 5 to 8 pytest cases from the example I/O in the brief. Stub the function so tests run red. Set up the project tree and .gitignore.

Day 3 (Wednesday)

2 to 3 hours

Translate pseudocode into Python, one block at a time. Run tests after each block. Use breakpoint() the moment a test goes red.

Day 4 (Thursday)

90 minutes

Add 4 to 6 edge-case tests the brief implied but did not state. Read every line of your code aloud. Rewrite anything you cannot explain.

Day 5 (Friday)

45 minutes, by 6 PM

Verify filename, bundle required files only, run final pytest pass, submit on Gradescope. Screenshot the submission confirmation.

Why the night-before pattern fails

The first hour is spent re-reading the brief because it was last seen 4 days ago. Pseudocode gets skipped under deadline pressure. Tests get skipped because "there is no time". The bug at hour four has no test to isolate it, no pseudocode to compare against, and no rubric annotation to remind you which 10-point item you missed. The submission uploads at 11:58 PM, passes 3 of 12 tests, and earns 35 of 100.

The 5-day plan turns the same brief into a 6-hour total spend with a verified result and a screenshot of a green confirmation panel.

Stuck halfway through this workflow?

Send the brief, the code you have so far, and the test output. A named Python developer picks up the assignment, runs the same 7 steps, and returns tested code with a walkthrough you can defend in your class discussion. Starts at $29. Pay 50% to start, 50% after the code runs on your data.