langchain-ai/langchain
Pitfalls
Common surprises when navigating or extending the LangChain codebase.
The langchain directory ships the wrong package
libs/langchain_v1/langchain/ ships the langchain PyPI distribution. libs/langchain/langchain_classic/ ships the langchain-classic distribution. The directory names are historical artifacts of the v1 rebuild — directly renaming libs/langchain/ would have broken every editable install in the wild.
Quick rule:
import langchain→ look inlibs/langchain_v1/langchain/.import langchain_classic→ look inlibs/langchain/langchain_classic/.
Editable installs don't propagate stale cached imports
uv sync sets up editable installs across packages, so changes in libs/core/ show up live in libs/langchain_v1/. But Python's import system caches modules: if you change langchain_core.messages.content while a notebook is running, the open kernel won't see the change without a restart or importlib.reload. This is normal Python; it bites people who expect IDE-style hot reload.
init_chat_model lazy-imports partner packages
If init_chat_model("anthropic:claude-sonnet-4") raises ImportError, you forgot to install langchain-anthropic. The error message points at the missing package; the _BUILTIN_PROVIDERS registry in libs/langchain_v1/langchain/chat_models/base.py knows the package name to suggest.
Provider keys are not the same as model names
init_chat_model("openai:gpt-5") parses out openai (the provider key) and gpt-5 (the model). The provider key is what _BUILTIN_PROVIDERS matches; the model is passed as a kwarg to the partner class. Some providers use different parameter names (e.g. Hugging Face uses model_id), and those have custom constructor lambdas in the registry.
Callback dispatch is sync or async, never mixed mid-stream
CallbackManager (sync) and AsyncCallbackManager (async) are separate dispatchers. A BaseCallbackHandler that only implements sync methods will get its async events forwarded to the sync version via a thread executor. If you implement a custom handler, override either both variants or none — partial implementations cause silent missed events.
Runnable.batch is not always parallel
The default batch implementation runs invoke in parallel using a thread pool. RunnableSequence.batch walks each step in turn, batching at each step. This is the right behavior, but it means a slow step in the middle of a chain is the bottleneck for the whole batch — not the slowest individual chain.
For true per-call parallelism, use RunnableParallel or Runnable.batch_as_completed.
Streaming a RunnableSequence yields only the last step's output
chain.astream(input) yields whatever the last runnable in the chain yields. Earlier steps run to completion before the last step starts streaming. If you want to see events from every step, use chain.astream_events(input, version="v2"), which fires typed events for every nested runnable.
bind_tools returns a new Runnable, not a mutated model
model_with_tools = model.bind_tools([search])
# model is unchanged; model_with_tools is a RunnableBindingThis is intentional — Runnables are immutable views — but trips up users who try to "configure" a model in place.
Pydantic v2 behavior in tool args schemas
LangChain pins pydantic>=2.7.4. v2's strict-mode validation, JSON Schema generation, and model_validate semantics differ from v1. Tool schemas generated from v2 models are subtly different (e.g. anyOf for optional fields). Most providers accept the v2 output, but a few partners (notably some older Mistral / Bedrock model variants) need additional schema mangling — see convert_to_<provider>_tool in each partner package.
Test directories that opt out of strict mypy
libs/langchain_v1/pyproject.toml excludes some test directories from mypy strict mode:
[tool.mypy]
strict = true
exclude = [
"tests/unit_tests/agents/middleware/",
"tests/unit_tests/agents/specifications/",
"tests/unit_tests/agents/test_.*\\.py",
]If you add a new agent test file, expect to either bring it under the strict regime or add it to the exclude list with justification.
CI runs only the affected packages
.github/workflows/check_diffs.yml inspects the git diff paths and dispatches to per-package workflows. A change that touches libs/core/ triggers tests in every downstream package, but a change that only touches libs/partners/openai/ only triggers OpenAI's CI. This is fast on average but means a langchain-core PR can take a while to finish.
Conventional Commits scopes are linted
.github/workflows/pr_lint.yml enforces the allowed scopes (core, langchain, langchain-classic, text-splitters, standard-tests, model-profiles, tests, partners, individual partner names). A scope typo will block your PR. The error message lists the valid scopes, so don't fight it — just rename your title.
GitHub Actions must be pinned to full SHAs
The workflows in .github/workflows/ pin every uses: to a 40-char commit SHA, never a tag. Adding a new step with uses: actions/checkout@v4 will fail the actionlint step. Look up the SHA via the GitHub API or the action's release page.
Don't change model= defaults in shipped code
Changing the default value of a model= kwarg on a partner class is treated as a breaking change. The reasoning: users who instantiated ChatOpenAI() last week and didn't pin a version expect the same model on next pip install. New defaults need a deprecation path. (Doc/example code is not subject to this rule — those should always cite the latest GA model.)
Pickle is banned
Don't reach for pickle to serialize chains, prompts, or runnables. langchain_core.load.serializable.Serializable plus dumpd/load is the documented mechanism. The repo's security policy explicitly excludes pickle from user-controlled paths (see _security/ in langchain-core and the rule in CLAUDE.md).
See also
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.