TechByteByByte

Python for Modern AI Development

Learn how embeddings, RAG, vector databases, agents, tool calling, structured outputs, and Pydantic validation are built from core Python concepts, and where LangChain, LangGraph, and MCP fit in.

#Python#RAG#AI Agents#Pydantic#LangChain#LangGraph#MCP#Python for AI

The problem: A model is only one component of an AI application. Python still has to prepare input, retrieve evidence, call the model, validate its output, control tools, preserve state, handle failure, and return a result.

What you will learn: This capstone connects your Python skills to embeddings, RAG, vector databases, agents, tool calling, structured outputs, Pydantic, LangChain, LangGraph, and MCP. Nothing needs to remain unexplained magic: you will see what ordinary Python is doing underneath and where each abstraction begins, so you can choose frameworks deliberately.


1. Python and Machine Learning Libraries

An AI application is larger than its model. Python commonly coordinates the parts around that model:

user input
    ↓ validate
retrieve data or call tools
    ↓ construct model input
model API or local model
    ↓ validate output
store, evaluate, observe, and show result

Libraries hide difficult implementation details, but they do not remove the need to understand data shapes, failures, security, and evaluation. This module connects familiar Python building blocks to each box in that flow before it introduces frameworks.

You won’t train models from scratch in this course — that’s a separate, much deeper topic. But you now have exactly the Python foundation (functions, classes, NumPy, exception handling) that libraries like PyTorch and scikit-learn are built on top of. When you see PyTorch code like:

import torch

x = torch.tensor([1.0, 2.0, 3.0])
y = x * 2

This tensor is similar to a NumPy array, but it is not literally a NumPy array. It has familiar shapes and vector operations plus features such as accelerator support and automatic differentiation, which training uses to calculate how parameters should change.

🤖 The takeaway: you’re not starting from zero when you eventually open a PyTorch tutorial — the array/vector mental model transfers directly.


2. Python and Embeddings

Meaning Represented as Numbers

An embedding is a list of numbers (a vector) that represents the meaning of a piece of text — produced by a model trained so that similar meanings end up as similar (nearby) vectors.

# Calling an embedding API (conceptually — actual SDK call shown for real use)
import numpy as np

def fake_embed(text):
    """Stand-in for a real embedding API call — returns a small vector."""
    # A real call looks like:
    # response = client.embeddings.create(model="text-embedding-3-small", input=text)
    # return response.data[0].embedding
    np.random.seed(len(text))   # deterministic "fake" embedding for this demo
    return np.random.rand(8)

embedding = fake_embed("Python is great for AI")
print(embedding.shape)
print(embedding)

Expected Output (deterministic given the fake seed):

(8,)
[0.29399155 0.90340192 0.30224445 0.52975037 0.19311129 0.20549821
 0.98815385 0.35836585 ...]

Connect Embeddings to NumPy Vectors

Recall Module 9: an embedding is a NumPy array. “Finding relevant documents” is computing cosine similarity between vectors — the exact cosine_similarity() function you already wrote.

🤖 How Is This Used in AI? Every semantic search, every RAG system, every recommendation engine built on text starts here: text → embedding model → vector → stored and compared using the vector math from Module 9.


3. Python and RAG (Retrieval-Augmented Generation)

What Is It?

RAG means: retrieve relevant information first, then ask the model to answer using that retrieved information — instead of relying purely on what the model already “knows” from training.

Why Does It Exist?

LLMs have a training cutoff and can’t know about your private documents, recent events, or company-specific data. RAG bridges that gap by injecting relevant, current, specific information directly into the prompt.

Here is the visual architecture of a standard RAG pipeline, separating offline ingestion from online query retrieval:

graph TD
    subgraph Offline["Data Ingestion (Offline)"]
        docs[Raw Docs] --> chunk[Chunking]
        chunk --> embed[Embedding Model]
        embed --> vecDB[(Vector DB)]
    end

    subgraph Online["Query Retrieval & Generation (Online)"]
        query[User Query] --> queryEmbed[Embed Query]
        queryEmbed --> search[Similarity Search]
        vecDB -.-> search
        search --> retrieve[Top-K Chunks]
        retrieve --> prompt[Augmented Prompt]
        query --> prompt
        prompt --> LLM[LLM Generation]
        LLM --> answer[Final Answer]
    end

