langchain-core
langchain-core is the foundation layer of the LangChain ecosystem: the message types, prompt templates, tool definitions, output parsers, and the Runnable protocol that lets you pipe components together with the | operator. You rarely install it on purpose; langchain, langgraph, and every langchain-* provider package (langchain-openai, langchain-anthropic, and so on) depend on it, so its 41M weekly downloads are mostly transitive. If you write against its interfaces, you can swap chat models and vector stores without rewriting the plumbing.
The de facto standard interface layer for LLM apps in Python, and the right dependency if you are in the LangChain or LangGraph ecosystem at all. If your app is one provider and a few prompts, the abstraction costs more than it returns; call the provider SDK directly.
Use it if
- You are building on LangChain or LangGraph anyway and want to import only the base abstractions (messages, prompts, tools) in library code without dragging in the full langchain package
- You are writing a custom integration (a chat model wrapper, retriever, or tool) and need the standard interfaces the rest of the ecosystem expects
- You want provider portability: code written against BaseChatModel and the message types runs against OpenAI, Anthropic, or a local model by swapping one object
- You use the Runnable composition style (prompt | model | parser) and want batch, stream, retry, and fallback behavior for free on every component
- You call one provider and that is unlikely to change: the openai or anthropic SDK is one dependency, has no abstraction layer to learn, and its errors point at your code instead of a Runnable stack
- You dislike churn: the 0.x to 1.0 transition renamed packages and moved imports, deprecation warnings are a regular part of upgrades, and pins across langchain-core, langchain, and provider packages must be kept compatible with each other
- Debugging matters more than composability to you: a failure inside prompt | model | parser surfaces as a deep stack of Runnable internals, and stepping through LCEL chains in a debugger is genuinely unpleasant
- You want a typed, minimal agent framework rather than an ecosystem: pydantic-ai covers tools, structured output, and model portability with a much smaller surface
Setup reality
pip install langchain-core is quick and pure Python, with pydantic v2, tenacity, jsonpatch, and langsmith as the notable dependencies (yes, the LangSmith client ships even if you never trace anything; it stays inert without an API key). The real setup cost is version discipline: langchain-core 1.x must line up with 1.x-era langchain and provider packages, and mixing a 0.3-era integration into a 1.x environment produces resolver conflicts or import errors. Old tutorials are a minefield because import paths moved repeatedly (langchain to langchain_community to langchain_core over the years), so anything pre-2025 needs translation.
Patterns
Build a chat prompt with variableschat-prompt-template
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a terse assistant. Answer in {language}."),
("human", "{question}"),
])
messages = prompt.invoke({"language": "French", "question": "What is LCEL?"})Template variables use single braces, so literal JSON in a prompt must escape braces as {{ and }} or the template raises a KeyError.
Pipe prompt, model, and parser togethercompose-chain
from langchain_core.output_parsers import StrOutputParser
chain = prompt | model | StrOutputParser()
answer = chain.invoke({"language": "French", "question": "What is LCEL?"})Any Runnable composes with |; the parser at the end is what turns an AIMessage into a plain string, so leaving it off hands you message objects.
Build a conversation from message objectsconstruct-messages
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
history = [
SystemMessage("You are a helpful assistant."),
HumanMessage("What is 2 + 2?"),
AIMessage("4"),
HumanMessage("Double it."),
]
result = model.invoke(history)Models also accept ('human', 'text') tuples and plain strings; a bare string becomes a single HumanMessage.
Turn a function into a tool with @tooldefine-tool
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Return current weather for a city."""
return f"Sunny in {city}"
model_with_tools = model.bind_tools([get_weather])
ai_msg = model_with_tools.invoke("Weather in Pune?")
print(ai_msg.tool_calls)The docstring is required; it becomes the tool description the model sees. bind_tools only requests calls, executing them and returning ToolMessages is on you (or LangGraph).
Get validated structured output from a modelstructured-output
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
structured = model.with_structured_output(Person)
person = structured.invoke("Amit is 34 years old.")
print(person.age)with_structured_output uses the provider's native tool-calling or JSON mode under the hood, so support and reliability vary by model.
Stream a response chunk by chunkstream-tokens
for chunk in chain.stream({"language": "English", "question": "Explain RAG."}):
print(chunk, end="", flush=True)Streaming a raw model yields AIMessageChunk objects you can sum with +; after StrOutputParser the chunks are already plain strings.
Run many inputs concurrently with batchbatch-inputs
questions = [{"language": "English", "question": q} for q in ["What is LCEL?", "What is RAG?"]]
answers = chain.batch(questions, config={"max_concurrency": 5})Results come back in input order regardless of completion order; cap max_concurrency or you will hit provider rate limits on big lists.
Fan out to parallel sub-chainsparallel-branches
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
mapper = RunnableParallel(
summary=summarize_chain,
original=RunnablePassthrough(),
)
out = mapper.invoke({"text": "long document..."})
print(out["summary"], out["original"])A plain dict inside a chain is auto-coerced to RunnableParallel, which is why {'context': retriever, 'question': RunnablePassthrough()} works in RAG examples.
Drop a plain function into a chainwrap-function
from langchain_core.runnables import RunnableLambda
clean = RunnableLambda(lambda text: text.strip().lower())
chain = clean | prompt | modelFunctions piped directly against another Runnable are wrapped automatically, but RunnableLambda is required when the function is the first step.
Add retries and a fallback modelretry-and-fallback
primary = model.with_retry(stop_after_attempt=3)
resilient = primary.with_fallbacks([backup_model])
result = (prompt | resilient | StrOutputParser()).invoke(inputs)with_retry retries on any exception by default, including bad-request errors that will never succeed; scope it with retry_if_exception_type.
Parse model output into a Pydantic objectparse-json-output
from langchain_core.output_parsers import PydanticOutputParser
parser = PydanticOutputParser(pydantic_object=Person)
prompt = ChatPromptTemplate.from_messages([
("system", "Answer as JSON.\n{format_instructions}"),
("human", "{query}"),
]).partial(format_instructions=parser.get_format_instructions())
person = (prompt | model | parser).invoke({"query": "Amit is 34."})Prefer with_structured_output when the provider supports it; prompt-based parsing fails on chatty models that wrap JSON in prose.
Use the async API for concurrent appsasync-invoke
import asyncio
async def main():
answer = await chain.ainvoke({"language": "English", "question": "What is LCEL?"})
async for chunk in chain.astream({"language": "English", "question": "More detail."}):
print(chunk, end="")
asyncio.run(main())Every Runnable gets ainvoke, astream, and abatch automatically, but custom RunnableLambdas with blocking I/O will still block the event loop.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pydantic-ai | PyPI | You want typed agents and structured output on top of pydantic with a far smaller abstraction surface. |
| llama-index-core | PyPI | Your application is retrieval-centric and you want data connectors and indexes as the primary abstraction. |
| haystack-ai | PyPI | You want explicit, declarative pipelines for RAG and search rather than operator-overloaded composition. |