27 min read

> "Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler

Chapter 7: Understanding AI-Generated Code

"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." — Martin Fowler


Learning Objectives

After completing this chapter, you will be able to:

  • Remember the key components of code structure (imports, classes, functions, control flow) and explain why reading code is an essential vibe coding skill
  • Understand common patterns and anti-patterns in AI-generated code, including over-commenting, unnecessary complexity, and verbose solutions
  • Apply structural analysis and line-by-line tracing techniques to unfamiliar code
  • Analyze AI-generated code for quality indicators such as naming conventions, error handling, and adherence to best practices
  • Evaluate code for potential issues including off-by-one errors, missing validation, security vulnerabilities, and performance problems
  • Create a personal code review checklist tailored to reviewing AI-generated code

Introduction

In Chapter 6, you built a CLI task manager with AI assistance. You typed prompts, received code, and ran it. But here is a question worth pausing on: did you actually understand every line of the code that AI generated for you?

If your answer is "not entirely," you are not alone, and this chapter exists precisely for that reason.

Vibe coding is not about blindly accepting whatever an AI produces. The most effective vibe coders are those who can read code with confidence, evaluate its quality, and spot problems before they become production bugs. You do not need to be able to write every line from scratch, but you absolutely need to understand what each line does and why it is there.

Think of it this way: you do not need to know how to build a car engine from raw metal, but if you are going to drive at highway speeds, you had better understand what the dashboard warning lights mean.

This chapter teaches you the systematic skills of reading, analyzing, and evaluating AI-generated code. By the end, you will have a mental toolkit and a concrete checklist that you can apply to every piece of code an AI writes for you.


7.1 Why Code Reading Is a Superpower

The Reading-to-Writing Ratio

Professional software engineers spend significantly more time reading code than writing it. Studies suggest the ratio is roughly 10:1 — for every line you write, you read ten. In vibe coding, this ratio shifts even more dramatically. The AI writes most of the code; your primary job is to read and evaluate it.

This means that code reading is not a secondary skill. It is the primary skill of a vibe coder.

Intuition

Imagine hiring a contractor to renovate your kitchen. You do not need to know how to install the plumbing yourself, but you absolutely need to be able to look at the finished work and determine whether the pipes are connected properly, whether the materials are quality, and whether the building codes were followed. Reading AI-generated code is the same kind of informed inspection.

What Makes Code Reading Difficult

Reading code is harder than reading prose for several reasons:

  1. Non-linear flow: Code does not execute top-to-bottom like a novel. Functions call other functions, loops repeat, and conditions branch execution in multiple directions.
  2. Implicit context: A single line like user = get_user(id) conceals an enormous amount of behavior — database queries, error handling, data transformation — behind a simple name.
  3. Multiple layers of abstraction: Code operates simultaneously at the level of individual operations, function logic, module architecture, and system design.
  4. Unfamiliar conventions: AI-generated code may use libraries, patterns, or idioms you have not encountered before.

The Cost of Not Reading

What happens when you skip the reading step and just run AI-generated code? In the best case, it works and you move on. But in common cases:

  • You accumulate code you do not understand. When something breaks later, you cannot debug it because you never understood how it worked in the first place. You end up asking the AI to fix the AI's code, creating a cycle of dependency.
  • Bugs slip into production. AI-generated code can look correct at a glance but contain subtle logic errors, missing edge case handling, or security vulnerabilities that only manifest under specific conditions.
  • Technical debt compounds silently. Each piece of unreviewed code is a potential liability. Over time, the codebase becomes a patchwork of patterns you did not choose and cannot maintain.
  • You do not learn. If you never read the code the AI produces, you never internalize the patterns, techniques, and idioms it uses. Your ability to guide the AI effectively stagnates because you cannot evaluate what it gives you.

The time invested in reading AI-generated code is not overhead. It is the core of your value as a developer in the AI era.

The Three Levels of Code Reading

Effective code reading operates at three levels, and you should practice all three:

Level What You're Doing Questions You're Asking
Structural Scanning the overall shape What are the main components? How are they organized?
Logical Tracing the execution flow What happens when this runs? What are the inputs and outputs?
Critical Evaluating quality and correctness Is this correct? Is it secure? Is it efficient? Could it fail?

We will cover each of these levels in the sections that follow. Each level builds on the previous one. You cannot effectively trace logic without first understanding the structure, and you cannot evaluate quality without understanding what the code actually does.

How to Practice Code Reading

Like any skill, code reading improves with deliberate practice. Here are some concrete ways to build the muscle:

  1. Read before you run. When AI generates code, resist the urge to immediately execute it. Spend two minutes reading and predicting what it will do. Then run it and compare your prediction to the actual behavior.
  2. Read other people's code. Browse open-source projects on GitHub. Pick a small utility library and read through its source code, applying the structural analysis technique from section 7.2.
  3. Review your own old code. Go back to code you wrote (or had AI write) a month ago. Can you still understand it? If not, that is a sign the code needs better naming, documentation, or structure.
  4. Explain code out loud. The act of articulating what code does forces a deeper level of understanding than silent reading. Some developers call this "rubber duck debugging" — explaining the code to an inanimate object (or a patient colleague) to clarify their own thinking.

Real-World Application

At companies that use AI coding tools extensively, the ability to quickly read and evaluate AI-generated code is becoming a key interview skill. Engineers are increasingly evaluated not just on their ability to write code, but on their ability to review it critically and identify issues. The techniques in this chapter directly translate to the kind of code review that professional teams perform daily.


7.2 Structural Analysis: The Bird's-Eye View

Before reading a single line of logic, step back and look at the code's structure. This is like looking at the table of contents of a book before reading the chapters.

The Four Structural Elements

Every Python file has four structural layers you should identify:

1. Imports (The Dependencies)

# Standard library imports
import json
import os
from datetime import datetime, timedelta
from pathlib import Path

# Third-party imports
import requests
from rich.console import Console
from rich.table import Table

# Local imports
from models import Task, TaskStatus
from storage import JSONStorage

When you see imports, ask yourself: - What standard library modules are used? This tells you what built-in capabilities the code relies on. - What third-party packages are required? These are external dependencies you will need to install. - Are there local imports? These indicate the code is part of a larger project with multiple files.

2. Constants and Configuration

