Introduction
🐍 Hands-On Agentic Coding

Python Developers:
Build Smarter
with AI Agents

From rusty Python fundamentals to shipping real projects with Claude Code — a complete self-paced course built for developers who want to work with AI, not around it.

4
Modules
15
Lessons
4
Quizzes
Replay anytime

What you'll learn

This course is designed for Python developers whose skills may be a bit rusty — and who are ready to pair a refreshed foundation with modern AI-assisted development. You don't need to be an expert. You just need curiosity and a terminal.

🔧
Python Functions
Arguments, defaults, *args, **kwargs, closures — the building blocks of clean Python code.
🏗️
Object-Oriented Python
Classes, instances, inheritance, and why OOP makes your code dramatically more maintainable.
🤖
Claude Code Workflow
Install, configure, and use Claude Code to build entire features from your terminal.
🚀
Ship Real Projects
Build a CLI tool from scratch, debug AI-generated code, and push to GitHub — all in one session.

Course Structure

The course is split into four modules. Start with a Python fundamentals refresher, then dive into agentic coding. Each module ends with a quiz to lock in what you've learned.

01
Python Fundamentals
Functions from first principles. Then OOP from scratch — classes, objects, methods, and inheritance.
4 lessons 2 quizzes
02
Claude Code Basics
What agentic coding is, how Claude Code works, and scaffolding your first project from zero.
2 lessons 1 quiz
03
Building with AI
Prompt patterns that actually work. Reviewing and trusting AI-generated code with confidence.
2 lessons 1 quiz
04
Real Project
Build a complete CLI tool, debug AI mistakes, and ship to GitHub. This is where it all comes together.
2 lessons hands-on lab
📖 Lesson 1.1

Functions Deep Dive

Everything you need to write clean, reusable Python functions — from basics to advanced patterns.

📦 Module 1 · Python Fundamentals
~20 min

Functions are the fundamental unit of reusability in Python. If you want to write code that's maintainable — and that AI agents can work with effectively — you need to understand functions deeply, not just superficially.

The Anatomy of a Function

Let's start from first principles. A function takes inputs, does something, and returns an output. Here's the simplest possible form:

Python
def greet(name): # name is a "parameter" — a variable local to this function message = f"Hello, {name}!" return message # "name" here is an "argument" — the value we pass in result = greet("Ada") print(result) # Hello, Ada!
💡
Parameter vs ArgumentParameters are the variable names in the function definition. Arguments are the actual values you pass when you call the function. Many developers use them interchangeably, but the distinction matters when you're debugging.

Default Parameter Values

You can give parameters default values. When the caller doesn't provide that argument, the default is used automatically.

Python
def greet(name, greeting="Hello"): return f"{greeting}, {name}!" print(greet("Ada")) # Hello, Ada! print(greet("Ada", "Hey")) # Hey, Ada! print(greet(greeting="Hi", name="Turing")) # Hi, Turing!

*args and **kwargs

Sometimes you don't know ahead of time how many arguments a function will receive. Python gives you two powerful tools for this.

Python
# *args captures any number of positional arguments as a tuple def add_all(*numbers): return sum(numbers) print(add_all(1, 2, 3)) # 6 print(add_all(10, 20, 30, 40)) # 100 # **kwargs captures keyword arguments as a dictionary def build_profile(**details): for key, value in details.items(): print(f"{key}: {value}") build_profile(name="Ada", role="Engineer", city="London") # name: Ada # role: Engineer # city: London

Type Hints

Python is dynamically typed — but you can (and should) add type hints. They don't enforce anything at runtime, but they make your code readable, and AI agents like Claude Code use them to generate better suggestions.

Python
def calculate_discount( price: float, discount_pct: float = 0.1, apply: bool = True ) -> float: """Calculate the final price after discount. Args: price: Original price in dollars. discount_pct: Discount as a decimal (0.1 = 10%). apply: Whether to apply the discount at all. Returns: Final price after discount. """ if not apply: return price return price * (1 - discount_pct) print(calculate_discount(100.0)) # 90.0 print(calculate_discount(100.0, 0.25)) # 75.0 print(calculate_discount(100.0, apply=False)) # 100.0
🤖
Agentic Coding TipAlways write docstrings and type hints in your functions. Claude Code reads these to understand what your code does, which means it generates far more accurate suggestions and modifications. Think of docstrings as instructions to your AI partner.