A rough RAG pipeline, built entirely from what you already know

import numpy as np

documents = [
    "Python is widely used for building AI applications.",
    "RAG combines document retrieval with LLM generation.",
    "Bananas are a good source of potassium.",
]

def fake_embed(text):
    np.random.seed(abs(hash(text)) % (10 ** 6))
    return np.random.rand(8)

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# 1. Embed all documents once (recall Module 9 + Module 4 - functions)
doc_embeddings = [(doc, fake_embed(doc)) for doc in documents]

# 2. Embed the incoming query
query = "How is Python used in AI?"
query_embedding = fake_embed(query)

# 3. Retrieve: rank documents by similarity (Module 9's dot product / Module 4's sorted+lambda)
ranked = sorted(
    doc_embeddings,
    key=lambda pair: cosine_similarity(query_embedding, pair[1]),
    reverse=True,
)
top_docs = [doc for doc, _ in ranked[:2]]

# 4. Build the augmented prompt (Module 1's f-strings)
context = "\n".join(top_docs)
prompt = f"""Answer the question using only this context.

Context:
{context}

Question: {query}
"""

print(prompt)
# 5. Generate: send `prompt` to an LLM API (Module 11) to get the final answer

Expected Output (shape, exact ranking may vary with the fake hash-based embedding):

Answer the question using only this context.

Context:
Python is widely used for building AI applications.
RAG combines document retrieval with LLM generation.

Question: How is Python used in AI?

🧠 The Big Realization

Every step of this pipeline is a concept from an earlier module:

RAG stepPython conceptModule
Store documentsLists / dicts2
Embed textNumPy arrays9
Rank by relevancesorted() + lambda + dot product4, 9
Build final promptf-strings1
Call the modelHTTP / SDK call11
Handle failurestry/except, retries6, 11

RAG isn’t a mysterious new discipline — it’s a specific arrangement of everything you already know.


4. Python and Vector Databases

What Is It?

A vector database is a specialized system for storing millions (or billions) of embeddings and finding the closest matches to a query extremely fast — far faster than the manual “compare every document” loop above, which becomes impractically slow at scale.

# Conceptual shape of working with a vector DB client (e.g. Pinecone, Chroma, Weaviate)
# results = vector_db.query(vector=query_embedding, top_k=3)
# for match in results:
#     print(match["score"], match["metadata"]["text"])

🧠 Intuition: Recall Module 3’s nested-loop query-matching example — comparing every query against every document with plain Python. A vector database does conceptually the same thing (find nearest vectors), but with specialized indexing (like HNSW or IVF — beyond this course’s scope) that makes it fast at massive scale, instead of the O(N × M) slowdown nested loops hit.

🤖 How Is This Used in AI? The documents list in our mini-RAG example above would, in a real system, be millions of chunks stored in a vector database — with .query() replacing our manual sorted(...) ranking step.


5. Python and AI Agents

What Is It?

An AI agent is a system where an LLM doesn’t just answer once — it decides what to do next, potentially calling tools, checking results, and looping until it has a satisfactory final answer.

Here is the visual cycle of a ReAct (Reasoning + Action) agent loop, running iteratively until a final answer is resolved:

graph TD
    Start([Receive User Goal]) --> Thought["Thought: Analyze state & history"]
    Thought --> Action["Action: Decide next step (call tool / respond)"]
    Action --> Call{Is it a tool call?}
    Call -->|Yes: Tool Call| Exec[Execute Tool]
    Exec --> Observation[Observation: Gather tool output]
    Observation --> Thought
    Call -->|No: Final Answer| Done([Return Answer to User])

The core agent loop, built from what you know

def run_agent(question, tools, max_steps=3):
    history = [{"role": "user", "content": question}]   # Module 2: list of dicts

    for step in range(max_steps):                        # Module 3: for loop
        # Ask the model what to do next (simplified — normally an actual LLM call)
        decision = decide_next_action(question, step)     # Module 4: function

        if decision["action"] == "final_answer":          # Module 3: if/elif
            return decision["text"]

        elif decision["action"] == "use_tool":
            tool_name = decision["tool"]
            if tool_name in tools:
                try:                                       # Module 6: exceptions
                    result = tools[tool_name](decision["input"])
                    history.append({"role": "tool", "content": result})
                except Exception as e:
                    history.append({"role": "tool", "content": f"Error: {e}"})

    return "Agent reached max steps without a final answer."

