Skip to main content

🔍 Lesson 9.3: Code Review and Next Steps

You've built a working project. That's a huge accomplishment — seriously. But every program can be improved. In this final lesson, you'll learn to look at your code with fresh eyes, clean it up using professional practices, and discover what to learn next in whichever language you want to pursue further.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Review your own code using a structured checklist
  • Identify common code smells and know how to fix them
  • Refactor code for readability, maintainability, and robustness
  • Write meaningful comments and documentation
  • Understand version control basics (Git) and why it matters
  • Choose your next learning path in Python, JavaScript, or C#

Estimated Time: 45 minutes

📑 In This Lesson

Self-Review Checklist

Before sharing your code with anyone, run through this checklist. Professional developers do this on every pull request — it's a habit worth building now.

✅ The Code Review Checklist

Readability

  • Are variable and function names descriptive? (contacts not c, search_by_name not sbn)
  • Is the code consistently formatted? (indentation, spacing, blank lines between sections)
  • Could someone unfamiliar with the project understand it in 5 minutes?

Correctness

  • Does every feature work as specified in your requirements?
  • Are edge cases handled? (empty list, invalid input, missing file)
  • Does the program exit cleanly without errors?

Robustness

  • Is all user input validated?
  • Are file operations wrapped in try/except (or try/catch)?
  • Does the program handle unexpected input without crashing?

Organization

  • Does each function do one thing?
  • Are related functions grouped together?
  • Is there any duplicated code that could be extracted into a function?
🎓 Instructor Note: Delivery Guidance

In a classroom, the best format for this lesson is a code review workshop. Have students swap their projects with a partner and review each other's code using the checklist. Each reviewer writes 3 things that are done well ("great naming!", "clean error handling") and 3 things that could be improved. This mirrors real-world code review culture. Emphasize that code review is about making the code better, not criticizing the programmer. Frame suggestions as "What if we..." rather than "You did this wrong." If self-paced, have students set their code aside for a day, then come back with fresh eyes.

Common Code Smells

A code smell is a pattern that suggests something could be improved. It's not a bug — the code works — but it makes the code harder to read, maintain, or extend.

Code Smell What It Looks Like How to Fix It
Magic Numbers if len(results) > 10: Use a constant: MAX_DISPLAY = 10
Duplicated Code Same 5 lines in 3 different functions Extract into a helper function
Long Functions A function that's 50+ lines Break into smaller functions
Unclear Names x, temp, data2, do_stuff() Use descriptive names: filtered_contacts
Deep Nesting 4+ levels of indentation Use early returns or extract functions
Silent Failures Empty except: pass Log or display the error
Dead Code Commented-out code, unused functions Delete it (Git remembers everything)

Before and After: Fixing Code Smells

# ❌ BEFORE: Deep nesting
def edit_contact(contacts):
    if len(contacts) > 0:
        name = input("Name to edit: ").lower()
        found = [c for c in contacts if name in c.name.lower()]
        if len(found) > 0:
            contact = found[0]
            new_name = input(f"Name [{contact.name}]: ")
            if new_name:
                contact.name = new_name
                print("Updated!")
            else:
                print("No change.")
        else:
            print("Not found.")
    else:
        print("No contacts.")

# ✅ AFTER: Early returns — flat and readable
def edit_contact(contacts):
    if not contacts:
        print("No contacts."); return

    name = input("Name to edit: ").lower()
    found = [c for c in contacts if name in c.name.lower()]

    if not found:
        print("Not found."); return

    contact = found[0]
    new_name = input(f"Name [{contact.name}]: ")
    contact.name = new_name or contact.name
    print("Updated!")
// ❌ BEFORE: Deep nesting
async function editContact(contacts) {
    if (contacts.length > 0) {
        const name = (await prompt("Name to edit: ")).toLowerCase();
        const found = contacts.filter(c => c.name.toLowerCase().includes(name));
        if (found.length > 0) {
            const contact = found[0];
            const newName = await prompt(`Name [${contact.name}]: `);
            if (newName) {
                contact.name = newName;
                console.log("Updated!");
            } else {
                console.log("No change.");
            }
        } else {
            console.log("Not found.");
        }
    } else {
        console.log("No contacts.");
    }
}