Functions as First-Class Objects

In Python, functions are objects. You can pass them as arguments, store them in variables, and return them from other functions. This is a key concept for writing flexible, powerful code.

Python
def apply_operation(numbers: list, operation) -> list: """Apply a function to each item in a list.""" return [operation(n) for n in numbers] def double(x): return x * 2 def square(x): return x ** 2 nums = [1, 2, 3, 4] print(apply_operation(nums, double)) # [2, 4, 6, 8] print(apply_operation(nums, square)) # [1, 4, 9, 16] # Or use a lambda — an anonymous inline function print(apply_operation(nums, lambda x: x + 10)) # [11, 12, 13, 14]
🧠 Quiz · Lesson 1.2

Functions Quiz

Four questions to check your understanding of Python functions. Select an answer to see instant feedback.

1. What does *args do in a Python function?
def my_func(*args): ...
A
Creates a list of required arguments
B
Collects any number of positional arguments into a tuple
C
Collects keyword arguments into a dictionary
D
Multiplies the arguments together
2. What will this print?
def greet(name, greeting="Hi"): return f"{greeting}, {name}!" print(greet("Alice"))
A
Hello, Alice!
B
greeting, Alice!
C
Hi, Alice!
D
TypeError: missing argument
3. Why should you add docstrings when using Claude Code?
A
Docstrings are required by Python to run the code
B
They make the file size smaller
C
Claude Code reads them to understand intent and generate better code
D
They enforce type checking at runtime
4. What is a lambda function?
A
A function imported from the lambda library
B
A small anonymous function defined inline with a single expression
C
A function that takes no arguments
D
A recursive function that calls itself
0/4
QUESTIONS CORRECT

📖 Lesson 1.3

OOP: Classes & Objects

Object-Oriented Programming from scratch. Build your first class, understand self, and see why OOP makes code easier to maintain and extend.

📦 Module 1 · Python Fundamentals
~25 min

If you've never done OOP before, here's the core idea: instead of writing separate variables and functions, you bundle related data (attributes) and behaviour (methods) together inside a class. A class is a blueprint. An object is a specific thing built from that blueprint.

🏠
Real-World AnalogyThink of a class as an architectural blueprint for a house. The blueprint defines how many rooms, doors, windows — but it's not a house. An object is an actual built house made from that blueprint. You can build many different houses (objects) from the same blueprint (class), each with different paint colours and furniture (attribute values).

Your First Class

Python
class Dog: """Represents a dog with a name and breed.""" def __init__(self, name: str, breed: str): # __init__ is the "constructor" — called when you create a Dog # self refers to this specific Dog instance self.name = name # instance attribute self.breed = breed # instance attribute self.tricks = [] # starts empty for every new dog def learn_trick(self, trick: str) -> None: """Teach this dog a new trick.""" self.tricks.append(trick) print(f"{self.name} learned: {trick}!") def show_off(self) -> str: """Return a summary of all tricks.""" if not self.tricks: return f"{self.name} doesn't know any tricks yet." trick_list = ", ".join(self.tricks) return f"{self.name} can: {trick_list}" # Create two Dog objects from the same class rex = Dog("Rex", "German Shepherd") luna = Dog("Luna", "Border Collie") rex.learn_trick("sit") rex.learn_trick("roll over") luna.learn_trick("fetch") print(rex.show_off()) # Rex can: sit, roll over print(luna.show_off()) # Luna can: fetch

Understanding `self`

self is the first parameter of every method and refers to the specific instance the method is called on. When you call rex.learn_trick("sit"), Python automatically passes rex as self. You don't pass it yourself — Python handles it.

Class vs Instance Attributes