MAX_TITLE_LENGTH = 100
DEFAULT_PRIORITY = "medium"
DATA_FILE = Path.home() / ".taskmanager" / "tasks.json"
VALID_PRIORITIES = ("low", "medium", "high", "critical")

Constants tell you about the code's configurable boundaries. Look for hardcoded values that might need to change.

3. Classes and Data Structures

class Task:
    """Represents a single task in the task manager."""

    def __init__(self, title: str, priority: str = "medium"):
        self.title = title
        self.priority = priority
        self.created_at = datetime.now()
        self.completed = False

Classes reveal the data model — what entities the code works with and what properties they have.

4. Functions (The Behavior)

def add_task(title: str, priority: str = "medium") -> Task:
    """Create and store a new task."""
    ...

def list_tasks(filter_by: str | None = None) -> list[Task]:
    """Retrieve tasks, optionally filtered by status."""
    ...

def complete_task(task_id: int) -> bool:
    """Mark a task as completed. Returns True if successful."""
    ...

Functions tell you what the code does. Read their names and signatures before diving into their bodies.

Applying Structural Analysis to the Task Manager

Let us apply this to a piece of code similar to what you might have generated in Chapter 6:

"""Task Manager CLI — A simple command-line task management tool."""

import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional

# Constants
DATA_DIR = Path.home() / ".taskmanager"
DATA_FILE = DATA_DIR / "tasks.json"
PRIORITIES = ["low", "medium", "high"]

# Data model
class Task:
    def __init__(self, task_id: int, title: str, priority: str = "medium",
                 due_date: Optional[str] = None):
        self.task_id = task_id
        self.title = title
        self.priority = priority
        self.due_date = due_date
        self.completed = False
        self.created_at = datetime.now().isoformat()

    def to_dict(self) -> dict:
        return vars(self)

    @classmethod
    def from_dict(cls, data: dict) -> "Task":
        task = cls(data["task_id"], data["title"], data.get("priority", "medium"))
        task.completed = data.get("completed", False)
        task.created_at = data.get("created_at", datetime.now().isoformat())
        task.due_date = data.get("due_date")
        return task

# Storage functions
def load_tasks() -> list[Task]:
    if not DATA_FILE.exists():
        return []
    with open(DATA_FILE, "r") as f:
        data = json.load(f)
    return [Task.from_dict(item) for item in data]

def save_tasks(tasks: list[Task]) -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    with open(DATA_FILE, "w") as f:
        json.dump([t.to_dict() for t in tasks], f, indent=2)

# Core operations
def add_task(title: str, priority: str = "medium",
             due_date: Optional[str] = None) -> Task:
    tasks = load_tasks()
    task_id = max((t.task_id for t in tasks), default=0) + 1
    task = Task(task_id, title, priority, due_date)
    tasks.append(task)
    save_tasks(tasks)
    return task

def complete_task(task_id: int) -> bool:
    tasks = load_tasks()
    for task in tasks:
        if task.task_id == task_id:
            task.completed = True
            save_tasks(tasks)
            return True
    return False

def list_tasks(show_completed: bool = False) -> list[Task]:
    tasks = load_tasks()
    if not show_completed:
        tasks = [t for t in tasks if not t.completed]
    return tasks

Your structural analysis should note:

  • Imports: Standard library only (json, sys, datetime, pathlib, typing). No external dependencies needed.
  • Constants: Three constants defining file paths and valid priority levels.
  • Classes: One class (Task) with serialization support (to_dict, from_dict).
  • Functions: Five functions — two for storage (load_tasks, save_tasks) and three for operations (add_task, complete_task, list_tasks).
  • Architecture: Simple single-file design with clear separation between data, storage, and operations.

Best Practice

When reviewing AI-generated code, always start with structural analysis. Before reading any function body, make a mental (or written) map of the file's components. This gives you context that makes the detailed reading much easier.


7.3 Line-by-Line Tracing

Once you understand the structure, it is time to trace through the logic. Line-by-line tracing means mentally executing the code, keeping track of variable values as they change.

The Tracing Technique

Here is a systematic approach to tracing:

  1. Pick a function to trace. Start with the main entry point or the function you are most concerned about.
  2. Define concrete inputs. Do not trace with abstract values. Pick specific, realistic inputs.
  3. Track variable state. Write down (yes, on paper or in a text file) the value of each variable after each line executes.
  4. Follow function calls. When the code calls another function, trace into that function with the specific arguments.
  5. Note the return value. What does the function produce for your chosen inputs?

Tracing Example: The add_task Function

Let us trace through add_task("Buy groceries", "high") assuming there is already one task with task_id=1 in storage:

def add_task(title: str, priority: str = "medium",
             due_date: Optional[str] = None) -> Task:
    # Step 1: title = "Buy groceries", priority = "high", due_date = None

    tasks = load_tasks()
    # Step 2: tasks = [Task(task_id=1, title="Walk the dog", ...)]

    task_id = max((t.task_id for t in tasks), default=0) + 1
    # Step 3: max of [1] is 1, plus 1 = 2. task_id = 2

    task = Task(task_id, title, priority, due_date)
    # Step 4: task = Task(task_id=2, title="Buy groceries",
    #                     priority="high", due_date=None)

    tasks.append(task)
    # Step 5: tasks now has 2 elements

    save_tasks(tasks)
    # Step 6: Both tasks written to disk

    return task
    # Step 7: Returns the new Task with task_id=2

Tracing Edge Cases

Now trace the same function with an empty task list (first task ever added):

def add_task(title: str, priority: str = "medium",
             due_date: Optional[str] = None) -> Task:
    tasks = load_tasks()
    # tasks = [] (empty list, no file exists yet)

    task_id = max((t.task_id for t in tasks), default=0) + 1
    # Generator is empty, so max returns default=0, then 0 + 1 = 1
    # task_id = 1  (correct! First task gets ID 1)

    task = Task(task_id, title, priority, due_date)
    tasks.append(task)
    save_tasks(tasks)
    return task

The default=0 parameter in max() is critical here. Without it, calling max() on an empty sequence would raise a ValueError. This is the kind of detail that tracing helps you discover.

Common Pitfall

When tracing code, beginners often skip edge cases and only trace the "happy path." Force yourself to trace with empty inputs, boundary values, and error conditions. This is where bugs hide.

When Tracing Reveals Hidden Complexity

Tracing is particularly valuable when a function looks simple but has hidden complexity. Consider this function from the task manager:

