🔍 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? (
contactsnotc,search_by_namenotsbn) - 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 ...
}
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.
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 environments —
python -m venvto isolate project dependencies - List comprehensions & generators — more Pythonic ways to process data
- Decorators — functions that modify other functions
- Type hints —
def greet(name: str) -> str:for better code clarity - Unit testing —
pytestor the built-inunittestmodule
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 Modules —
import/exportinstead ofrequire - Destructuring & spread —
const { 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
- Generics —
List<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?
Comments and Documentation
Good code is mostly self-documenting through clear naming. Comments should explain why, not what.
✅ 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.