Python
class Counter: # Class attribute — shared by ALL instances total_counters = 0 def __init__(self, start: int = 0): # Instance attribute — unique to each Counter self.count = start Counter.total_counters += 1 def increment(self) -> None: self.count += 1 def __repr__(self) -> str: # __repr__ controls how the object prints return f"Counter(count={self.count})" a = Counter() b = Counter(10) a.increment() a.increment() print(a) # Counter(count=2) print(b) # Counter(count=10) print(Counter.total_counters) # 2 — shared across all instances
🤖
Why OOP matters for agentic codingWhen you describe your project to Claude Code, having clear class structures with good docstrings means the AI understands your architecture. It won't add a User class method to the Database class by mistake. Well-structured OOP code is readable by both humans and AI agents.
📖 Lesson 1.4

OOP: Inheritance

Build on existing classes, extend behaviour, and avoid repeating yourself — the most powerful OOP concept.

📦 Module 1 · Python Fundamentals
~20 min

Inheritance lets you create a new class that builds on an existing one. The new class (child) gets all the attributes and methods of the existing class (parent), and can add new ones or override existing ones.

Basic Inheritance

Python
class Animal: """Base class for all animals.""" def __init__(self, name: str, sound: str): self.name = name self.sound = sound def speak(self) -> str: return f"{self.name} says {self.sound}!" def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name!r})" # Dog inherits from Animal — gets all Animal methods for free class Dog(Animal): """A dog that can also fetch.""" def __init__(self, name: str, breed: str): # super() calls the parent class's __init__ super().__init__(name, sound="woof") self.breed = breed # new attribute only Dogs have def fetch(self, item: str) -> str: # New method — Animals don't have this return f"{self.name} fetched the {item}!" class Cat(Animal): """A cat that overrides speak.""" def __init__(self, name: str): super().__init__(name, sound="meow") def speak(self) -> str: # Override the parent method with different behaviour return f"{self.name} ignores you and says {self.sound} anyway." rex = Dog("Rex", "Labrador") whiskers = Cat("Whiskers") print(rex.speak()) # Rex says woof! print(rex.fetch("ball")) # Rex fetched the ball! print(whiskers.speak()) # Whiskers ignores you... print(isinstance(rex, Animal)) # True — Dog IS an Animal

A Practical Example: AI Tool Hierarchy

Let's see inheritance in action with something relevant to what we'll build — a hierarchy of AI tool classes.

Python
class BaseTool: """Abstract base for all CLI tools.""" def __init__(self, name: str, version: str): self.name = name self.version = version self._running = False def start(self) -> None: self._running = True print(f"▶ {self.name} v{self.version} started") def stop(self) -> None: self._running = False print(f"■ {self.name} stopped") def run(self, command: str) -> str: # Child classes MUST implement this raise NotImplementedError("Subclasses must implement run()") class NoteTool(BaseTool): """A note-taking CLI tool.""" def __init__(self): super().__init__("NoteKeeper", "1.0") self.notes: list[str] = [] def run(self, command: str) -> str: if command.startswith("add:"): note = command[4:].strip() self.notes.append(note) return f"✓ Added: {note}" elif command == "list": return "\n".join( f"{i+1}. {n}" for i, n in enumerate(self.notes) ) or "No notes yet." return f"Unknown command: {command}" tool = NoteTool() tool.start() print(tool.run("add: Learn Python OOP")) print(tool.run("add: Build with Claude Code")) print(tool.run("list")) tool.stop()
⚠️
A Common MistakeForgetting to call super().__init__() in a child class. If you override __init__, always call super().__init__(...) first — otherwise the parent class's initialization code never runs, and your object will be missing attributes.
🧠 Quiz · Lesson 1.5

OOP Quiz

Test your understanding of Python classes and inheritance.

1. What is `self` in a Python class method?
A
A keyword that refers to the class itself
B
A reference to the current object instance
C
A built-in variable that stores class attributes
D
The return value of __init__
2. What does `super().__init__()` do in a child class?
A
Deletes the parent class
B
Creates a new instance of the child class
C
Calls the parent class's constructor to initialize inherited attributes
D
Imports the parent class module
3. What's the difference between a class attribute and an instance attribute?
A
Class attributes are defined with self, instance attributes are not
B
Class attributes are shared by all instances; instance attributes are unique per object
C
There is no difference — both work identically
D
Instance attributes are read-only; class attributes can be modified
4. If `Dog` inherits from `Animal`, which statement is true?
A
isinstance(Dog(), Animal) returns False
B
A Dog object can call methods defined on Animal
C
Dog and Animal share the same __init__
D
Animal is a subclass of Dog
0/4
QUESTIONS CORRECT

📖 Lesson 2.1

What Is Agentic Coding?

The difference between chat-based AI and a true coding agent — and why it changes everything.

📦 Module 2 · Claude Code Basics
~15 min

You've probably used ChatGPT or Claude in a browser to get code suggestions. You paste your function, ask a question, copy the answer back into your editor, run it, hit an error, paste that error back in... and so on.

That workflow has a name: copy-paste development. And it has a fundamental limitation: the AI can't see your project. It only sees what you paste into the chat window.

Chat AI vs Agentic AI
💬
Chat-Based AI
You paste code snippets. It responds with suggestions. Forgets everything when context fills up. Can't run your tests. Doesn't know your project structure.
🤖
Agentic AI (Claude Code)
Lives in your terminal. Reads all your files. Edits them directly. Runs tests. Sees errors. Manages git. Works across your entire codebase.

How Claude Code Works

Claude Code is a command-line tool that you install once and run from your project directory. When you describe a task, it:

  • Reads your files — the entire project structure, not just what you paste
  • Makes changes — directly edits source files, creates new ones, refactors across multiple files
  • Runs commands — executes your tests, sees the failures, fixes the code
  • Manages git — creates commits, opens PRs, with as much or as little autonomy as you allow

Setting Up Claude Code

Terminal
# Install Claude Code via npm npm install -g @anthropic-ai/claude-code # Navigate to your project cd my-python-project # Start Claude Code claude # You're now in an interactive session # Claude Code can see all your files
📋
CLAUDE.md — Your Agent's InstructionsCreate a file called CLAUDE.md in your project root. Claude Code reads this every session. Use it to define your project's conventions: always write tests first, always add docstrings, use specific libraries. Think of it as standing instructions to a new developer who starts every morning with no memory of yesterday.

Permission Modes

You control how much autonomy Claude Code has. By default, it asks for approval before writing files or running commands. You can allow it to proceed automatically — useful for trusted workflows — or keep it in "ask every time" mode when you're learning or working on sensitive code.

Terminal
# Ask before every file write (safest — good for learning) claude --permission-mode ask # Auto-approve safe operations (faster for trusted workflows) claude --permission-mode auto # Plan only — Claude describes what it WOULD do, without doing it claude --plan
🔬 Lab · Lesson 2.2

Your First Project

Scaffold a complete Python project from a single prompt. From empty directory to running code in minutes.

📦 Module 2 · Claude Code Basics
~20 min hands-on

Enough theory. Let's build something. You'll start with an empty directory and end with a running Python project — scaffolded entirely by Claude Code from one prompt.

Step 1: Create the project directory

Terminal
mkdir my-note-keeper && cd my-note-keeper claude

Step 2: Write your scaffolding prompt

The key skill here is writing a precise, structured prompt. Don't just say "make a note app". Describe the architecture you want.

Claude Code Prompt
Scaffold a Python CLI application called "note-keeper" with these requirements: - Package structure with src/note_keeper/ layout - pyproject.toml with project metadata and dependencies: - click for CLI - rich for terminal formatting - A NoteKeeper class in note_keeper/core.py that: - Stores notes as a list of dicts with id, text, and created_at - Has add_note(text), list_notes(), delete_note(id) methods - Includes full type hints and docstrings - A CLI entry point in note_keeper/cli.py using Click - A basic test file in tests/ for the NoteKeeper class - A README.md with setup instructions Use uv for dependency management. Add a CLAUDE.md with: always write tests, always add docstrings, use rich for output.
💡
The Scaffolding MomentWatch what happens next. Claude Code will create a directory tree, write each file, wire them together, and give you something that runs. This is what chat-based AI can't do — it doesn't just suggest code, it builds a working project.

Step 3: Run it

Terminal
# Install dependencies uv sync # Run the CLI uv run note-keeper --help uv run note-keeper add "Learn Python OOP today" uv run note-keeper add "Build something with Claude Code" uv run note-keeper list # Run tests uv run pytest tests/ -v

Step 4: Push to GitHub

Claude Code Prompt
Initialize a git repo, create an initial commit with all files, create a GitHub repository called "note-keeper", and push. Use conventional commits format for the commit message.
🎯
What you just didYou went from zero to a working, tested, version-controlled Python project — without touching your editor once. The OOP and functions you practiced in Module 1 are now exactly what Claude Code used to build the NoteKeeper class. Now you understand both sides of the equation.
🧠 Quiz · Lesson 2.3

Agentic Coding Quiz

Check your understanding of Claude Code and agentic workflows.

1. What is the key advantage of Claude Code over chat-based AI?
A
It generates more creative code
B
It reads your files, runs tests, and edits code directly in your project
C
It works without an internet connection
D
It only supports Python projects
2. What is CLAUDE.md used for?
A
Storing Claude Code's API key
B
Defining which Python version to use
C
Giving the agent standing instructions about project conventions
D
Documenting your project for other developers
3. What does `--plan` mode do in Claude Code?
A
Describes what the agent would do without actually doing it
B
Creates a project management plan in Asana
C
Runs only on files that have been planned for modification
D
Auto-approves all operations
0/3
QUESTIONS CORRECT

📖 Lesson 3.1

Prompting Patterns That Work

The difference between prompts that produce great code and prompts that waste your time.

📦 Module 3 · Building with AI
~15 min

Prompting is a skill. Good prompts produce focused, correct, reviewable code. Vague prompts produce sprawling, hard-to-verify output. Here are the patterns that consistently work.

Pattern 1: Feature-First, Then Plan

Don't start with "write me a function that...". Start by describing the feature goal, then let Claude Code create a plan before writing any code.

❌ Weak Prompt
Write a search function for my notes app.
✅ Strong Prompt
I want to add a search feature to note-keeper. Before writing any code, give me a plan: - Which files will need to change? - What new methods will be added to NoteKeeper? - Will the CLI need a new command? - Are there any edge cases to handle? After I approve the plan, implement it with tests.

Pattern 2: Constrain the Scope

The more constraints you give, the more predictable the output. Unconstrained prompts invite the agent to make assumptions you won't like.

✅ Constrained Prompt
Add a search_notes(query: str) method to the NoteKeeper class in core.py only. Do NOT modify cli.py yet. The method should: - Accept a string query - Return a list of notes where query appears in the text (case-insensitive) - Return an empty list if no matches - Include a docstring and type hints - Add a corresponding test in tests/test_core.py

Pattern 3: Verify Before Commit

Always ask Claude Code to run tests before committing changes. This is the workflow that prevents AI-generated bugs from entering your codebase quietly.

Claude Code Prompt
Run pytest, show me the output, and only commit if all tests pass. If any test fails, fix the failing test before committing.

Pattern 4: Describe Intent, Not Implementation

Tell the agent what you want to achieve, not how to achieve it. Let it choose implementation details — that's what it's good at.

❌ Over-specified
Create a list called results, loop through self.notes with a for loop, check if query is in note['text'].lower(), append matching notes to results, return results.
✅ Intent-First
Filter notes by a case-insensitive substring match on the text field. Return the matching notes. Use whatever Python idiom is most readable.
📖 Lesson 3.2

Reviewing AI-Generated Code

How to read, verify, and trust code you didn't write — without anxiety.

📦 Module 3 · Building with AI
~15 min

One of the most common concerns developers have about AI-generated code: "I'm accepting code I don't fully understand. What if it breaks something?" Here's a structured approach to reviewing AI code with confidence.

The Four-Question Review

For every significant piece of AI-generated code, ask these four questions:

🎯
Does it do what I asked?
Read the code against your original prompt. Does it implement the right behaviour? Did it make any assumptions you didn't specify?
🧪
Do the tests pass?
Always run tests. If there are no tests, ask Claude Code to write them before you accept the code.
💥
What could break?
Think about edge cases: empty inputs, None values, very large inputs, concurrent access. Ask Claude Code to handle the ones that matter.
🔗
What did it change?
Run git diff after every Claude Code session. Review every changed line. AI agents sometimes "helpfully" refactor things you didn't ask about.

Using Git as Your Safety Net

Terminal
# Before any Claude Code session, commit your current state git add -A && git commit -m "chore: checkpoint before AI session" # After the session, review everything that changed git diff HEAD # If something went wrong, roll back completely git reset --hard HEAD # Or cherry-pick just the good commits git log --oneline # find the good commit hash git reset --hard abc1234 # reset to it
⚠️
The "Helpful Refactor" ProblemClaude Code sometimes refactors code it wasn't asked to touch — usually in an attempt to be helpful. This is the most common source of unexpected breakages. After every session, run git diff and check for changes outside the files you specified in your prompt.

Ask Claude Code to Explain Itself

If you don't understand a piece of generated code, just ask. Claude Code can explain any part of the code it wrote — including why it made specific choices.

Claude Code Prompt
Explain this line you just wrote: notes = [n for n in self.notes if query.lower() in n["text"].lower()] Why did you use a list comprehension instead of a for loop? What does the .lower() do, and why is it called twice?
🧠 Quiz · Lesson 3.3

Best Practices Quiz

Prompting and code review — apply what you've learned.

1. Why should you commit your code before a Claude Code session?
A
Claude Code requires a clean git status to run
B
So you can safely roll back if the agent makes unexpected changes
C
To save API costs during the session
D
Committing activates Claude Code's context window
2. What's the best approach when prompting for a new feature?
A
Describe the implementation step by step so Claude follows it exactly
B
Describe the goal and constraints, let the agent plan, then approve before implementing
C
Ask for code first, then ask for tests separately
D
Keep prompts as short as possible to save tokens
3. What should you do if you don't understand code Claude Code generated?
A
Accept it anyway — if tests pass it must be correct
B
Delete it and write it yourself from scratch
C
Ask Claude Code to explain the code and its design choices
D
Post it in Stack Overflow
0/3
QUESTIONS CORRECT

🔬 Lab · Lesson 4.1

Build a CLI Tool

Put it all together. Build a feature-complete CLI application using Claude Code, OOP architecture, and proper tests.

📦 Module 4 · Real Project
~30 min hands-on

This is the capstone lab. You'll extend the note-keeper project from Module 2 into a fully-featured CLI tool. Each feature uses the prompting patterns from Module 3, and the OOP knowledge from Module 1.

Feature 1: Tagging System

Add support for tagging notes. Notes can have multiple tags, and you can filter by tag.

Claude Code Prompt
Plan (don't implement yet) adding a tagging system to NoteKeeper: - Notes should support optional tags as a list of strings - add_note() should accept an optional tags parameter - Add list_by_tag(tag: str) -> list method - The CLI should support: note-keeper add "text" --tag work --tag python - Tests must cover tag filtering Show me your plan, then I'll confirm before you write any code.

Feature 2: Persistence

Notes disappear when the script ends. Fix that with JSON file persistence.

Claude Code Prompt
Add file persistence to NoteKeeper. Requirements: - Default storage at ~/.note-keeper/notes.json - Load notes from file on __init__ - Save notes to file after every modification - Create the directory if it doesn't exist - Handle the case where the file doesn't exist yet (first run) - Keep the existing in-memory API identical — no changes to method signatures - Add tests that use a temporary directory (use Python's tmp_path fixture)

Feature 3: Rich Terminal Output

Claude Code Prompt
Improve the CLI output using the rich library (already installed). For `note-keeper list`, display a rich Table with columns: ID | Text (truncated at 60 chars) | Tags | Created For `note-keeper search`, highlight the matching query term in the results using rich markup. Do not change any code in core.py — only modify cli.py.
🎯
Notice the OOP ConnectionEach feature prompt specifies exactly which class and methods to change. This is only possible because your project has a clear architecture — NoteKeeper handles data, cli.py handles presentation. That's the benefit of the OOP structure you learned in Module 1. Claude Code can work surgically because your code is organized.

Final Step: Ship It

Claude Code Prompt
Run the full test suite. If all tests pass: 1. Update the README with the new features and usage examples 2. Create a git commit for each feature using conventional commits 3. Push all commits to GitHub 4. Create a GitHub release tagged v1.0.0 with a changelog
🔬 Lab · Lesson 4.2

Debugging with AI

A systematic workflow for finding and fixing bugs in AI-generated code — without starting from scratch.

📦 Module 4 · Real Project
~20 min

AI-generated code always has bugs. Not sometimes — always. The question isn't whether there are bugs, it's whether you have a system to find and fix them. Here's that system.

Step 1: Reproduce the Bug

Before involving Claude Code, reproduce the bug yourself. Know exactly what input causes it and what the wrong output is. Without this, you're asking the agent to guess.

Claude Code Prompt
There's a bug in the note search feature. Here's how to reproduce it: 1. Run: note-keeper add "Python is great" 2. Run: note-keeper add "python programming" 3. Run: note-keeper search "Python" Expected: Both notes returned (case-insensitive match) Actual: Only the first note is returned Investigate list_by_tag in core.py and identify the root cause before suggesting any fix.

Step 2: Investigate, Don't Just Fix

Ask Claude Code to find the root cause before it writes any fix. If you skip this step, it may patch the symptom and leave the actual bug in place.

Step 3: Fix with Tests

Claude Code Prompt
Now that we've identified the root cause: 1. Write a failing test that reproduces this exact bug 2. Fix the bug 3. Confirm the test now passes 4. Run the full test suite to make sure nothing else broke 5. Commit with message: "fix: case-insensitive search in list_by_tag"

Step 4: Log for Next Time

Claude Code Prompt
Add structured logging to the search and tag filtering methods using Python's logging module. Log at DEBUG level: - The query/tag received - How many notes matched - Time taken (using time.perf_counter) This will make future bugs easier to trace.
🤖
What's Next From HereYou've covered the full cycle: Python fundamentals → agentic workflow → building → debugging → shipping. From here, explore Claude Code's skills system (reusable slash commands), MCP server integrations, and running Claude Code headless in CI pipelines. The workflow you built here scales to any project size.
🎓 Course Complete

You Did It.

You've gone from rusty Python to shipping real projects with an AI agent. Here's what you've accomplished.

Python Functions
Arguments, defaults, *args, **kwargs, type hints, lambdas — solid foundation rebuilt.
OOP from Scratch
Classes, objects, self, __init__, inheritance, super() — a completely new skill.
Claude Code Workflow
Scaffolding, CLAUDE.md, permission modes, plan mode — the full agentic toolkit.
Shipped a Real Project
A CLI tool with tagging, persistence, rich output, tests, and a GitHub release.

What to Build Next

The workflow you have now applies to any Python project. Some ideas to practice with:

  • A habit tracker CLI — extend your note-keeper with streak tracking and a daily review command
  • A local file search tool — use Python's pathlib and Claude Code to build a ripgrep-style searcher
  • A webhook listener — build a FastAPI endpoint that receives GitHub webhooks and posts to Slack
  • Claude Code skills — turn your best prompts into reusable /commit and /feature slash commands

Going Deeper

  • MCP Servers — connect Claude Code to external APIs like GitHub, Linear, and databases
  • Headless Mode — run Claude Code in CI pipelines without an interactive terminal
  • Hooks — automate pre/post actions like running linters before every commit
🚀
Keep the MomentumThe best thing you can do right now is open a real project — something you've been putting off — and run a Claude Code session on it this week. The workflow is fresh. Use it before the friction of "getting started" returns.
🎉
Course Complete
PYTHON DEVELOPERS: HANDS-ON AGENTIC CODING
🐍
Loading your progress…