def complete_task(task_id: int) -> bool:
    tasks = load_tasks()
    for task in tasks:
        if task.task_id == task_id:
            task.completed = True
            save_tasks(tasks)
            return True
    return False

On first reading, this seems straightforward: find the task, mark it complete, save, return success. But tracing reveals important details:

Trace with task_id = 3, tasks = [Task(1, ...), Task(2, ...), Task(3, ...)]:

  1. load_tasks() reads the entire file from disk and deserializes all tasks.
  2. The loop starts. Task 1: task_id == 3? No. Task 2: task_id == 3? No. Task 3: task_id == 3? Yes.
  3. task.completed = True mutates the task object in the tasks list.
  4. save_tasks(tasks) writes all three tasks back to disk, not just the modified one.
  5. Returns True.

Trace with task_id = 99 (nonexistent task):

  1. load_tasks() reads the entire file.
  2. The loop checks every task. None match.
  3. Returns False.

Notice that even for the "not found" case, the entire file is loaded from disk. If you called this function in a loop checking many task IDs, you would read the file each time. The tracing process reveals this performance characteristic that a casual read might miss.

Also notice that save_tasks(tasks) is called inside the loop. This means it writes all tasks including those not yet iterated over. This is fine because return True exits the loop immediately after saving, but if the function were modified to mark multiple tasks complete, the save-inside-loop pattern would need to change.

Building a Tracing Table

For complex functions, a tracing table helps organize your mental execution. Here is the format:

Line Variable Value Notes
1 title "Buy groceries" Parameter
1 priority "high" Parameter
1 due_date None Default parameter
3 tasks [Task(1, ...)] Loaded from disk
5 task_id 2 max(1) + 1
7 task Task(2, "Buy groceries", "high", None) New task object
9 tasks [Task(1,...), Task(2,...)] After append

This technique becomes especially valuable when you encounter code with nested loops, recursive calls, or complex conditional logic.

Intuition

Line-by-line tracing is like being a detective following a suspect through a city. You do not just know the suspect is "somewhere in the city" — you track their exact location at every moment. Similarly, tracing means knowing the exact value of every variable at every step.


7.4 Evaluating Code Quality

Not all code that works is good code. AI-generated code can be functionally correct but still problematic in terms of readability, maintainability, or robustness. Here are the quality indicators to look for.

Naming Quality

Good names are the single most important factor in code readability.

Poor naming (common in AI output):

def proc(d, f=False):
    r = []
    for i in d:
        if f or not i.get("done"):
            r.append(i)
    return r

Good naming:

def filter_tasks(tasks: list[dict], include_completed: bool = False) -> list[dict]:
    filtered = []
    for task in tasks:
        if include_completed or not task.get("completed"):
            filtered.append(task)
    return filtered

Both functions do the same thing, but the second one is instantly understandable. When reviewing AI-generated code, check:

  • Function names: Do they describe what the function does? (Verbs are good: calculate_total, validate_email, send_notification)
  • Variable names: Do they describe what the variable holds? (Nouns are good: user_count, filtered_tasks, error_message)
  • Avoid abbreviations: msg instead of message saves three characters but costs readability across the entire codebase.
  • Boolean names: Should read as true/false questions: is_valid, has_permission, should_retry.

Function Design

Well-designed functions follow several principles:

# BAD: Function does too many things
def handle_task(action, title=None, task_id=None, priority=None,
                show_completed=False, output_format="text"):
    if action == "add":
        # 30 lines of add logic
        ...
    elif action == "delete":
        # 20 lines of delete logic
        ...
    elif action == "list":
        # 25 lines of list logic
        ...
    elif action == "export":
        # 40 lines of export logic
        ...
# GOOD: Each function does one thing
def add_task(title: str, priority: str = "medium") -> Task:
    """Create a new task and save it to storage."""
    ...

def delete_task(task_id: int) -> bool:
    """Delete a task by ID. Returns True if found and deleted."""
    ...

def list_tasks(include_completed: bool = False) -> list[Task]:
    """Return all tasks, optionally including completed ones."""
    ...

def export_tasks(tasks: list[Task], format: str = "json") -> str:
    """Export tasks to the specified format."""
    ...

Check for: - Single Responsibility: Each function should do one thing well. - Reasonable length: Functions over 30-40 lines often do too much. - Clear inputs and outputs: Type hints make this explicit. - Minimal side effects: Functions that modify global state or external resources should make this obvious.

Error Handling

AI-generated code frequently has weak error handling. Compare:

# BAD: No error handling
def load_config(path: str) -> dict:
    with open(path) as f:
        return json.load(f)
# GOOD: Handles realistic failure modes
def load_config(path: str) -> dict:
    """Load configuration from a JSON file.

    Args:
        path: Path to the configuration file.

    Returns:
        Configuration dictionary.

    Raises:
        FileNotFoundError: If the config file does not exist.
        json.JSONDecodeError: If the file contains invalid JSON.
    """
    config_path = Path(path)
    if not config_path.exists():
        raise FileNotFoundError(f"Configuration file not found: {path}")

    try:
        with open(config_path) as f:
            config = json.load(f)
    except json.JSONDecodeError as e:
        raise json.JSONDecodeError(
            f"Invalid JSON in configuration file {path}: {e.msg}",
            e.doc, e.pos
        )

    return config

Best Practice

When evaluating AI-generated error handling, ask: "What happens if this fails?" for every operation that touches the outside world — file I/O, network requests, user input, database queries. If the answer is "the program crashes with an unhelpful error message," the error handling needs improvement.

Documentation Quality

AI tools tend to produce either too much documentation or too little. Here is what good documentation looks like:

def calculate_overdue_tasks(tasks: list[Task],
                            reference_date: datetime | None = None) -> list[Task]:
    """Identify tasks that are past their due date.

    Filters the task list to return only tasks that have a due date
    set and that due date is before the reference date. Completed
    tasks are excluded.

    Args:
        tasks: List of Task objects to filter.
        reference_date: The date to compare against. Defaults to
            the current date and time if not provided.

    Returns:
        A list of overdue, incomplete tasks sorted by due date
        (most overdue first).

    Example:
        >>> tasks = [Task(1, "Report", due_date="2025-01-01")]
        >>> overdue = calculate_overdue_tasks(tasks)
        >>> len(overdue)
        1
    """

