LangChain Crash Course — Chains, Models, and Output Parsers
You’ve called the OpenAI API on its own. You’ve built prompt strings by hand, parsed JSON, and written error-handling code. It works — until you need to swap models, chain steps together, or get clean structured output without a custom parser each time.
That’s where LangChain comes in. It gives you four snap-together building blocks: models, prompts, parsers, and chains. Link them with a single pipe operator, and you go from raw API calls to reusable LLM pipelines in minutes.
Before we write any code, here’s how the pieces fit together.
You start with a chat model — a wrapper around an LLM API like OpenAI. Instead of raw HTTP requests, you call .invoke() and get a structured response back. But you don’t want to hard-code prompts. So you build a prompt template with slots for dynamic values. The template fills in the blanks and sends formatted messages to the model. The model returns text, but your app needs data. An output parser takes that raw text and turns it into a Python dict or a typed Pydantic object. Last, the pipe operator (|) chains all three into one pipeline. Data flows left to right — template to model to parser — in a single line.
We’ll build each piece from scratch. By the end, you’ll chain them to pull structured meeting summaries from raw notes.
What Is LangChain and Why Does It Exist?
LangChain is an open-source Python framework for building apps powered by large language models. It wraps raw API calls into parts you can mix, match, and reuse.
Why not just use the OpenAI SDK? Three pain points push people to LangChain.
Model lock-in. With the raw SDK, your code is tied to one provider. LangChain’s ChatOpenAI shares the same interface as ChatAnthropic or ChatGoogle. Swap one import, and your whole pipeline runs on a new provider.
Prompt chaos. When prompts have dynamic values, string formatting gets messy fast. ChatPromptTemplate handles variable injection, role setup, and message formatting in one spot.
Parsing headaches. The raw API returns plain text. Want JSON? You call json.loads() and hope the model played along. LangChain’s output parsers handle format instructions, parsing, and type checks for you.
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Set your OpenAI API key (or load from .env)
# os.environ["OPENAI_API_KEY"] = "your-key-here"
print("LangChain imports loaded successfully")
LangChain imports loaded successfully
Tip: You need an OpenAI API key to run this tutorial. Create one at platform.openai.com/api-keys. Store it as the OPENAI_API_KEY environment variable. Never hard-code keys in your scripts.
Prerequisites
-
Python version: 3.10+
-
Required libraries: langchain (0.3+), langchain-openai (0.3+), langchain-core (0.3+), pydantic (2.0+)
-
Install:
pip install langchain langchain-openai langchain-core pydantic -
API key: OpenAI API key stored as
OPENAI_API_KEYenvironment variable -
Time to complete: ~25 minutes
Chat Models — Your LLM Connection
What does it look like to call an LLM through LangChain? Here’s the simplest case. We make a ChatOpenAI instance and pass a string to .invoke().
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = llm.invoke("What is LangChain in one sentence?")
print(type(response))
print(response.content)
<class 'langchain_core.messages.ai.AIMessage'>
LangChain is an open-source framework that simplifies building applications powered by large language models by providing tools for prompt management, chaining, memory, and integrations.
Two things to notice. First, you don’t get a string back — you get an AIMessage object. The text lives in .content. Second, temperature=0 locks the output. Same input, same output, every time. I use temperature=0 in tutorials so results stay the same. For creative tasks, try 0.3 to 0.7.
Quick check: What if you printed response instead of response.content? You’d see the full AIMessage with metadata — token counts, model name, and more. Give it a try.
Key Insight: Every LangChain part uses the same .invoke() interface. Models, prompts, parsers, chains — they all take input through .invoke() and return output. This shared interface is what makes them snap together.
Prompt Templates — Reusable Prompts with Variables
Say you’re building a chatbot that explains topics across different fields. One user asks about machine learning. Another asks about chemistry. The prompt shape is the same — only the topic and question change.
Instead of formatting strings by hand each time, you define a template once and reuse it. ChatPromptTemplate.from_messages() takes a list of (role, message) tuples. Put variables inside curly braces.
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that explains {topic} concepts simply."),
("human", "{question}")
])
formatted = prompt.invoke({
"topic": "machine learning",
"question": "What is overfitting?"
})
print(formatted.to_string())
System: You are a helpful assistant that explains machine learning concepts simply.
Human: What is overfitting?
The .invoke() call gives back a ChatPromptValue — formatted messages ready for the model. No manual dict building, no missing role keys.
Don’t need a system message? Use from_template() for simpler prompts.
simple_prompt = ChatPromptTemplate.from_template(
"Translate '{text}' to {language}."
)
result = simple_prompt.invoke({"text": "Hello world", "language": "French"})
print(result.to_string())
Human: Translate 'Hello world' to French.
Use from_template() for single-message prompts. Use from_messages() when you need a system prompt to shape model behavior. That’s the only choice to make.
Output Parsers — Getting Structured Data from LLMs
LLMs return strings. Your app needs Python objects. Output parsers bridge that gap. LangChain gives you three parsers that cover most needs.
StrOutputParser — Extract Plain Text
Why do you need a parser just to get a string? Because without it, your chain returns an AIMessage object — not clean text. Watch the difference:
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
raw_response = llm.invoke("Say hello")
print(f"Without parser: {type(raw_response).__name__}")
parsed = parser.invoke(raw_response)
print(f"With parser: {type(parsed).__name__} -> {parsed}")
Without parser: AIMessage
With parser: str -> Hello! How can I assist you today?
On its own, StrOutputParser seems small. Its real value shows up in a chain — it’s the piece that gives you a clean string at the end of the pipeline.
JsonOutputParser — Get Python Dictionaries
When you need structured data, JsonOutputParser does two things: it creates format instructions for the prompt (telling the model to reply in JSON) and it parses the response into a Python dict.
The parser’s .get_format_instructions() method builds a string you inject into your prompt. This tells the model exactly what JSON shape to produce. We’ll use .partial() to plug those instructions into the template.
from langchain_core.output_parsers import JsonOutputParser
json_parser = JsonOutputParser()
prompt = ChatPromptTemplate.from_messages([
("system", "Extract the requested information. {format_instructions}"),
("human", "Give me the name, population, and continent of Japan.")
])
chain = prompt.partial(
format_instructions=json_parser.get_format_instructions()
) | llm | json_parser
result = chain.invoke({})
print(type(result))
print(result)
This produces:
<class 'dict'>
{'name': 'Japan', 'population': 125700000, 'continent': 'Asia'}
That chain line uses the pipe operator | — we’ll cover it fully in the chains section. The key point: the output is a Python dict, not a string. You can grab result["name"] right away.
PydanticOutputParser — Validated Structured Output
JsonOutputParser gives you dicts, but it doesn’t check the shape or types. What if the model returns "pop" instead of "population"? Your code breaks later with a confusing KeyError.
PydanticOutputParser stops this. You define a Pydantic model — a Python class with typed fields — and the parser checks the LLM’s response against that schema.
Here we make a CountryInfo model with three required fields. The Field(description=...) tells the parser (and the LLM) what each field should hold.
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
class CountryInfo(BaseModel):
name: str = Field(description="Name of the country")
population: int = Field(description="Approximate population")
continent: str = Field(description="Continent the country is in")
pydantic_parser = PydanticOutputParser(pydantic_object=CountryInfo)
prompt = ChatPromptTemplate.from_messages([
("system", "Extract country information. {format_instructions}"),
("human", "Tell me about {country}.")
])
chain = prompt.partial(
format_instructions=pydantic_parser.get_format_instructions()
) | llm | pydantic_parser
result = chain.invoke({"country": "Brazil"})
print(type(result))
print(f"Name: {result.name}")
print(f"Population: {result.population}")
print(f"Continent: {result.continent}")
<class '__main__.CountryInfo'>
Name: Brazil
Population: 214000000
Continent: South America
The result isn’t a dict — it’s a CountryInfo object with dot access. Pydantic checks types on the fly. If the model sent a string for population, you’d get a ValidationError right away instead of a hidden bug.
Predict the output: What if you set population as float instead of int? The parser would accept both 214000000 and 214000000.0, because Pydantic can convert between them. But if the model sent "two hundred million" as a string, it would fail.
Warning: LLM output isn’t always going to match your schema. Even with format instructions, models can still return bad JSON. In production, always wrap chain calls in try/except and handle OutputParserException. Retry with a clearer prompt, or fall back to string output.
I suggest PydanticOutputParser over JsonOutputParser for anything past quick tests. The type safety catches bugs before they reach your users.
Parser Comparison
Here’s a quick look at which parser to use and when:
- StrOutputParser — When you just need text. Simple and fast.
- JsonOutputParser — When you need a dict but don’t care about strict types. Good for quick prototypes.
- PydanticOutputParser — When you need typed, checked output. Best for production code.
Chains — Connecting Components with LCEL
Here’s where it all clicks together.
A chain links LangChain parts into a pipeline. Data flows from one to the next. The prompt fills in values, the model creates a response, and the parser pulls out structured results.
LangChain Expression Language (LCEL) uses the pipe operator | to connect parts. If you’ve used Unix pipes (cat file.txt | grep "error"), it works the same way. The output on the left becomes the input on the right.
The simplest useful chain has three steps: prompt, model, parser.
prompt = ChatPromptTemplate.from_template(
"Explain {concept} in 2 sentences for a beginner."
)
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"concept": "neural networks"})
print(result)
Neural networks are computational models inspired by the human brain, consisting of layers of interconnected nodes that process and learn patterns from data. They are widely used in tasks like image recognition, language processing, and prediction by adjusting the strength of connections during training.
Three parts. One pipe line. Clean string output.
What Happens Under the Hood
When you write prompt | llm | parser, Python doesn’t run anything yet. It calls the __or__ method on each part, building a RunnableSequence object. This stores the list of steps in order. Nothing runs until you call .invoke().
Let’s check by looking at the chain.
from langchain_core.runnables import RunnableSequence
chain = prompt | llm | StrOutputParser()
print(f"Chain type: {type(chain).__name__}")
print(f"Is RunnableSequence: {isinstance(chain, RunnableSequence)}")
print(f"Steps: {len(chain.steps)}")
for i, step in enumerate(chain.steps):
print(f" Step {i}: {type(step).__name__}")
Prints:
Chain type: RunnableSequence
Is RunnableSequence: True
Steps: 3
Step 0: ChatPromptTemplate
Step 1: ChatOpenAI
Step 2: StrOutputParser
Key Insight: The pipe operator | doesn’t run anything. It builds a RunnableSequence — a blueprint for your pipeline. Code only runs when you call .invoke(), .batch(), or .stream().
Beyond .invoke() — Batch and Stream
Every chain supports three run modes out of the box. You don’t write extra code for any of them.
.batch() runs many inputs at once. Pass a list of dicts, get a list of results.
results = chain.batch([
{"concept": "API"},
{"concept": "REST"},
])
for r in results:
print(r[:80] + "...")
print()
An API (Application Programming Interface) is a set of rules and protocols that...
REST (Representational State Transfer) is an architectural style for designing n...
.stream() sends output tokens one at a time — great for showing responses in real time. You’d use this in a chatbot where users want to see text appear word by word.
for chunk in chain.stream({"concept": "machine learning"}):
print(chunk, end="", flush=True)
print()
Machine learning is a subset of artificial intelligence that enables computers to learn patterns from data and make predictions or decisions without being explicitly programmed. It works by training algorithms on large datasets, allowing them to improve their performance over time as they are exposed to more data.
These three methods — .invoke(), .batch(), .stream() — work on every Runnable in LangChain. Models, parsers, chains, all of them. That’s the payoff of the shared interface.
Building a Real-World Chain
Toy examples teach syntax. Let’s build something you’d use at work.
Scenario: You’re building a tool that takes raw meeting notes and pulls out a structured summary with action items. The output must be a typed Python object you can send to a project management API.
We need three things: a Pydantic schema for the output, a prompt that tells the model what to extract, and a chain that ties it all together.
First, the output schema. A meeting summary has a title, a list of key decisions, and action items. Each action item pairs a task with a person.
from typing import List
class ActionItem(BaseModel):
task: str = Field(description="Description of the action item")
assignee: str = Field(description="Person responsible")
class MeetingSummary(BaseModel):
title: str = Field(description="Brief meeting title")
key_decisions: List[str] = Field(description="Main decisions made")
action_items: List[ActionItem] = Field(description="Tasks assigned")
Next, the chain. We inject Pydantic format instructions into the prompt so the model knows what JSON shape to return. Then we pipe prompt to model to parser.
summary_parser = PydanticOutputParser(pydantic_object=MeetingSummary)
summary_prompt = ChatPromptTemplate.from_messages([
("system",
"You are a meeting notes assistant. Extract structured "
"information from the meeting transcript.\n"
"{format_instructions}"),
("human", "{transcript}")
])
summary_chain = summary_prompt.partial(
format_instructions=summary_parser.get_format_instructions()
) | llm | summary_parser
Let’s feed it a sample transcript and see what we get.
meeting_notes = """
Team standup March 10. Present: Alice, Bob, Carol.
Alice said the data pipeline is done and ready for review.
Bob mentioned the API rate limits are causing issues in production.
We decided to implement exponential backoff for API calls.
Carol will write the retry logic by Friday.
Bob will set up monitoring dashboards by next Tuesday.
Alice will review Carol's PR once it's ready.
"""
summary = summary_chain.invoke({"transcript": meeting_notes})
print(f"Title: {summary.title}")
print(f"\nKey Decisions:")
for decision in summary.key_decisions:
print(f" - {decision}")
print(f"\nAction Items:")
for item in summary.action_items:
print(f" - {item.task} (Assigned to: {item.assignee})")
Title: Team Standup March 10
Key Decisions:
- Implement exponential backoff for API calls
Action Items:
- Write the retry logic by Friday (Assigned to: Carol)
- Set up monitoring dashboards by next Tuesday (Assigned to: Bob)
- Review Carol's PR once it's ready (Assigned to: Alice)
One chain call turned messy meeting notes into a typed Python object with clean fields. You could send summary.action_items straight to Jira, Asana, or any task API — no manual parsing needed.
Tip: Use nested Pydantic models for rich output. The ActionItem class nested inside MeetingSummary shows how to get deeply structured data. You can nest as many levels as you need. The parser creates format instructions for the whole tree.
LangChain vs Raw OpenAI API
Is LangChain worth the extra package? Here’s an honest look.
For the same “extract country info” task, the raw OpenAI way looks like this:
# Raw OpenAI approach (pseudocode for comparison)
# client = openai.OpenAI()
# response = client.chat.completions.create(
# model="gpt-4o-mini",
# messages=[
# {"role": "system", "content": "Return JSON with: name, population, continent"},
# {"role": "user", "content": "Tell me about France"}
# ],
# response_format={"type": "json_object"}
# )
# data = json.loads(response.choices[0].message.content)
# No type validation -- data["population"] could be a string
The LangChain way with PydanticOutputParser gives you type checks, reusable templates, and a chain you can extend — all in roughly the same lines of code.
| Feature | Raw OpenAI SDK | LangChain |
|---|---|---|
| Swap to Claude/Gemini | Rewrite all API calls | Change one import |
| Reusable prompts | Copy-paste strings | Template objects |
| Output validation | Write custom code | Built-in parsers |
| Chain multiple steps | Nested function calls | Pipe operator \| |
| Streaming | Manual chunk handling | .stream() built-in |
| Batch processing | Write a loop | .batch() built-in |
For a single API call in a script, the raw SDK works fine. But once you’re building multi-step pipelines, switching providers, or need typed output, LangChain saves real time. That’s the tradeoff.
Common Mistakes and How to Fix Them
Mistake 1: Missing the provider package
LangChain keeps its core separate from provider packages. If you only install langchain, you won’t have ChatOpenAI.
Wrong:
# pip install langchain
# from langchain_openai import ChatOpenAI # ImportError!
Right:
# pip install langchain langchain-openai
from langchain_openai import ChatOpenAI
print("Import successful")
Import successful
Mistake 2: Passing a string to a chain that expects a dict
Chains expect dicts with keys that match your template variables. A plain string throws a TypeError.
Wrong:
# chain.invoke("What is Python?")
# TypeError: Expected mapping type as input
Right:
result = chain.invoke({"concept": "Python"})
print(result[:100])
Python is a high-level, versatile programming language known for its readability and simplicity, mak
Mistake 3: Not handling parser failures
LLMs don’t always produce valid JSON. Without error handling, your app crashes on the first bad response.
Wrong:
# result = pydantic_chain.invoke({"country": "..."})
# Crashes if model returns invalid JSON
Right:
from langchain_core.exceptions import OutputParserException
try:
result = chain.invoke({"concept": "error handling in Python"})
print(result[:100])
except OutputParserException as e:
print(f"Parser failed: {e}")
Error handling in Python involves using try-except blocks to catch and manage exceptions that may oc
Mistake 4: Using the old LLMChain class
Older guides use LLMChain from langchain.chains. This class was dropped in LangChain 0.1.17. Always use LCEL with the pipe operator.
Wrong:
# from langchain.chains import LLMChain # Deprecated!
# chain = LLMChain(llm=llm, prompt=prompt)
Right:
chain = prompt | llm | StrOutputParser()
print("LCEL chain created successfully")
LCEL chain created successfully
Note: LangChain 1.0 came out in November 2025. It moved old code to langchain-classic. The LCEL pipe syntax is the way forward. If you follow an older guide and see LLMChain, SequentialChain, or SimpleSequentialChain, swap them out for LCEL pipe chains.
Practice Exercises
Exercise 1: Build a Translation Chain
Create a LangChain chain that translates text between languages.
- Make a
ChatPromptTemplatewith variables{text}and{target_language} - Chain it with the llm and
StrOutputParserusing the pipe operator - Call the chain to translate “Good morning” to Spanish
- Print the result
Starter code:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Step 1: Create the prompt template
translate_prompt = ChatPromptTemplate.from_template(
# Your template here -- include {text} and {target_language}
)
# Step 2: Build the chain with |
translate_chain = # prompt | llm | parser
# Step 3: Invoke and print
result = translate_chain.invoke({
"text": "Good morning",
"target_language": "Spanish"
})
print(result)
Hints:
– Template: "Translate the following text to {target_language}: {text}"
– Full chain: translate_prompt | llm | StrOutputParser()
Click to see the solution
translate_prompt = ChatPromptTemplate.from_template(
"Translate the following text to {target_language}: {text}"
)
translate_chain = translate_prompt | llm | StrOutputParser()
result = translate_chain.invoke({
"text": "Good morning",
"target_language": "Spanish"
})
print(result)
The prompt template uses `{text}` for the input and `{target_language}` for the target. The pipe operator chains template to model to parser. `StrOutputParser` pulls the plain text from the `AIMessage`.
Exercise 2: Extract Structured Data with PydanticOutputParser
Create a chain that pulls book info into a Pydantic model.
- Add
author(str) andyear(int) fields to theBookInfomodel - The parser and prompt are set up for you
- Build the chain and run it with the given text
- Print the title, author, and year
Starter code:
from pydantic import BaseModel, Field
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
# Step 1: Complete the Pydantic model
class BookInfo(BaseModel):
title: str = Field(description="Title of the book")
# Add author and year fields here
# Step 2: Parser and prompt (already done)
book_parser = PydanticOutputParser(pydantic_object=BookInfo)
book_prompt = ChatPromptTemplate.from_messages([
("system", "Extract book information. {format_instructions}"),
("human", "{text}")
])
# Step 3: Build and invoke the chain
book_chain = book_prompt.partial(
format_instructions=book_parser.get_format_instructions()
) | llm | book_parser
result = book_chain.invoke({
"text": "The Great Gatsby was written by F. Scott Fitzgerald in 1925"
})
print(f"Title: {result.title}")
print(f"Author: {result.author}")
print(f"Year: {result.year}")
Hints:
– Add: author: str = Field(description="Author of the book")
– Add: year: int = Field(description="Publication year")
Click to see the solution
class BookInfo(BaseModel):
title: str = Field(description="Title of the book")
author: str = Field(description="Author of the book")
year: int = Field(description="Publication year")
book_parser = PydanticOutputParser(pydantic_object=BookInfo)
book_prompt = ChatPromptTemplate.from_messages([
("system", "Extract book information. {format_instructions}"),
("human", "{text}")
])
book_chain = book_prompt.partial(
format_instructions=book_parser.get_format_instructions()
) | llm | book_parser
result = book_chain.invoke({
"text": "The Great Gatsby was written by F. Scott Fitzgerald in 1925"
})
print(f"Title: {result.title}")
print(f"Author: {result.author}")
print(f"Year: {result.year}")
The Pydantic model sets typed fields for structured data. `PydanticOutputParser` builds format instructions telling the LLM what JSON shape to produce. The parser then checks and converts the response into a `BookInfo` object with proper type handling.
Summary
You’ve learned the four building blocks that power every LangChain app:
-
Chat models (
ChatOpenAI) wrap LLM APIs behind a shared.invoke()interface -
Prompt templates (
ChatPromptTemplate) create reusable prompts with dynamic values -
Output parsers (
StrOutputParser,JsonOutputParser,PydanticOutputParser) turn raw model text into structured Python objects -
Chains (LCEL pipe
|) link parts into pipelines where data flows left to right
These are the base for LangGraph. When you build agents, state machines, and multi-step workflows in future posts, you’ll use these same pieces in more advanced patterns.
Practice exercise: Build a chain that takes a product blurb and pulls out a ProductInfo object with fields: name (str), category (str), price_range (str — “low”, “medium”, or “high”), and tagline (str — one-sentence marketing copy). Use PydanticOutputParser for type checks. Test it with: “The AirPods Pro 2 are premium wireless earbuds by Apple with active noise cancellation, priced at $249.”
Click to see the solution
class ProductInfo(BaseModel):
name: str = Field(description="Product name")
category: str = Field(description="Product category")
price_range: str = Field(description="Price range: low, medium, or high")
tagline: str = Field(description="One-sentence marketing tagline")
product_parser = PydanticOutputParser(pydantic_object=ProductInfo)
product_prompt = ChatPromptTemplate.from_messages([
("system",
"Extract product information from the description. "
"{format_instructions}"),
("human", "{description}")
])
product_chain = product_prompt.partial(
format_instructions=product_parser.get_format_instructions()
) | llm | product_parser
result = product_chain.invoke({
"description": "The AirPods Pro 2 are premium wireless earbuds "
"by Apple with active noise cancellation, priced at $249."
})
print(f"Name: {result.name}")
print(f"Category: {result.category}")
print(f"Price Range: {result.price_range}")
print(f"Tagline: {result.tagline}")
The chain pulls all four fields into a typed `ProductInfo` object. Pydantic checks each field’s type. If the model skips `tagline`, the parser throws a `ValidationError` instead of giving you a half-finished object.
Complete Code
Click to expand the full script (copy-paste and run)
# Complete code from: LangChain Crash Course -- Chains, Models, and Output Parsers
# Requires: pip install langchain langchain-openai langchain-core pydantic
# Python 3.10+
# Set OPENAI_API_KEY environment variable before running
import os
from typing import List
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import (
StrOutputParser,
JsonOutputParser,
PydanticOutputParser,
)
from langchain_core.runnables import RunnableSequence
from langchain_core.exceptions import OutputParserException
from pydantic import BaseModel, Field
# --- Section 1: Chat Model ---
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = llm.invoke("What is LangChain in one sentence?")
print("=== Chat Model ===")
print(response.content)
# --- Section 2: Prompt Templates ---
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that explains {topic} concepts simply."),
("human", "{question}")
])
formatted = prompt.invoke({
"topic": "machine learning",
"question": "What is overfitting?"
})
print("\n=== Prompt Template ===")
print(formatted.to_string())
simple_prompt = ChatPromptTemplate.from_template(
"Translate '{text}' to {language}."
)
# --- Section 3: StrOutputParser ---
parser = StrOutputParser()
raw_response = llm.invoke("Say hello")
parsed = parser.invoke(raw_response)
print("\n=== StrOutputParser ===")
print(f"Type: {type(parsed).__name__} -> {parsed}")
# --- Section 4: JsonOutputParser ---
json_parser = JsonOutputParser()
json_prompt = ChatPromptTemplate.from_messages([
("system", "Extract the requested information. {format_instructions}"),
("human", "Give me the name, population, and continent of Japan.")
])
json_chain = json_prompt.partial(
format_instructions=json_parser.get_format_instructions()
) | llm | json_parser
json_result = json_chain.invoke({})
print("\n=== JsonOutputParser ===")
print(json_result)
# --- Section 5: PydanticOutputParser ---
class CountryInfo(BaseModel):
name: str = Field(description="Name of the country")
population: int = Field(description="Approximate population")
continent: str = Field(description="Continent the country is in")
pydantic_parser = PydanticOutputParser(pydantic_object=CountryInfo)
pydantic_prompt = ChatPromptTemplate.from_messages([
("system", "Extract country information. {format_instructions}"),
("human", "Tell me about {country}.")
])
pydantic_chain = pydantic_prompt.partial(
format_instructions=pydantic_parser.get_format_instructions()
) | llm | pydantic_parser
pydantic_result = pydantic_chain.invoke({"country": "Brazil"})
print("\n=== PydanticOutputParser ===")
print(f"Name: {pydantic_result.name}")
print(f"Population: {pydantic_result.population}")
print(f"Continent: {pydantic_result.continent}")
# --- Section 6: LCEL Chain ---
explain_prompt = ChatPromptTemplate.from_template(
"Explain {concept} in 2 sentences for a beginner."
)
explain_chain = explain_prompt | llm | StrOutputParser()
chain_result = explain_chain.invoke({"concept": "neural networks"})
print("\n=== LCEL Chain ===")
print(chain_result)
# --- Section 7: RunnableSequence inspection ---
print(f"\nChain type: {type(explain_chain).__name__}")
print(f"Steps: {len(explain_chain.steps)}")
for i, step in enumerate(explain_chain.steps):
print(f" Step {i}: {type(step).__name__}")
# --- Section 8: Batch execution ---
batch_results = explain_chain.batch([
{"concept": "API"},
{"concept": "REST"},
])
print("\n=== Batch Results ===")
for r in batch_results:
print(r[:80] + "...\n")
# --- Section 9: Real-World Meeting Summary Chain ---
class ActionItem(BaseModel):
task: str = Field(description="Description of the action item")
assignee: str = Field(description="Person responsible")
class MeetingSummary(BaseModel):
title: str = Field(description="Brief meeting title")
key_decisions: List[str] = Field(description="Main decisions made")
action_items: List[ActionItem] = Field(description="Tasks assigned")
summary_parser = PydanticOutputParser(pydantic_object=MeetingSummary)
summary_prompt = ChatPromptTemplate.from_messages([
("system",
"You are a meeting notes assistant. Extract structured "
"information from the meeting transcript.\n"
"{format_instructions}"),
("human", "{transcript}")
])
summary_chain = summary_prompt.partial(
format_instructions=summary_parser.get_format_instructions()
) | llm | summary_parser
meeting_notes = """
Team standup March 10. Present: Alice, Bob, Carol.
Alice said the data pipeline is done and ready for review.
Bob mentioned the API rate limits are causing issues in production.
We decided to implement exponential backoff for API calls.
Carol will write the retry logic by Friday.
Bob will set up monitoring dashboards by next Tuesday.
Alice will review Carol's PR once it's ready.
"""
summary = summary_chain.invoke({"transcript": meeting_notes})
print("\n=== Meeting Summary ===")
print(f"Title: {summary.title}")
for d in summary.key_decisions:
print(f" Decision: {d}")
for item in summary.action_items:
print(f" Action: {item.task} -> {item.assignee}")
print("\nScript completed successfully.")
Frequently Asked Questions
Can I use LangChain with models other than OpenAI?
Yes. LangChain supports dozens of providers. Install langchain-anthropic for Claude, langchain-google-genai for Gemini, or use langchain-community for open-source models through Ollama. The .invoke() interface is the same for all of them. Swap the import and the setup, and your chains work as before.
# Example: switching to Anthropic (not runnable without the package)
# from langchain_anthropic import ChatAnthropic
# llm = ChatAnthropic(model="claude-sonnet-4-20250514")
# Same .invoke() interface -- chains work identically
What’s the difference between .invoke(), .batch(), and .stream()?
.invoke() handles one input and returns the full result. .batch() runs many inputs at once and returns a list. .stream() sends output tokens one at a time for live display. All three work on every LangChain Runnable — models, parsers, and chains alike.
Do I still need LangChain if OpenAI supports structured outputs on its own?
OpenAI’s response_format handles JSON mode for their models. But it only works with OpenAI. LangChain adds model portability, prompt templates, and chain building. If you only use OpenAI and don’t need multi-step chains, the raw SDK works fine. Once you add more steps or want to switch providers, LangChain pays for itself.
Is LCEL the only way to build chains?
The older LLMChain, SequentialChain, and SimpleSequentialChain classes are gone and live in langchain-classic. LCEL with the pipe operator is the current standard. All new LangChain docs, examples, and features use LCEL only.
References
- LangChain Documentation — Chat models
- LangChain Documentation — LangChain Expression Language (LCEL)
- LangChain Documentation — Output parsers
- LangChain Documentation — Prompt templates
- LangChain-OpenAI package — PyPI
- Pydantic Documentation — Models
- OpenAI API Reference — Chat completions
- LangChain GitHub — Releases and changelog
Build a strong Python foundation with hands-on exercises designed for aspiring Data Scientists and AI/ML Engineers.
Start Free Course →