// ✅ AFTER: Early returns — flat and readable
async function editContact(contacts) {
    if (contacts.length === 0) { console.log("No contacts."); return; }

    const name = (await prompt("Name to edit: ")).toLowerCase();
    const found = contacts.filter(c => c.name.toLowerCase().includes(name));

    if (found.length === 0) { console.log("Not found."); return; }

    const contact = found[0];
    const newName = await prompt(`Name [${contact.name}]: `);
    contact.name = newName || contact.name;
    console.log("Updated!");
}
// ❌ BEFORE: Deep nesting
static void EditContact(List<Contact> contacts)
{
    if (contacts.Count > 0)
    {
        Console.Write("Name to edit: ");
        string name = Console.ReadLine()!.ToLower();
        var found = contacts.Where(c => c.Name.ToLower().Contains(name)).ToList();
        if (found.Count > 0)
        {
            var contact = found[0];
            Console.Write($"Name [{contact.Name}]: ");
            string newName = Console.ReadLine()!;
            if (!string.IsNullOrEmpty(newName))
            {
                contact.Name = newName;
                Console.WriteLine("Updated!");
            }
        }
        else { Console.WriteLine("Not found."); }
    }
    else { Console.WriteLine("No contacts."); }
}

// ✅ AFTER: Early returns — flat and readable
static void EditContact(List<Contact> contacts)
{
    if (contacts.Count == 0) { Console.WriteLine("No contacts."); return; }

    Console.Write("Name to edit: ");
    string name = Console.ReadLine()!.ToLower();
    var contact = contacts.FirstOrDefault(c => c.Name.ToLower().Contains(name));

    if (contact == null) { Console.WriteLine("Not found."); return; }

    Console.Write($"Name [{contact.Name}]: ");
    string newName = Console.ReadLine()!;
    if (!string.IsNullOrEmpty(newName)) contact.Name = newName;
    Console.WriteLine("Updated!");
}

💡 The "Early Return" Pattern

Instead of wrapping your entire function in an if block, check for invalid conditions at the top and return early. This keeps your "happy path" code at the shallowest indentation level and makes the function much easier to read.

Refactoring Patterns

Refactoring means changing the structure of code without changing its behavior. The program does the same thing before and after — it's just cleaner.

# ❌ BEFORE: Same "find contact" logic in edit, delete, and search
def edit_contact(contacts):
    name = input("Name: ").lower()
    found = [c for c in contacts if name in c.name.lower()]
    if not found:
        print("Not found."); return
    contact = found[0]
    # ... edit logic ...

def delete_contact(contacts):
    name = input("Name: ").lower()
    found = [c for c in contacts if name in c.name.lower()]
    if not found:
        print("Not found."); return
    contact = found[0]
    # ... delete logic ...

# ✅ AFTER: Extract the shared logic into a helper
def find_contact(contacts, action_name="find"):
    """Find a contact by name. Returns the contact or None."""
    name = input(f"Enter name to {action_name}: ").lower()
    found = [c for c in contacts if name in c.name.lower()]
    if not found:
        print(f"No contact found matching '{name}'.")
        return None
    return found[0]

def edit_contact(contacts):
    contact = find_contact(contacts, "edit")
    if not contact: return
    # ... edit logic (no find code needed!) ...

def delete_contact(contacts):
    contact = find_contact(contacts, "delete")
    if not contact: return
    # ... delete logic (no find code needed!) ...
// ✅ Extract the shared "find contact" logic
async function findContact(contacts, actionName = "find") {
    const name = (await prompt(`Enter name to ${actionName}: `)).toLowerCase();
    const found = contacts.filter(c => c.name.toLowerCase().includes(name));

    if (found.length === 0) {
        console.log(`No contact found matching '${name}'.`);
        return null;
    }
    return found[0];
}