A good docstring explains what the function does, what its parameters mean, what it returns, and gives an example. It does not explain obvious implementation details.

Code Structure and Organization

Beyond individual function quality, consider the overall organization of the code:

Logical grouping: Are related functions placed near each other? Are utility functions separated from business logic? In our task manager example, the storage functions (load_tasks, save_tasks) are grouped together, separate from the operation functions (add_task, complete_task, list_tasks). This makes it easy to find what you are looking for.

Consistent abstraction levels: Functions at the same level of the code should operate at similar levels of abstraction. A function that orchestrates high-level operations should not also contain low-level string manipulation. For example:

# BAD: Mixed abstraction levels
def process_tasks(filepath: str) -> None:
    with open(filepath) as f:
        raw = f.read()
    data = json.loads(raw)
    for item in data:
        title = item["title"].strip().lower().replace("  ", " ")
        if len(title) > 0 and item.get("status") != "deleted":
            save_to_database(title, item["priority"])

# GOOD: Each function works at one level
def process_tasks(filepath: str) -> None:
    tasks = load_tasks_from_file(filepath)
    active_tasks = filter_active_tasks(tasks)
    for task in active_tasks:
        save_to_database(task)

The "good" version reads like a high-level description of the process. The details of how tasks are loaded, filtered, and saved are delegated to separate functions. This makes the code much easier to understand, test, and modify.

Common Pitfall

AI-generated code sometimes mixes abstraction levels within a single function because it tries to provide a complete, working solution in one place. When you see a function that reads files, processes data, and writes output all in 30 lines, consider whether it should be split into three functions with clear responsibilities.


7.5 Identifying Common AI Code Patterns

AI code generators have distinctive habits. Recognizing these patterns helps you evaluate AI output more efficiently.

Pattern 1: Over-Commenting

AI models are trained to be helpful, which often translates to excessive commenting:

# This is problematic — comments restate the obvious
def add_numbers(a: int, b: int) -> int:
    # Add the two numbers together
    result = a + b  # Store the sum in result
    # Return the result
    return result  # Returns the sum of a and b

Every comment here restates what the code already says clearly. Good comments explain why, not what:

def calculate_retry_delay(attempt: int) -> float:
    # Exponential backoff with jitter to prevent thundering herd
    base_delay = min(2 ** attempt, 60)
    jitter = random.uniform(0, base_delay * 0.1)
    return base_delay + jitter

Pattern 2: Verbose Solutions

AI often generates more code than necessary, especially when a concise Pythonic solution exists:

# AI-generated verbose version
def get_unique_priorities(tasks: list[Task]) -> list[str]:
    unique = []
    for task in tasks:
        if task.priority not in unique:
            unique.append(task.priority)
    return unique
# Concise Pythonic version
def get_unique_priorities(tasks: list[Task]) -> list[str]:
    return list({task.priority for task in tasks})

The verbose version is not wrong, but it is less idiomatic and harder to scan quickly.

Pattern 3: Unnecessary Wrapper Functions

AI sometimes creates functions that add no value:

# Unnecessary wrapper
def get_string_length(text: str) -> int:
    """Get the length of a string."""
    return len(text)

def is_list_empty(items: list) -> bool:
    """Check if a list is empty."""
    return len(items) == 0

# These add no value over using len() and not directly

Pattern 4: Overly Defensive Code

AI tends to add excessive type checking and validation that Python's duck typing handles naturally:

# Overly defensive
def process_items(items):
    if items is None:
        raise ValueError("items cannot be None")
    if not isinstance(items, list):
        raise TypeError("items must be a list")
    if len(items) == 0:
        return []

    results = []
    for item in items:
        if item is not None:
            if isinstance(item, str):
                results.append(item.strip())
            else:
                results.append(str(item).strip())
    return results
# Appropriately defensive
def process_items(items: list[str]) -> list[str]:
    """Strip whitespace from each item in the list."""
    return [item.strip() for item in items]

The type hints communicate expectations; the code trusts its callers. If validation is needed (for example, at an API boundary), it should be done at the boundary, not inside every function.

Pattern 5: Copy-Paste Variations

When asked for multiple similar functions, AI often generates near-identical code instead of abstracting the common pattern:

# AI-generated copy-paste pattern
def get_high_priority_tasks(tasks: list[Task]) -> list[Task]:
    result = []
    for task in tasks:
        if task.priority == "high":
            result.append(task)
    return result

def get_medium_priority_tasks(tasks: list[Task]) -> list[Task]:
    result = []
    for task in tasks:
        if task.priority == "medium":
            result.append(task)
    return result

def get_low_priority_tasks(tasks: list[Task]) -> list[Task]:
    result = []
    for task in tasks:
        if task.priority == "low":
            result.append(task)
    return result
# Better: One parameterized function
def get_tasks_by_priority(tasks: list[Task], priority: str) -> list[Task]:
    """Filter tasks by priority level."""
    return [task for task in tasks if task.priority == priority]

Common Pitfall

Recognizing these patterns is not about being critical of AI. It is about being a good editor. Think of AI as a brilliant but sometimes verbose first-draft writer. Your job is to polish the draft into clean, maintainable code.


7.6 Spotting Potential Issues

Beyond style and patterns, AI-generated code can contain actual bugs. Here are the most common categories to watch for.

Off-by-One Errors

Off-by-one errors are among the most common bugs in all of programming, and AI is not immune:

# Bug: Should this be < or <=?
def get_page(items: list, page: int, page_size: int = 10) -> list:
    start = page * page_size
    end = start + page_size
    return items[start:end]

# If page is 0-indexed, this is correct.
# If page is 1-indexed (as users expect), this is WRONG.
# Page 1 would return items[10:20] instead of items[0:10].
# Fixed for 1-indexed pages
def get_page(items: list, page: int, page_size: int = 10) -> list:
    """Get a page of items. Pages are 1-indexed."""
    if page < 1:
        raise ValueError("Page number must be 1 or greater")
    start = (page - 1) * page_size
    end = start + page_size
    return items[start:end]

Missing Input Validation

AI code often trusts its inputs too much:

# Missing validation
def set_priority(task: Task, priority: str) -> None:
    task.priority = priority  # What if priority is "ultra-mega-high"?

