Starting CS50P even though I already write Python

August 16, 2026 code learningpython

Starting CS50P this month felt a little backwards, tbh. I've already shipped a Django CRM, a camera script that emails me, a news digest, a handful of scrapers and PDF tools, two boot.dev projects (bookbot, webflyx). CS50P is the "intro to Python" course. I already know what a for loop is.

I did it anyway, and I'm glad I did, four weeks in.

what fundamentals courses actually check

Not whether you can write a for loop. Whether you can write one that another person, or check50, or future-me six months from now, can actually read and trust. CS50P's test_ file convention, actually writing real pytest tests instead of eyeballing output in the terminal, was the thing that embarrassed me a little. I've been writing "real" scripts for a while and testing basically none of them.

the first test file I wrote, which technically passed and proved nothing

The week's assignment had me writing a small input-validation function, checking that a plate-style code follows a set of formatting rules, and writing tests alongside it. My first pass at the test file looked like this:

from project import is_valid

def test_valid():
    assert is_valid("CS50P")

def test_invalid():
    assert not is_valid("cs50p")

Two tests. Both passed. Green checkmarks, felt great for about four minutes, right up until check50 ran its own hidden tests against my is_valid function and failed on an input I'd never actually tried: a plate with a number in the middle, like AB12CD, which my function incorrectly accepted.

:( is_valid rejects plates with a letter after a digit
    expected "False", not "True"

the actual problem, once I looked

# project.py, the broken version
def is_valid(s):
    return s[:2].isalpha() and s[2:].isalnum()

Checks that it starts with letters and the rest is alphanumeric. Doesn't check the actual order the rules require, that once a digit shows up, nothing but digits can follow it. AB12CD passes this check completely: letters at the start, alphanumeric for the rest, technically true, wrong answer.

My own two tests hadn't caught this because I'd only tested one obviously-valid case and one obviously-invalid case. Neither was anywhere near the actual edge, which is exactly where the real bug was hiding.

what I actually googled

"pytest test edge cases how many test cases is enough" led me, eventually, to the idea of table-driven or parametrized tests, writing a list of cases up front (input, expected result) instead of one assert at a time, specifically so adding a new edge case is just adding one row instead of writing a whole new function.

import pytest
from project import is_valid

@pytest.mark.parametrize("plate, expected", [
    ("CS50P", True),
    ("cs50p", False),
    ("AB12CD", False),
    ("A1", False),
    ("", False),
])
def test_is_valid(plate, expected):
    assert is_valid(plate) == expected

Five cases instead of two, and writing them out as a table made the gap in my own thinking obvious before check50 had to point it out for me. AB12CD was right there once I was forced to actually list edge cases instead of picking two that felt representative.

the fixed function

# project.py, the fixed version
def is_valid(s):
    seen_digit = False
    for ch in s:
        if ch.isdigit():
            seen_digit = True
        elif seen_digit:
            return False
    return len(s) >= 2 and s[:2].isalpha()

Walks the string once, and the moment a letter shows up after a digit has already been seen, it's invalid. Passed my expanded local tests and check50's hidden ones on the next submit.

the rename script, revisited

Remember the photo-renaming script I wrote a while back, the one that crashed on a filename collision the first time I ran it? I went back and looked at it after a CS50P lecture on exceptions, and past-me had wrapped nothing in a try block, just let it crash and fixed the bug by reading the traceback afterward. Worked, technically. Not exactly the recommended approach.

The old rename script, from before this course made me think about failure modes

Going back with fresh eyes, the fix wasn't even about exceptions really, it was that I'd never once considered what should happen when a rename target already exists, only ever discovered it by watching the thing crash. A try/except FileExistsError around the actual os.rename call, falling back to a numbered suffix, would have made the first version correct on day one instead of correct on the fourth debugging pass.

a small tangent about how long I fought the lecture video instead of the code

Genuinely lost about forty minutes convinced there was a bug in pytest itself, because my parametrized test kept reporting the wrong expected value in the failure message, showing False when I was certain I'd written True for that row. There was no bug. I'd miscounted rows while adding a new case in the middle of the list and shifted every subsequent expected value off by one, so row four's plate was being checked against row five's expected answer. pytest's error message was completely correct the entire time. I just didn't believe it until I printed the whole parametrize list out and counted by hand like it was a spelling test. Table-driven tests are only as trustworthy as the table, which is obvious in hindsight and wasn't obvious at all at minute thirty of staring at a red X.

what's actually different this time around

I'm not learning syntax. I already have that, mostly by accident, from just building things that needed to work. What CS50P is actually giving me is the stuff nobody points at directly until a course does: proper library usage instead of copy-pasting Stack Overflow until it compiles, actual testing discipline, code that reads like it was written for a person and not just for the interpreter to accept.

It's a weird kind of humbling. bookbot and webflyx both worked, shipped, did the thing they were supposed to do. Neither of them would have survived a code review from someone who actually knew what they were doing, and I only really believe that now that I've seen what the alternative looks like, table-driven tests included.

what I'd do differently going forward

Write the table of edge cases before writing the function, not after. Every bug I've hit in this course so far was sitting in the gap between "the two cases I thought to test" and "the case check50 actually tried." Listing edge cases first would turn that gap into something I catch on my own, instead of something a grading script catches for me.

I'd also stop trusting a green test run as proof of anything by itself. Two passing tests told me nothing useful about is_valid, because the two cases I picked were both comfortably far from the actual edge of the rule. A test suite is only as good as the cases somebody actually thought to write, mine included, and "it passed" is a much weaker sentence than it sounds like the first time you type it.

Whether I'll actually keep writing tests for every throwaway script after the course ends is a separate question. Ask me in a few months. My honest guess right now is: for the camera script and the news digest, probably yes, since those run unattended and I won't be there to notice a silent failure. For a one-off scraper I run once and throw away, probably still no, and I'm not sure that's actually the wrong call.

1 comment

Log in to comment.

tore_b August 18, 2026

A beginner course being useful even when you already know the language is more common than people admit. You quietly fill in the holes you did not know you had.

Log in

No account?