async function editContact(contacts) {
    const contact = await findContact(contacts, "edit");
    if (!contact) return;
    // ... edit logic only — no find code needed ...
}

async function deleteContact(contacts) {
    const contact = await findContact(contacts, "delete");
    if (!contact) return;
    // ... delete logic only — no find code needed ...
}
// ✅ Extract the shared "find contact" logic
static Contact? FindContact(List<Contact> contacts, string actionName = "find")
{
    Console.Write($"Enter name to {actionName}: ");
    string name = Console.ReadLine()!.ToLower();
    var contact = contacts.FirstOrDefault(c => c.Name.ToLower().Contains(name));

    if (contact == null)
        Console.WriteLine($"No contact found matching '{name}'.");
    return contact;
}

static void EditContact(List<Contact> contacts)
{
    var contact = FindContact(contacts, "edit");
    if (contact == null) return;
    // ... edit logic only ...
}

static void DeleteContact(List<Contact> contacts)
{
    var contact = FindContact(contacts, "delete");
    if (contact == null) return;
    // ... delete logic only ...
}

Comments and Documentation

Good code is mostly self-documenting through clear naming. Comments should explain why, not what.

# ❌ BAD: Comments that restate the code
x = x + 1  # Increment x by 1
contacts = []  # Create an empty list
if len(contacts) == 0:  # Check if list is empty

# ✅ GOOD: Comments that explain WHY
# Use lowercase for case-insensitive search
term = search_input.lower()

# Default to "General" because older JSON files may not have this field
category = data.get("category", "General")

# Reuse one HttpClient instance — creating new ones per request
# causes socket exhaustion under load
client = HttpClient()


# ✅ GOOD: Docstrings that describe the function's purpose
def find_contact(contacts, action_name="find"):
    """Search contacts by name and return the first match, or None.

    Args:
        contacts: List of Contact objects to search.
        action_name: Verb to display in the prompt (e.g., "edit", "delete").

    Returns:
        The first matching Contact, or None if not found.
    """
// ❌ BAD: Comments that restate the code
let x = x + 1; // Increment x
const contacts = []; // Create empty array

// ✅ GOOD: Comments that explain WHY
// URLSearchParams handles encoding of special chars like & and #
const params = new URLSearchParams({ q: searchTerm });

// Fall back to empty array — older saved files may not have this field
const categories = data.categories ?? [];

/**
 * Search contacts by name and return the first match.
 * @param {Contact[]} contacts - Array of contacts to search
 * @param {string} actionName - Verb for the prompt ("edit", "delete")
 * @returns {Contact|null} The first match, or null
 */
async function findContact(contacts, actionName = "find") {
    // ...
}
// ❌ BAD: Comments that restate the code
int x = x + 1; // Increment x
var contacts = new List<Contact>(); // Create empty list

// ✅ GOOD: Comments that explain WHY
// PropertyNameCaseInsensitive handles APIs that use camelCase
// while our C# properties use PascalCase
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };

// Nullable because older JSON files may have null for optional fields
public string? Bio { get; set; }

/// <summary>
/// Search contacts by name and return the first match, or null.
/// </summary>
/// <param name="contacts">List of contacts to search.</param>
/// <param name="actionName">Verb for the prompt (e.g., "edit").</param>
/// <returns>The first matching Contact, or null if not found.</returns>
static Contact? FindContact(List<Contact> contacts, string actionName = "find")
{
    // ...
}

✅ The Comment Golden Rule

Good code answers "what?" through naming. Good comments answer "why?"

If you find yourself writing a comment that just restates the code, consider renaming the variable or function instead. A function called find_contact_by_partial_name() doesn't need a comment explaining what it does.

Version Control with Git

If you haven't used Git yet, now is the time to start. Git tracks every change you make to your code, so you can undo mistakes, experiment safely, and share your work.

# Terminal commands — these are the same regardless of language!

# 1. Initialize a Git repository in your project folder
# git init