# With validation
def set_priority(task: Task, priority: str) -> None:
    valid_priorities = ("low", "medium", "high", "critical")
    if priority not in valid_priorities:
        raise ValueError(
            f"Invalid priority '{priority}'. "
            f"Must be one of: {', '.join(valid_priorities)}"
        )
    task.priority = priority

Unhandled Edge Cases

# What happens with an empty list?
def get_most_recent_task(tasks: list[Task]) -> Task:
    return sorted(tasks, key=lambda t: t.created_at, reverse=True)[0]
    # IndexError if tasks is empty!

# Safer version
def get_most_recent_task(tasks: list[Task]) -> Task | None:
    if not tasks:
        return None
    return sorted(tasks, key=lambda t: t.created_at, reverse=True)[0]

Resource Leaks

AI sometimes opens resources without ensuring they are properly closed:

# Resource leak risk
def read_data(path: str) -> dict:
    f = open(path, "r")
    data = json.load(f)
    # If json.load raises an exception, f is never closed!
    f.close()
    return data

# Safe version using context manager
def read_data(path: str) -> dict:
    with open(path, "r") as f:
        return json.load(f)

Silent Failures

Code that fails silently is especially dangerous because problems go undetected:

# Silent failure — bug goes unnoticed
def delete_task(task_id: int) -> None:
    tasks = load_tasks()
    tasks = [t for t in tasks if t.task_id != task_id]
    save_tasks(tasks)
    # No indication whether a task was actually deleted!

# Better — inform the caller
def delete_task(task_id: int) -> bool:
    """Delete a task by ID. Returns True if found and deleted."""
    tasks = load_tasks()
    original_count = len(tasks)
    tasks = [t for t in tasks if t.task_id != task_id]

    if len(tasks) == original_count:
        return False  # Task not found

    save_tasks(tasks)
    return True

Race Conditions in File Operations

When AI generates code that reads, modifies, and writes files, there is often a race condition:

# Race condition: another process could modify the file between
# load_tasks() and save_tasks()
def add_task(title: str) -> Task:
    tasks = load_tasks()        # Step 1: Read
    task = Task(len(tasks) + 1, title)  # Step 2: Create
    tasks.append(task)          # Step 3: Modify
    save_tasks(tasks)           # Step 4: Write
    return task
    # If another process adds a task between Steps 1 and 4,
    # that task will be lost!

For a simple single-user CLI tool, this may be acceptable. For a web application or multi-user system, it is a serious bug.

Advanced

Race conditions are a category of bug that depends on timing, making them notoriously hard to reproduce and debug. For file-based storage, solutions include file locking (fcntl.flock on Unix, msvcrt.locking on Windows) or switching to a database with proper transaction support. For the task manager from Chapter 6, the single-user CLI context means this is a low-priority concern, but you should be aware of the pattern.


7.7 Understanding Dependencies and Imports

Every import statement is a dependency, and dependencies have consequences.

Standard Library vs. Third-Party

# Standard library — always available with Python
import json
import os
import sys
from datetime import datetime
from pathlib import Path

# Third-party — must be installed separately
import requests          # pip install requests
from flask import Flask  # pip install flask
import pandas as pd      # pip install pandas

When reviewing AI-generated code, always check:

  1. Are all third-party packages actually needed? AI sometimes imports packages it does not end up using.
  2. Are the packages well-maintained and reputable? Check the package's PyPI page, GitHub repository, and download statistics.
  3. Are the version requirements specified? A requirements.txt should pin versions.
  4. Could a standard library module do the job? For example, AI might use requests for a simple HTTP call where urllib.request from the standard library would suffice.

Import Organization

PEP 8 specifies import ordering:

# 1. Standard library imports
import json
import os
from datetime import datetime

# 2. Third-party imports
import requests
from rich.console import Console

# 3. Local application imports
from .models import Task
from .storage import load_tasks

AI-generated code often mixes these groups. While not a functional issue, disorganized imports make it harder to quickly assess dependencies.

Checking for Unused Imports

AI frequently includes imports it does not use:

import json
import os           # Never used in the code!
import sys          # Never used in the code!
from typing import Optional, List, Dict, Tuple  # Only Optional is used

Unused imports are clutter. They suggest the AI included them "just in case" or from a template. Remove them.

Best Practice

After receiving AI-generated code, scan the imports and mentally check each one against the code body. If you cannot find where an import is used, it is likely unnecessary. Tools like pylint or flake8 can automate this check with the F401 (unused import) warning.

Understanding Import Depth

The complexity of a dependency matters. Consider these two imports:

# Simple dependency — a small, focused library
from dataclasses import dataclass

# Complex dependency — pulls in a massive framework
from django.contrib.auth.models import User

The second import brings in the entire Django framework as a dependency. Make sure the weight of the dependency is justified by how much you use it.

Evaluating Dependency Health

When AI introduces a third-party package you are not familiar with, take a moment to evaluate it. Here is a quick checklist for assessing a Python package:

  1. PyPI page: Search for the package on pypi.org. Check the description, version history, and download statistics. A package with millions of monthly downloads is generally more trustworthy than one with a few hundred.
  2. Last release date: When was the package last updated? A package that has not been updated in three or more years may be abandoned or incompatible with recent Python versions.
  3. GitHub repository: Does it have a public repository? Check the number of stars, open issues, and whether maintainers are responsive to issues and pull requests.
  4. License: Is the license compatible with your project? MIT and Apache 2.0 are permissive and widely used. GPL requires that derivative works also be open-source, which may conflict with commercial projects.
  5. Dependencies of the dependency: Some packages pull in dozens of their own dependencies. Run pip show <package> to see what it requires. A package that brings in 30 transitive dependencies for a single function is probably not worth the complexity.

Advanced

The Python ecosystem has seen supply chain attacks where malicious packages use names similar to popular packages (a technique called "typosquatting"). For example, requests is legitimate, but reqeusts (note the transposed letters) could be malicious. Always verify that the package name in AI-generated code exactly matches the correct package name. If you are unsure, search for it on pypi.org before installing.


7.8 Assessing Performance Characteristics

You do not need to be a performance expert to spot common performance issues in AI-generated code.

Algorithmic Complexity Basics

The most important performance concept is algorithmic complexity — how the runtime grows as the input size increases.

Complexity Name Example 1,000 items 1,000,000 items
O(1) Constant Dictionary lookup Instant Instant
O(log n) Logarithmic Binary search 10 ops 20 ops
O(n) Linear List scan 1,000 ops 1,000,000 ops
O(n log n) Linearithmic Sorting 10,000 ops 20,000,000 ops
O(n^2) Quadratic Nested loops 1,000,000 ops 1,000,000,000,000 ops