def decide_next_action(question, step):
    """Stand-in for a real LLM call that decides the agent's next move."""
    if step == 0:
        return {"action": "use_tool", "tool": "search", "input": question}
    return {"action": "final_answer", "text": f"Final answer for: {question}"}

tools = {
    "search": lambda q: f"search results for '{q}'",
}

print(run_agent("What is retrieval-augmented generation?", tools))

Expected Output:

Final answer for: What is retrieval-augmented generation?

🧠 The Big Realization

This loop is an agent, at its conceptual core: a while/for loop (Module 3), a growing list of messages (Module 2), functions representing tools (Module 4), try/except around tool calls that might fail (Module 6). Real agent frameworks add a lot of polish — but the skeleton is exactly this.


6. Python and Tool Calling

What Is It?

Tool calling (also called “function calling”) is how an LLM API lets the model itself decide which function to call and with what arguments — your code executes the actual function, then sends the result back.

# The shape of a tool definition sent to an LLM API
tool_definition = {
    "name": "get_weather",
    "description": "Get the current weather for a location",
    "input_schema": {
        "type": "object",
        "properties": {
            "location": {"type": "string", "description": "City name"}
        },
        "required": ["location"],
    },
}

def get_weather(location: str) -> str:
    return f"The weather in {location} is sunny, 22°C."

# Conceptual flow:
# 1. Send `tool_definition` + the user's question to the model
# 2. Model responds: "call get_weather with location='Paris'"
# 3. Your code actually runs: get_weather("Paris")
# 4. Send the result back to the model for a final natural-language answer

tool_call_from_model = {"name": "get_weather", "input": {"location": "Paris"}}

if tool_call_from_model["name"] == "get_weather":
    result = get_weather(**tool_call_from_model["input"])   # Module 4: **kwargs unpacking!
    print(result)

Expected Output:

The weather in Paris is sunny, 22°C.

🧠 Notice get_weather(**tool_call_from_model["input"]) — this is exactly Module 4’s **kwargs unpacking, used to call a real Python function using arguments the model itself decided on.


7. Python and Structured Outputs

What Is It?

Instead of letting an LLM return free-form text, you ask it to return data in a specific, guaranteed shape — a JSON object matching a schema you define — so your code can reliably use the result without fragile text-parsing.

# Without structured output — fragile, must parse free text
raw_response = "The sentiment is positive, with a confidence of about 85%."
# Parsing this reliably in code is error-prone and brittle.

# With structured output — the model returns exactly this shape:
structured_response = {
    "sentiment": "positive",
    "confidence": 0.85,
}
print(structured_response["sentiment"], structured_response["confidence"])

Expected Output:

positive 0.85

🤖 How Is This Used in AI? Structured outputs are essential the moment your code needs to act on a model’s response programmatically (routing, storing in a database, triggering another function) rather than just displaying text to a human.


8. Python and Pydantic

What Problem Pydantic Solves

An LLM can return JSON that’s shaped correctly but contains invalid values — a confidence of "very high" instead of a number, or a missing required field. Pydantic validates that structured output actually matches what your code expects, before your application trusts it.

from pydantic import BaseModel, ValidationError
from typing import Optional

class SentimentResult(BaseModel):
    sentiment: str
    confidence: float
    flagged: Optional[bool] = None

# Valid data — works fine
valid_data = {"sentiment": "positive", "confidence": 0.85}
result = SentimentResult(**valid_data)
print(result)

# Real API response validation from a raw JSON string
json_string = '{"sentiment": "positive", "confidence": 0.85}'
result_from_string = SentimentResult.model_validate_json(json_string)
print(f"Parsed text string successfully: {result_from_string.sentiment}")

# Invalid data from a misbehaving model — Pydantic catches it immediately
invalid_data = {"sentiment": "positive", "confidence": "very high"}
try:
    SentimentResult(**invalid_data)