# 2. Create a .gitignore file to exclude files you don't want tracked
# echo "contacts.json" >> .gitignore    # Don't track user data
# echo "__pycache__/" >> .gitignore     # Python cache
# echo ".env" >> .gitignore             # API keys

# 3. Stage your files
# git add .

# 4. Make your first commit
# git commit -m "Initial commit: Contact Manager v1.0"

# 5. After making changes:
# git add .
# git commit -m "Add search by category feature"

# 6. View your history:
# git log --oneline

# 7. Undo a mistake (before committing):
# git checkout -- filename.py

# Why Git matters:
# - You can experiment freely — if it breaks, just revert
# - Your commit history IS your documentation
# - It's required for contributing to open source
# - Every job posting expects Git knowledge
// Terminal commands — same as Python and C#!

// 1. Initialize Git
// git init

// 2. Create .gitignore
// echo "contacts.json" >> .gitignore   // Don't track user data
// echo "node_modules/" >> .gitignore   // npm packages (BIG folder!)
// echo ".env" >> .gitignore            // API keys

// 3. Stage and commit
// git add .
// git commit -m "Initial commit: Contact Manager v1.0"

// 4. After changes:
// git add .
// git commit -m "Add search by category feature"

// 5. View history:
// git log --oneline

// Important for JavaScript:
// ALWAYS .gitignore node_modules/
// It can contain thousands of files — never commit it
// Others will run 'npm install' to recreate it
// Terminal commands — same as Python and JavaScript!

// 1. Initialize Git
// git init

// 2. Create .gitignore (dotnet has a built-in template!)
// dotnet new gitignore
// echo "contacts.json" >> .gitignore   // Don't track user data

// 3. Stage and commit
// git add .
// git commit -m "Initial commit: Contact Manager v1.0"

// 4. After changes:
// git add .
// git commit -m "Add search by category feature"

// 5. View history:
// git log --oneline

// The dotnet gitignore template automatically excludes:
// - bin/ and obj/ folders (build output)
// - .vs/ folder (Visual Studio settings)
// - *.user files (personal IDE settings)

💡 Commit Messages That Help Future You

  • "fixed stuff" — What stuff? How?
  • "update" — Updated what?
  • "Add search by category feature" — Clear and specific
  • "Fix crash when contacts.json is empty" — Describes the bug fixed
  • "Refactor find logic into helper function" — Describes the improvement

What to Learn Next

You've learned the fundamentals that are shared across all three languages. Now it's time to go deeper in whichever language excites you most. Here's a roadmap for each.