Spotting O(n^2) in Disguise

AI-generated code often hides quadratic complexity inside innocent-looking constructs:

# Hidden O(n^2) — 'in' on a list is O(n), inside a loop that is O(n)
def find_duplicates(items: list[str]) -> list[str]:
    duplicates = []
    for item in items:
        if items.count(item) > 1 and item not in duplicates:
            duplicates.append(item)
    return duplicates
# items.count() is O(n), and 'item not in duplicates' is O(n)
# Total: O(n^2) or worse
# O(n) solution using a dictionary
def find_duplicates(items: list[str]) -> list[str]:
    counts: dict[str, int] = {}
    for item in items:
        counts[item] = counts.get(item, 0) + 1
    return [item for item, count in counts.items() if count > 1]

Repeated I/O Operations

AI code sometimes performs I/O operations inside loops:

# BAD: Reads from disk on every call
def get_task_title(task_id: int) -> str:
    tasks = load_tasks()  # Reads JSON file from disk!
    for task in tasks:
        if task.task_id == task_id:
            return task.title
    return ""

# If called in a loop, this reads the file N times
for task_id in task_ids:
    print(get_task_title(task_id))  # File read on EVERY iteration!
# BETTER: Load once, query many times
def get_task_titles(task_ids: list[int]) -> dict[int, str]:
    tasks = load_tasks()  # Read once
    task_map = {t.task_id: t.title for t in tasks}
    return {tid: task_map.get(tid, "") for tid in task_ids}

String Concatenation in Loops

# BAD: O(n^2) string building
def format_task_list(tasks: list[Task]) -> str:
    result = ""
    for task in tasks:
        result += f"- [{task.task_id}] {task.title}\n"  # Creates new string each time
    return result

# GOOD: O(n) string building
def format_task_list(tasks: list[Task]) -> str:
    lines = [f"- [{task.task_id}] {task.title}" for task in tasks]
    return "\n".join(lines)

Unnecessary Sorting

AI sometimes sorts data when it is not needed, or sorts it multiple times:

# Sorting when only the minimum is needed
def get_oldest_task(tasks: list[Task]) -> Task | None:
    if not tasks:
        return None
    sorted_tasks = sorted(tasks, key=lambda t: t.created_at)  # O(n log n)
    return sorted_tasks[0]

# Better: Just find the minimum — O(n)
def get_oldest_task(tasks: list[Task]) -> Task | None:
    if not tasks:
        return None
    return min(tasks, key=lambda t: t.created_at)

Intuition

Performance assessment at the code review stage is about spotting obviously wasteful patterns, not micro-optimizing every line. If a function processes 10 items, an O(n^2) algorithm is fine. If it processes 10 million items, it matters enormously. Always consider the expected data size.


7.9 Verifying Security Properties

Security issues in AI-generated code deserve special attention because AI models have been trained on vast amounts of code — including insecure code. Here are the most critical security checks.

Hardcoded Secrets

AI sometimes generates placeholder secrets that look real:

# DANGER: Hardcoded credentials
API_KEY = "sk-1234567890abcdef"
DATABASE_URL = "postgresql://admin:password123@localhost/mydb"
SECRET_KEY = "my-super-secret-key-do-not-share"

Secrets should always come from environment variables or a secure configuration system:

import os

API_KEY = os.environ.get("API_KEY")
if not API_KEY:
    raise RuntimeError("API_KEY environment variable is required")

DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///local.db")
SECRET_KEY = os.environ.get("SECRET_KEY")

SQL Injection

When AI generates database code, watch for string interpolation in queries:

# VULNERABLE to SQL injection
def find_user(username: str) -> dict:
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    # An attacker could pass: username = "'; DROP TABLE users; --"

# SAFE: Parameterized query
def find_user(username: str) -> dict:
    query = "SELECT * FROM users WHERE username = ?"
    cursor.execute(query, (username,))

Path Traversal

When code handles file paths based on user input:

# VULNERABLE to path traversal
def read_user_file(filename: str) -> str:
    path = f"/app/uploads/{filename}"
    with open(path) as f:
        return f.read()
    # Attacker could pass: filename = "../../etc/passwd"

# SAFER: Validate the resolved path
def read_user_file(filename: str) -> str:
    base_dir = Path("/app/uploads").resolve()
    file_path = (base_dir / filename).resolve()

    if not str(file_path).startswith(str(base_dir)):
        raise ValueError("Access denied: path traversal detected")

    with open(file_path) as f:
        return f.read()

Unsanitized User Input

Any place where user input is included in output without sanitization is a potential vulnerability:

# VULNERABLE to XSS if used in web context
def create_greeting(name: str) -> str:
    return f"<h1>Welcome, {name}!</h1>"
    # If name = "<script>alert('hacked')</script>", this creates an XSS attack

# SAFE: Escape HTML entities
from html import escape

def create_greeting(name: str) -> str:
    return f"<h1>Welcome, {escape(name)}!</h1>"

Insecure Deserialization

AI sometimes uses pickle or eval for data handling, both of which are dangerous:

# DANGEROUS: pickle can execute arbitrary code
import pickle

def load_data(path: str):
    with open(path, "rb") as f:
        return pickle.load(f)  # Could execute malicious code!

# SAFE: Use JSON for data serialization
import json

def load_data(path: str) -> dict:
    with open(path, "r") as f:
        return json.load(f)  # Only parses data, never executes code

Common Pitfall

AI-generated code often uses eval() or exec() for dynamic behavior. These functions execute arbitrary Python code and should almost never be used with untrusted input. If you see eval() in AI-generated code, treat it as a red flag and look for safer alternatives like ast.literal_eval() for parsing data structures, or a proper parser for more complex needs.

Command Injection

Similar to SQL injection, command injection occurs when user input is passed to operating system commands:

# VULNERABLE to command injection
import subprocess

def ping_host(hostname: str) -> str:
    result = subprocess.run(
        f"ping -c 1 {hostname}",
        shell=True,  # Danger! shell=True enables injection
        capture_output=True,
        text=True,
    )
    return result.stdout
    # Attacker input: hostname = "google.com; rm -rf /"