except ValidationError as e:
    print("Validation failed:", e.errors()[0]["msg"])

Expected Output:

sentiment='positive' confidence=0.85 flagged=None
Parsed text string successfully: positive
Validation failed: Input should be a valid number, unable to parse string as a number

🧠 Intuition

Recall Module 10’s @dataclass — Pydantic’s BaseModel is that same idea, plus automatic validation. A dataclass trusts you; a Pydantic model checks you.

The realistic flow

LLM

Structured JSON (the model's raw output)

Pydantic model (SentimentResult(**json_data))

Validated Python object (guaranteed correct types)

Application logic (safe to use without extra checks)

🤖 Why this is much safer than blindly trusting LLM output: Models occasionally produce malformed or unexpected output — a missing field, a wrong type, an out-of-range value. Pydantic turns “hope the model got it right” into “guaranteed correct, or a clear error you can catch and handle” (recall Module 6’s exception handling, applied here to validation errors specifically).


9. Python and LangChain

Now that you’ve built a rough retriever, a rough agent loop, and rough tool-calling by hand, LangChain will look familiar rather than magical.

# Conceptual LangChain shape — NOT executable without installation/setup
# from langchain_anthropic import ChatAnthropic
# from langchain_core.prompts import ChatPromptTemplate
#
# model = ChatAnthropic(model="claude-sonnet-4-6")
# prompt = ChatPromptTemplate.from_template("Answer using context: {context}\n\n{question}")
# chain = prompt | model
# response = chain.invoke({"context": "...", "question": "..."})

🧠 What LangChain actually is, underneath: classes wrapping API calls (your LLMClient from Module 5), functions chained together (your pipeline functions from Module 4), and structured data flowing between them (your dicts and Pydantic models). LangChain organizes and standardizes these patterns across many providers and use cases — it doesn’t invent new concepts you haven’t already touched.

This is exactly the promised connection from earlier in the course: “This is exactly the kind of Python code that frameworks such as LangChain/LangGraph help organize.”


10. Python and LangGraph

Where LangChain often chains steps linearly, LangGraph models an application as a graph of steps, with explicit branching, loops, and state — a more structured version of the run_agent() loop you wrote by hand in Section 5.

# Conceptual shape — NOT executable without installation/setup
# from langgraph.graph import StateGraph
#
# graph = StateGraph(AgentState)
# graph.add_node("retrieve", retrieve_documents)
# graph.add_node("generate", generate_answer)
# graph.add_edge("retrieve", "generate")

🧠 Intuition: Your run_agent() loop’s if/elif branching (Module 3) and growing history list (Module 2) are a graph of steps, just written by hand instead of declared explicitly as nodes and edges. LangGraph makes that structure visible and manageable as agent logic grows more complex than a simple loop can cleanly express.


11. Python and MCP

MCP (Model Context Protocol) standardizes how an AI application connects to external tools and data sources — instead of every app writing custom integration code for every tool, MCP defines a common protocol both sides agree to speak.

graph LR
    subgraph Host Application ["Host Application (e.g. Claude Desktop, Cursor)"]
        Client[MCP Client]
    end

    subgraph Protocol ["Protocol (JSON-RPC)"]
        direction LR
        Client <-->|Standard Messages| Server[MCP Server]
    end

    subgraph External Tools ["External Resources"]
        Server <--> Filesystem[(Local Filesystem)]
        Server <--> Database[(Postgres / sqlite)]
        Server <--> Search[Web Search API]
    end

🧠 Intuition: Recall Module 5’s BaseTool class with a shared run() interface, and Module 11’s HTTP request/response mechanics. MCP is essentially that same idea — a shared, standardized interface for tools — applied consistently across the whole AI ecosystem, so a tool built for one AI application can work with any MCP-compatible client without custom glue code. Instead of writing custom API connection clients for GitHub, Slack, and your local database, you run standard MCP servers that the host application’s client queries natively using JSON-RPC requests.


12. Putting Everything Together

The full picture, one more time, now completely understood

User

Python application                    (Modules 1–5: variables, functions, classes)

Environment variables                  (Module 8: .env, secure API keys)

API call                               (Module 11: HTTP, headers, JSON body)

JSON response                          (Module 7: JSON ↔ dict)

Pydantic validation                    (Module 14: BaseModel, guaranteed-safe data)

Data processing                        (Modules 2, 4, 9: collections, functions, NumPy/Pandas)

Async operations                       (Module 13: concurrent calls, if there are many)

Logging                                (Module 12: what happened, safely recorded)

AI/LLM                                 (the actual model, called via your Python code)

Final response

Where each concept lives in this flow

  • Variables & data types (1) — configuration: model name, temperature, thresholds.
  • Collections (2) — documents, chat history, search results as lists/dicts.
  • Control flow (3) — deciding relevance, routing agent actions, retrying.
  • Functions (4) — every discrete pipeline step: clean, embed, search, generate.
  • OOP (5) — model wrapper classes, tool classes, an Agent or RAGSystem composed of parts.
  • Exceptions (6) — surviving API failures, invalid input, timeouts.
  • Files & JSON (7) — loading documents, saving results, the literal shape of every API request/response.
  • Modules & environments (8) — organizing code, securely holding API keys.
  • NumPy & Pandas (9) — embeddings, similarity math, evaluation datasets.
  • Advanced Python (10) — generators streaming tokens, decorators wrapping retries, dataclasses/Pydantic-adjacent structure.
  • APIs (11) — the actual mechanism of calling any AI model at all.
  • Logging (12) — the only record of what a production AI service did.
  • Async (13) — running many slow AI/tool calls concurrently instead of one at a time.
  • This module (14) — the AI-specific concepts (RAG, agents, tool calling, Pydantic validation) that everything above assembles into.

Start with the Smallest Useful Abstraction

For one model call, an official SDK plus an ordinary Python function is often the clearest design. A framework becomes valuable when the application truly needs reusable retrieval chains, durable agent state, branching workflows, or observability. Frameworks change quickly, so isolate provider-specific code and pin tested versions.

MCP is a protocol that lets an AI application discover and invoke tools or read resources through a standard interface. It is not a model, database, or safety system by itself.

Model Output Is Untrusted Input

A model can suggest a tool name and arguments, but your Python application owns the real decision:

model proposes tool call

validate shape and allowed values

check user permission and business rules

ask for approval if the action is sensitive

execute with least privilege → record result

Structured output can guarantee a shape such as {city: string} when the provider supports strict schemas. It cannot guarantee that the city is real, that a factual answer is correct, or that an action is authorized. Validate meaning as well as structure, and never place raw model-generated text into a shell command or database query.

Module Summary

You’ve now seen exactly how embeddings, RAG, vector databases, agents, tool calling, structured outputs, and Pydantic validation are built from Python concepts you already deeply understand — and how LangChain, LangGraph, and MCP organize those same patterns at framework scale rather than inventing anything fundamentally new.

AI Connection

This entire module is the AI connection — every concept here is a direct, traceable assembly of the thirteen modules before it. That’s the whole point of this course: not to memorize “AI terms,” but to be able to look at RAG code, agent code, or a LangChain snippet and genuinely understand what it’s doing and why, because you’ve already built rough versions of all of it yourself.

Mini Practice

  1. Extend the mini-RAG pipeline in Section 3 to return the top 3 documents instead of 2, and print each with its similarity score.
  2. Define a Pydantic model ToolCallResult with tool_name: str, success: bool, and output: Optional[str], and validate one correct and one intentionally invalid example.
  3. Extend the run_agent() function in Section 5 to log (Module 12) each step it takes, using an appropriate logging level.
  4. Rewrite the agent loop’s tool-calling step to run two independent tools concurrently using asyncio.gather (Module 13).
  5. In your own words, explain to someone who has never coded before: what is RAG, why does it need Python, and which three Python concepts from this course does it lean on the most?

You’ve gone from print("hello world") to building — by hand, using nothing but the Python you now understand — a rough retriever, a rough agent loop, tool calling, and validated structured output. That is genuinely most of what real AI application code is doing underneath its frameworks.

Two practical modules remain, rounding out what a real AI codebase needs beyond the model-facing logic itself:

Next: Module 15 — Unit Testing and Mock Testing — testing this kind of AI code reliably, without hitting a live API on every test run.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed