Before we start, I want to show you something. I pulled the first piece of code you sent me — the config loader from Day 3. Eleven if key in config checks.
I remember that function. I thought it was the most defensive code I had ever written.
Look at what you sent me this morning. Day 3: eleven if key in config / else: default blocks. This morning: a typed, documented function that does the same thing in three lines with .get() defaults and a PipelineConfig return type. Same problem. Four weeks between them.
I did not realize I was solving the same problem. The second version is shorter and communicates more. That is what "Pythonic" means — not tricks, not brevity for its own sake.
Exactly. Now here is the brief for today. A junior dev submitted a PR for a reporting module. It works. All tests pass. But every week's patterns are in it — in the wrong direction:
def generate_order_report(orders, include_cancelled=False, results=[]):
report_string = ""
for i in range(len(orders)):
order = orders[i]
if order != None:
if order.get("status") != None:
if order["status"] == "active" or include_cancelled == True:
if order.get("amount") != None:
if order["amount"] > 0:
line = "Order " + str(order["id"]) + ": $" + str(order["amount"])
report_string = report_string + line + chr(10)
results.append(order)
return {"report": report_string, "orders": results}I have written that function. I recognize it. Mutable default. range(len()). Multiple != None. == True. Five nesting levels. String concatenation in a loop.
That is every anti-pattern from every week. Week 1: != None and == True. Week 2: mutable default, no docstring, no annotations. Week 3: string concatenation in a loop, range(len()). Week 4 debugging: five conditions before reaching the interesting line — you cannot set a meaningful breakpoint without stepping through four guards.
Guard clauses first. Each condition becomes a continue — the happy path reaches column zero without nesting. Mutable default replaced by a fresh list inside the function. range(len()) replaced by direct iteration. String accumulation replaced by collecting parts in a list and joining once at the end.
Every anti-pattern from every week — fixed. And the function is correct in a way the original was not: the results=[] default silently accumulates across calls in the same process. A unit test would pass. Production would double the report contents.
I would have caught the logic bugs in testing. But the mutable default would have passed every unit test and then doubled the report contents in production when two requests hit the function without a restart between them.
Exactly the scenario from Week 2. And I profiled the original on a realistic dataset:
ncalls tottime cumtime filename:lineno(function)
1 0.003 4.821 report.py:1(generate_order_report)
4200 4.802 4.802 {built-in method builtins.str}All the time is in str. The string construction, not the loop.
4200 concatenations, each copying the entire accumulated string. O(n²). The join version: 9 milliseconds. cProfile found it in three seconds. You would have optimized the loop and missed the real bottleneck.
The function still needs observability. In production, malformed orders should log a warning with the order ID rather than failing silently. Named logger, percent-style formatting, enough context to reconstruct what happened without redeploying.
That is production-grade code. Named logger scoped to the module. Percent-style formatting — the string is never built unless the level is active. Enough context in each message to reconstruct what happened without a debugger. The function's logic is unchanged. It gained observability.
Three months ago I would have written print(f"WARNING: skipping order") and committed it. And then removed it when the reviewer said "remove the print statements." The problem was not that I was logging — it was that I had no idea there was a professional way to do it.
The print and the logger.warning are identical in development. In production, the logging version is routed, filtered, timestamped, and queryable. The print is lost to stdout. The choice between them is invisible while writing the code and critical once it is running.
The capstone is full_code_review: take broken code, a cProfile output, and an error log, return a complete audit — anti-patterns, hotspots with severity, parsed tracebacks with locations, and a verdict.
Four weeks of tools in one function. Anti-patterns first, then profile hotspots by cumtime, then parse the error log for tracebacks — exception type, location, and whether the exception is chained.
I built each piece as its own lesson. Anti-pattern detection is line scanning. Profile analysis is filtering by cumtime. Traceback parsing is the Day 25 function. The capstone is knowing which tool to reach for when.
The last thing before you build it. You said on Day 1 your goal was to write code that senior devs would not rewrite in code review. That was the wrong goal.
How was that the wrong goal?
Code that senior devs do not touch is often code they have not read carefully. The goal is code they read and then go learn something from. The refactored generate_order_report — guard clauses, named logger, typed signature, single join — a senior dev reviewing that does not rewrite it. They mark it approved and ask you how you got there. Build it.
And tomorrow is the post-Likert. Day 1 I rated myself a three on idioms and a two on profiling. I want to see what I say now — not because the number matters, but because it is the measure of the gap between where I started and where I am.
The number does not matter. The fact that you have a clear answer is the measure.
Anti-pattern detection as static analysis. The line-by-line scan in full_code_review is a simplified form of the same approach linters like Ruff use for syntactic checks. Each pattern has a detection heuristic: leading whitespace depth for nesting, += inside an indented block for string concatenation, range(len( as a substring match. Real static analysis tools use an AST — ast.parse(code) and ast.walk(tree) — which catches patterns like == True regardless of formatting and handles multi-line constructs correctly. The substring approach in the challenge is sufficient for the patterns defined; for production linting, AST walking is the right tool.
Profile analysis and the two-pass bottleneck rule. cProfile outputs tottime (time in the function excluding callees) and cumtime (time in the function including callees). A function can have high cumtime and low tottime — it is a coordinator that calls expensive functions. A function with high tottime is doing expensive work directly. The correct optimization question is: which function's tottime can I reduce? cumtime tells you where time is being spent in aggregate; tottime tells you where the work is actually happening. Sorting by cumtime descending (as the capstone does) finds the most expensive call chains. The hotspot threshold of > 1.0 second is a practical default — your specific context may require a different threshold.
Traceback parsing and the chaining graph. A Python traceback string is a serialization of the exception chain. raise X from Y sets X.__cause__ = Y; bare raise X in an except block sets X.__context__ = Y. traceback.format_exception follows these links to produce the multi-block output. Parsing it in reverse — splitting on the connector phrase, taking the last block as the final exception — reconstructs the chain direction. The outermost frame of the final block (the origin) is the entry point from the calling code's perspective: the first place in the chain where an assumption was made that ultimately led to the exception. This is the frame that answers "where should the fix go," not the frame that raised the exception.
The verdict as a codified review standard. full_code_review produces a verdict of approved, review, or needs_work based on counts and severity. This pattern — reducing structured observations to a categorical judgment — is how automated code review gates work in CI pipelines. The thresholds in the challenge are illustrative; a real tool calibrates them against historical review outcomes. The value is not the specific verdict but the fact that the judgment is derived from evidence that can be inspected and disputed, rather than from a reviewer's uncommunicated intuitions.
Sign up to write and run code in this lesson.
Before we start, I want to show you something. I pulled the first piece of code you sent me — the config loader from Day 3. Eleven if key in config checks.
I remember that function. I thought it was the most defensive code I had ever written.
Look at what you sent me this morning. Day 3: eleven if key in config / else: default blocks. This morning: a typed, documented function that does the same thing in three lines with .get() defaults and a PipelineConfig return type. Same problem. Four weeks between them.
I did not realize I was solving the same problem. The second version is shorter and communicates more. That is what "Pythonic" means — not tricks, not brevity for its own sake.
Exactly. Now here is the brief for today. A junior dev submitted a PR for a reporting module. It works. All tests pass. But every week's patterns are in it — in the wrong direction:
def generate_order_report(orders, include_cancelled=False, results=[]):
report_string = ""
for i in range(len(orders)):
order = orders[i]
if order != None:
if order.get("status") != None:
if order["status"] == "active" or include_cancelled == True:
if order.get("amount") != None:
if order["amount"] > 0:
line = "Order " + str(order["id"]) + ": $" + str(order["amount"])
report_string = report_string + line + chr(10)
results.append(order)
return {"report": report_string, "orders": results}I have written that function. I recognize it. Mutable default. range(len()). Multiple != None. == True. Five nesting levels. String concatenation in a loop.
That is every anti-pattern from every week. Week 1: != None and == True. Week 2: mutable default, no docstring, no annotations. Week 3: string concatenation in a loop, range(len()). Week 4 debugging: five conditions before reaching the interesting line — you cannot set a meaningful breakpoint without stepping through four guards.
Guard clauses first. Each condition becomes a continue — the happy path reaches column zero without nesting. Mutable default replaced by a fresh list inside the function. range(len()) replaced by direct iteration. String accumulation replaced by collecting parts in a list and joining once at the end.
Every anti-pattern from every week — fixed. And the function is correct in a way the original was not: the results=[] default silently accumulates across calls in the same process. A unit test would pass. Production would double the report contents.
I would have caught the logic bugs in testing. But the mutable default would have passed every unit test and then doubled the report contents in production when two requests hit the function without a restart between them.
Exactly the scenario from Week 2. And I profiled the original on a realistic dataset:
ncalls tottime cumtime filename:lineno(function)
1 0.003 4.821 report.py:1(generate_order_report)
4200 4.802 4.802 {built-in method builtins.str}All the time is in str. The string construction, not the loop.
4200 concatenations, each copying the entire accumulated string. O(n²). The join version: 9 milliseconds. cProfile found it in three seconds. You would have optimized the loop and missed the real bottleneck.
The function still needs observability. In production, malformed orders should log a warning with the order ID rather than failing silently. Named logger, percent-style formatting, enough context to reconstruct what happened without redeploying.
That is production-grade code. Named logger scoped to the module. Percent-style formatting — the string is never built unless the level is active. Enough context in each message to reconstruct what happened without a debugger. The function's logic is unchanged. It gained observability.
Three months ago I would have written print(f"WARNING: skipping order") and committed it. And then removed it when the reviewer said "remove the print statements." The problem was not that I was logging — it was that I had no idea there was a professional way to do it.
The print and the logger.warning are identical in development. In production, the logging version is routed, filtered, timestamped, and queryable. The print is lost to stdout. The choice between them is invisible while writing the code and critical once it is running.
The capstone is full_code_review: take broken code, a cProfile output, and an error log, return a complete audit — anti-patterns, hotspots with severity, parsed tracebacks with locations, and a verdict.
Four weeks of tools in one function. Anti-patterns first, then profile hotspots by cumtime, then parse the error log for tracebacks — exception type, location, and whether the exception is chained.
I built each piece as its own lesson. Anti-pattern detection is line scanning. Profile analysis is filtering by cumtime. Traceback parsing is the Day 25 function. The capstone is knowing which tool to reach for when.
The last thing before you build it. You said on Day 1 your goal was to write code that senior devs would not rewrite in code review. That was the wrong goal.
How was that the wrong goal?
Code that senior devs do not touch is often code they have not read carefully. The goal is code they read and then go learn something from. The refactored generate_order_report — guard clauses, named logger, typed signature, single join — a senior dev reviewing that does not rewrite it. They mark it approved and ask you how you got there. Build it.
And tomorrow is the post-Likert. Day 1 I rated myself a three on idioms and a two on profiling. I want to see what I say now — not because the number matters, but because it is the measure of the gap between where I started and where I am.
The number does not matter. The fact that you have a clear answer is the measure.
Anti-pattern detection as static analysis. The line-by-line scan in full_code_review is a simplified form of the same approach linters like Ruff use for syntactic checks. Each pattern has a detection heuristic: leading whitespace depth for nesting, += inside an indented block for string concatenation, range(len( as a substring match. Real static analysis tools use an AST — ast.parse(code) and ast.walk(tree) — which catches patterns like == True regardless of formatting and handles multi-line constructs correctly. The substring approach in the challenge is sufficient for the patterns defined; for production linting, AST walking is the right tool.
Profile analysis and the two-pass bottleneck rule. cProfile outputs tottime (time in the function excluding callees) and cumtime (time in the function including callees). A function can have high cumtime and low tottime — it is a coordinator that calls expensive functions. A function with high tottime is doing expensive work directly. The correct optimization question is: which function's tottime can I reduce? cumtime tells you where time is being spent in aggregate; tottime tells you where the work is actually happening. Sorting by cumtime descending (as the capstone does) finds the most expensive call chains. The hotspot threshold of > 1.0 second is a practical default — your specific context may require a different threshold.
Traceback parsing and the chaining graph. A Python traceback string is a serialization of the exception chain. raise X from Y sets X.__cause__ = Y; bare raise X in an except block sets X.__context__ = Y. traceback.format_exception follows these links to produce the multi-block output. Parsing it in reverse — splitting on the connector phrase, taking the last block as the final exception — reconstructs the chain direction. The outermost frame of the final block (the origin) is the entry point from the calling code's perspective: the first place in the chain where an assumption was made that ultimately led to the exception. This is the frame that answers "where should the fix go," not the frame that raised the exception.
The verdict as a codified review standard. full_code_review produces a verdict of approved, review, or needs_work based on counts and severity. This pattern — reducing structured observations to a categorical judgment — is how automated code review gates work in CI pipelines. The thresholds in the challenge are illustrative; a real tool calibrates them against historical review outcomes. The value is not the specific verdict but the fact that the judgment is derived from evidence that can be inspected and disputed, rather than from a reviewer's uncommunicated intuitions.