# SAFE: Use a list of arguments, no shell
def ping_host(hostname: str) -> str:
    # Validate hostname format first
    if not re.match(r"^[a-zA-Z0-9.-]+$", hostname):
        raise ValueError(f"Invalid hostname: {hostname}")

    result = subprocess.run(
        ["ping", "-c", "1", hostname],
        capture_output=True,
        text=True,
    )
    return result.stdout

The key defense is avoiding shell=True in subprocess calls and passing arguments as a list rather than a single string. When shell interpretation is disabled, the attacker's input is treated as a literal argument, not as shell commands.

Sensitive Data in Error Messages

AI-generated code sometimes exposes too much information in error messages:

# BAD: Reveals internal details
def connect_to_database(url: str):
    try:
        conn = create_connection(url)
    except ConnectionError as e:
        raise RuntimeError(
            f"Failed to connect to {url}: {e}"
        )
        # This reveals the database URL (including password!)
        # to anyone who sees the error message

# BETTER: Generic error, details in logs
def connect_to_database(url: str):
    try:
        conn = create_connection(url)
    except ConnectionError as e:
        logger.error("Database connection failed: %s (URL: %s)", e, url)
        raise RuntimeError("Failed to connect to the database. Check logs for details.")

A Security Review Checklist

When reviewing any AI-generated code for security, run through this quick checklist:

  1. Are there any hardcoded secrets, passwords, or API keys?
  2. Is user input ever inserted into SQL queries, shell commands, or file paths without sanitization?
  3. Is eval(), exec(), or pickle.load() used with potentially untrusted data?
  4. Are file operations bounded to expected directories?
  5. Is sensitive data (passwords, tokens) ever logged or printed?
  6. Are HTTP requests made over HTTPS (not HTTP)?
  7. Are error messages revealing internal details (stack traces, file paths, database schemas)?
  8. Are subprocess calls using shell=True with user-controlled input?
  9. Is user input ever rendered in HTML without escaping?

7.10 Building Your Code Review Checklist

Now that you understand the dimensions of code review, it is time to build a systematic checklist you can apply every time you receive AI-generated code.

The Five-Phase Review Process

Phase 1: Structural Scan (30 seconds)

Before reading any logic, answer these questions:

  • [ ] What are the imports? Any unused? Any unexpected third-party dependencies?
  • [ ] What classes are defined? What do they represent?
  • [ ] What functions are defined? What are their responsibilities?
  • [ ] How is the code organized? Is there a clear separation of concerns?
  • [ ] How long is each function? Any suspiciously long functions (50+ lines)?

Phase 2: Logic Trace (2-5 minutes)

Pick the most important function and trace through it:

  • [ ] Trace the happy path with realistic inputs.
  • [ ] Trace with edge cases: empty inputs, None values, boundary values.
  • [ ] Verify return values match the type hints and docstrings.
  • [ ] Check loop termination: will every loop eventually end?
  • [ ] Verify conditional logic: are all branches handled?

Phase 3: Quality Check (1-2 minutes)