flowchart TD A["You Are Here:
Programming Fundamentals"] --> B["🐍 Python Path"] A --> C["⚡ JavaScript Path"] A --> D["🔷 C# Path"] B --> B1["Web: Flask/Django"] B --> B2["Data: Pandas/NumPy"] B --> B3["AI/ML: TensorFlow"] B --> B4["Automation: Scripts"] C --> C1["Frontend: React/Vue"] C --> C2["Backend: Node/Express"] C --> C3["Full-Stack: Next.js"] C --> C4["Mobile: React Native"] D --> D1["Web: ASP.NET Core"] D --> D2["Desktop: WPF/MAUI"] D --> D3["Games: Unity"] D --> D4["Cloud: Azure"] style A fill:#f59e0b,color:#fff style B fill:#3b82f6,color:#fff style C fill:#f59e0b,color:#000 style D fill:#6366f1,color:#fff

🐍 Python: Next Steps

Core Skills to Add

  • Virtual environmentspython -m venv to isolate project dependencies
  • List comprehensions & generators — more Pythonic ways to process data
  • Decorators — functions that modify other functions
  • Type hintsdef greet(name: str) -> str: for better code clarity
  • Unit testingpytest or the built-in unittest module

Career Paths

  • Web Development: Flask (small apps) → Django (large apps) → REST APIs
  • Data Science: Pandas → NumPy → Matplotlib → Jupyter Notebooks
  • AI / Machine Learning: scikit-learn → TensorFlow or PyTorch
  • DevOps / Automation: Scripts, cron jobs, system administration tools

⚡ JavaScript: Next Steps

Core Skills to Add

  • Async/await deep dive — Promises, Promise.all, error handling patterns
  • ES Modulesimport/export instead of require
  • Destructuring & spreadconst { name, email } = contact;
  • TypeScript — JavaScript with types, used by most professional teams
  • npm ecosystem — package.json, dependencies, scripts

Career Paths

  • Frontend: HTML/CSS → React or Vue → Next.js or Nuxt
  • Backend: Node.js → Express → databases (PostgreSQL, MongoDB)
  • Full-Stack: Combine frontend + backend → deploy to Vercel, Netlify, or Railway
  • Mobile: React Native (iOS & Android from one codebase)

🔷 C#: Next Steps

Core Skills to Add

  • LINQ — powerful collection queries: contacts.Where(c => c.Age > 18).OrderBy(c => c.Name)
  • Async/await patterns — Task-based asynchronous programming
  • GenericsList<T>, creating your own generic classes
  • Interfaces — contracts that classes must implement
  • NuGet packages — C#'s package manager (like pip or npm)

Career Paths

  • Web Development: ASP.NET Core → Razor Pages → Blazor → REST APIs
  • Desktop Apps: WPF (Windows) → .NET MAUI (cross-platform)
  • Game Development: Unity (C# is Unity's primary language)
  • Cloud / Enterprise: Azure → microservices → enterprise applications

Recommended Resources

Resource Type Best For
freeCodeCamp Free courses JavaScript, Python, web development
The Odin Project Free curriculum Full-stack JavaScript / Ruby
CS50 (Harvard) Free course Computer science fundamentals
Exercism.org Practice problems All three languages — mentored exercises
LeetCode / HackerRank Coding challenges Algorithm practice, interview prep
Microsoft Learn Free tutorials C#, .NET, Azure — official and excellent
MDN Web Docs Reference JavaScript — the best reference on the web
Real Python Tutorials Python — in-depth articles on every topic
GitHub Portfolio + community Host your projects, contribute to open source

✅ The #1 Way to Improve: Build Things

Tutorials teach you syntax. Building projects teaches you programming. After this course, pick a project that interests you and build it. Get stuck. Google it. Fix it. That cycle — build, break, fix — is how every professional developer learned their craft. Your capstone project is proof that you can do it.

Course Summary

🎓 What You've Accomplished

Over 27 lessons and 9 modules, you've gone from "Hello, World!" to building a complete, data-persistent application. Here's everything you now know:

  • Module 1: Set up three development environments and wrote your first programs
  • Module 2: Mastered variables, data types, and type conversion
  • Module 3: Controlled program flow with conditionals, loops, and logical operators
  • Module 4: Organized code into reusable functions with parameters, return values, and scope
  • Module 5: Stored and processed data with arrays, dictionaries, and iteration
  • Module 6: Modeled real-world concepts with classes, inheritance, and polymorphism
  • Module 7: Built robust code with error handling, custom exceptions, and debugging
  • Module 8: Connected to the outside world with file I/O, JSON, and API requests
  • Module 9: Planned, built, and reviewed a complete capstone project

🌟 The Three-Language Advantage

By learning three languages simultaneously, you gained something most beginners don't have: language-agnostic thinking. You know that a for loop is a for loop whether it uses Python's for x in items:, JavaScript's for (let x of items), or C#'s foreach (var x in items). The syntax is different, but the concept is identical.

This means picking up a fourth language — Java, Go, Rust, TypeScript, Ruby, Swift — will be dramatically easier. You already know the concepts. You just need to learn the syntax. That's a superpower.

🚀 Your Journey Continues

This course gave you the foundation. What you build on it is up to you. Whether you become a web developer, a data scientist, a game designer, or an automation wizard, the fundamentals you learned here will serve you in every line of code you write.

Thank you for completing Programming Fundamentals: Three Language Approach. Now go build something amazing. 🎉

🎯 Final Reflection

Question 1: What should code comments explain?

Question 2: What is refactoring?

Question 3: What's the biggest advantage of learning three languages at once?