Evaluate code quality indicators:

  • [ ] Are names descriptive and consistent?
  • [ ] Do functions follow single responsibility?
  • [ ] Is error handling present for I/O operations, user input, and external calls?
  • [ ] Are docstrings accurate and helpful (not just restating the code)?
  • [ ] Is the code DRY (Don't Repeat Yourself)?

Phase 4: Issue Scan (1-2 minutes)

Look for common problems:

  • [ ] Off-by-one errors in indexing or pagination
  • [ ] Missing input validation at boundaries
  • [ ] Unhandled edge cases (empty collections, None values)
  • [ ] Resource leaks (files, connections opened but not closed)
  • [ ] Silent failures (errors swallowed without logging or reporting)
  • [ ] Race conditions in shared resource access

Phase 5: Security and Performance (1-2 minutes)

Final safety checks:

  • [ ] No hardcoded secrets or credentials
  • [ ] No SQL injection, command injection, or path traversal vulnerabilities
  • [ ] No use of eval(), exec(), or pickle with untrusted data
  • [ ] No obvious O(n^2) or worse algorithms where O(n) alternatives exist
  • [ ] No unnecessary repeated I/O operations
  • [ ] User input is validated and sanitized before use

Real-World Application

Professional code review at major tech companies follows checklists similar to this one. Google's engineering practices documentation, for example, recommends structured review focusing on design, functionality, complexity, naming, comments, and testing. The checklist above is adapted for the specific challenges of reviewing AI-generated code.

Making the Checklist Your Own

The checklist above is a starting point. As you gain experience, you will discover patterns specific to your projects, your AI tool of choice, and your domain. Keep a running document of issues you discover and add them to your personal checklist.

Here are some examples of personalized additions:

  • "Check that all datetime operations handle timezones explicitly" (after being burned by naive datetime bugs)
  • "Verify that any file paths work on both Windows and Unix" (after deploying cross-platform)
  • "Ensure database connections are returned to the pool" (after debugging a connection leak)
  • "Check that the AI did not invent API endpoints that do not exist" (after a hallucination incident)

The Checklist as a Living Document

Your code review checklist should evolve over time. Every bug you find in AI-generated code is an opportunity to add a new check. Every false alarm is an opportunity to refine your criteria.

# Example: A simple checklist tracker
review_log = {
    "date": "2025-03-15",
    "code_source": "Claude",
    "issues_found": [
        {"category": "validation", "severity": "medium",
         "description": "No validation on email format"},
        {"category": "performance", "severity": "low",
         "description": "Sorting when only min was needed"},
    ],
    "checklist_additions": [
        "Check email/URL format validation when user input includes these types"
    ]
}

Best Practice

After every significant code review, spend 30 seconds asking yourself: "Would my checklist have caught this?" If yes, great — your checklist works. If no, add the new check immediately. Over time, your checklist becomes a powerful, personalized safety net.


Putting It All Together: A Complete Review Walkthrough

Let us apply everything we have learned to review a piece of AI-generated code end-to-end. Imagine you asked an AI to add a "search" feature to the task manager from Chapter 6 and received this:

def search_tasks(query, tasks=None, case_sensitive=False):
    if tasks == None:
        tasks = load_tasks()

    results = []
    for i in range(0, len(tasks)):
        task = tasks[i]
        title = task.title
        if not case_sensitive:
            title = title.lower()
            query = query.lower()
        if query in title:
            results.append(task)

    # Sort results by relevance
    sorted_results = []
    for r in results:
        if r.title.startswith(query):
            sorted_results.insert(0, r)
        else:
            sorted_results.append(r)

    return sorted_results

Phase 1: Structural Scan

  • No new imports needed (good).
  • Single function, reasonable length.
  • No type hints or docstring (bad).

Phase 2: Logic Trace

Trace with query = "buy", tasks = [Task(1, "Buy groceries"), Task(2, "Read a book"), Task(3, "Buy birthday gift")]:

  • case_sensitive is False, so query becomes "buy".
  • Loop iterates through all three tasks.
  • Task 1: "buy groceries" contains "buy" -- added.
  • Task 2: "read a book" does not contain "buy" -- skipped.
  • Task 3: "buy birthday gift" contains "buy" -- added.
  • Sorting: "Buy groceries" starts with "buy"? Wait -- startswith is called on the original title (mixed case), but query has been lowered. This is a bug. "Buy groceries".startswith("buy") is False because Python's startswith is case-sensitive.

Phase 3: Quality Check

  • No type hints on parameters or return value.
  • No docstring.
  • Uses range(0, len(tasks)) instead of iterating directly (un-Pythonic).
  • The query variable is mutated inside the loop (lowered on every iteration, which is wasteful but not buggy since lowering an already-lowercase string is idempotent).
  • tasks == None should be tasks is None.

Phase 4: Issue Scan

  • Bug: Case-sensitive comparison in the relevance sort despite case-insensitive search.
  • Bug: query is modified in the loop body. If case_sensitive=True, the first iteration that happens to be case-insensitive would corrupt the query. Actually, on closer reading, the lowercase conversion only happens when case_sensitive is False, so the query mutation is consistent — but it is still poor practice to mutate a parameter.
  • Missing validation: No check for empty query string.
  • Mutable default argument: tasks=None is fine, but tasks=[] would be a classic Python bug. The AI got this right.

Phase 5: Security and Performance

  • No security concerns for a local search function.
  • Performance is O(n) for the search, which is appropriate.
  • The relevance sorting uses insert(0, ...) which is O(n) per insert, making the sort O(n^2) in the worst case. For a small task list this is fine.

The Improved Version

After review, you might ask the AI to produce this improved version:

def search_tasks(
    query: str,
    tasks: list[Task] | None = None,
    case_sensitive: bool = False,
) -> list[Task]:
    """Search tasks by title, returning matches sorted by relevance.

    Tasks whose titles start with the query appear first,
    followed by tasks that contain the query elsewhere.

    Args:
        query: The search string to match against task titles.
        tasks: List of tasks to search. Loads from storage if None.
        case_sensitive: Whether to match case exactly. Default is False.

    Returns:
        List of matching tasks, sorted with prefix matches first.

    Raises:
        ValueError: If query is empty or whitespace-only.
    """
    if not query or not query.strip():
        raise ValueError("Search query cannot be empty")

    if tasks is None:
        tasks = load_tasks()

    search_query = query if case_sensitive else query.lower()

    matches_start: list[Task] = []
    matches_contain: list[Task] = []

    for task in tasks:
        title = task.title if case_sensitive else task.title.lower()
        if title.startswith(search_query):
            matches_start.append(task)
        elif search_query in title:
            matches_contain.append(task)

    return matches_start + matches_contain

This improved version: - Has type hints and a complete docstring. - Validates the query input. - Uses is None instead of == None. - Does not mutate the query parameter. - Fixes the case-sensitivity bug in relevance sorting. - Iterates directly over tasks instead of using range(len(...)). - Separates prefix matches and substring matches cleanly.

Communicating Your Findings

After completing a review, you need to communicate your findings effectively. Whether you are writing feedback for yourself, for a colleague, or asking the AI to fix the issues, clarity matters.

Here is a template for documenting review findings:

REVIEW: search_tasks function
DATE: 2025-03-15
REVIEWER: [Your name]

CRITICAL:
  (none)

HIGH:
  1. Case-sensitivity bug in relevance sorting (line 15)
     - startswith() is called on the original mixed-case title
       but compared against the lowered query
     - Fix: apply the same case normalization to the relevance
       comparison as to the search comparison

MEDIUM:
  2. No input validation on query parameter
     - Empty string or whitespace-only query matches everything
     - Fix: raise ValueError for empty/whitespace queries

  3. Parameter mutation (line 9)
     - query variable is lowered inside the loop body
     - Fix: create a separate search_query variable

LOW:
  4. Missing type hints and docstring
  5. Un-Pythonic iteration with range(len(...))
  6. == None instead of is None

RECOMMENDATION: Ask AI to regenerate with these fixes, or
apply fixes manually. The case-sensitivity bug (item 1) must
be fixed before using this function.

This format makes it easy to prioritize fixes and ensures nothing falls through the cracks. When asking the AI to fix the issues, you can paste this review directly into your prompt for precise, targeted corrections.


Summary

Understanding AI-generated code is a skill that develops with practice. In this chapter, you learned to:

  1. Read structurally by identifying imports, classes, functions, and their organization before diving into details.
  2. Trace logically by mentally executing code with concrete inputs, including edge cases.
  3. Evaluate quality by checking naming, function design, error handling, and documentation.
  4. Recognize AI patterns like over-commenting, verbose solutions, and copy-paste duplication.
  5. Spot bugs including off-by-one errors, missing validation, silent failures, and resource leaks.
  6. Assess dependencies by distinguishing standard library from third-party imports and checking for unused imports.
  7. Check performance by identifying hidden quadratic complexity, unnecessary I/O, and wasteful operations.
  8. Verify security by looking for hardcoded secrets, injection vulnerabilities, and unsafe deserialization.
  9. Apply a checklist systematically, and customize it based on your experience.

The goal is not to become suspicious of AI-generated code, but to become a confident reviewer of it. Every piece of code that passes through your checklist and your understanding is code you can trust. And the more you practice these skills, the faster and more automatic they become.

In the next chapter, we move from reading code to communicating about it: Chapter 8: Prompt Engineering Fundamentals will teach you how to write prompts that generate better code in the first place, reducing the number of issues you need to catch in review.


This chapter's code examples are available in the code/ directory. Run example-01-code-analysis.py to practice structural analysis, example-02-quality-checker.py to automate quality checks, and example-03-review-checklist.py to build your own checklist system.