Submission checklist
Package (Required)
Related Issues / PRs
None found — searched issues and discussions for __ror__, NotImplemented, and coerce_to_runnable; all hits were user-side type mistakes, none discussed this design point.
Reproduction Steps / Example Code (Python)
from langchain_core.runnables import RunnableLambda
class Foreign:
"""A third-party class that defines the reflected operator."""
def __ror__(self, other):
return ("foreign", other)
chain = RunnableLambda(lambda x: x * 2)
chain | Foreign()
# Foreign.__ror__ is never called; TypeError is raised instead (see below).
# Per the Python data model, __or__ should return NotImplemented here so the
# interpreter can try Foreign.__ror__.
Error Message and Stack Trace (if applicable)
Traceback (most recent call last):
File "C:\Projects\lagnchain_or\repo.py", line 12, in <module>
chain | Foreign()
~~~~~~^~~~~~~~~~~
File "C:\Projects\lagnchain_or\arch\langchain\libs\core\langchain_core\runnables\base.py", line 667, in __or__
return RunnableSequence(self, coerce_to_runnable(other))
^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Projects\lagnchain_or\arch\langchain\libs\core\langchain_core\runnables\base.py", line 6652, in coerce_to_runnable
raise TypeError(msg)
TypeError: Expected a Runnable, callable or dict.Instead got an unsupported type: <class '__main__.Foreign'>
Description
To be precise about the word "bug": the runtime behavior itself appears
correct (argued below) — what I'm reporting is that this load-bearing
behavior is undocumented and untested: a dunder that raises has no
Raises: section, NotImplemented is never mentioned, and nothing pins
the behavior against a well-meaning "convention compliance" patch.
What diverges from the data model. Per the Python docs, binary dunders
that cannot handle an operand should return NotImplemented so the
interpreter tries the other operand's reflected method
(https://docs.python.org/3/library/constants.html#NotImplemented).
Runnable.__or__/__ror__ instead propagate the TypeError raised by
coerce_to_runnable, so a third party's __ror__ is never consulted.
Builtins (int, set, dict, frozenset) and other operator-overloading
stdlib code (pathlib.Path.__truediv__) all return NotImplemented here.
(Amusingly, mypy strict mode types chain | Foreign() as str via
Foreign.__ror__ — the static type system assumes the very protocol the
runtime does not honor.)
Why I am NOT proposing to "fix" it. I prototyped the convention-compliant
variants and tested them against operands that are ubiquitous next to LCEL
code (NumPy arrays, i.e. embeddings). Each variant is worse than the status
quo:
| variant |
result |
return NotImplemented (pure convention) |
chain | np.array([1,2,3]) is intercepted by NumPy's elementwise __ror__ and dies with a misleading per-element error ('RunnableLambda' and 'int'); common mistakes like chain | 5 lose the current descriptive message |
manual delegation to other.__ror__, re-raise on failure |
preserves error messages for dumb operands, but chain | np.array([f, g]) (an object array of callables) silently succeeds, returning an ndarray of RunnableSequence objects instead of a clear error |
The root issue is that | is a homonym: composition for LCEL, elementwise
bitwise-or for the numeric ecosystem. The reflected-operator protocol
coordinates turn-taking, not meaning — opening it invites cross-semantic
accidents precisely from the neighbors LCEL users have in scope. Raising
immediately acts as a firewall. Notably, LCEL already relies on the other
side of this convention: {"context": ...} | prompt works only because
dict.__or__ returns NotImplemented and defers to Runnable.__ror__ —
LCEL consumes the protocol without extending it, and the asymmetry is
coherent because coercion already covers every operand LCEL can
meaningfully accept. Third parties that genuinely want to interoperate have
sanctioned paths today: subclass Runnable, be callable, or define
__or__ and stand on the left.
Archaeology. The first released version containing __or__
(v0.0.245, Jul 2023) already had today's shape — coercion plus this exact
descriptive TypeError — and at both endpoints I checked (v0.0.245 and
current master) the protocol sentinel NotImplemented appears zero
times in the module (NotImplementedError, unrelated, does appear). I
found no comment, test, issue, or discussion about it in between. So the
firewall property looks emergent rather than designed. Comparable
operator-borrowing libraries pin this down: Django's Q objects fail
loudly on unknown operands, SQLAlchemy documents its operator overloads and
guards adjacent misuse, and NumPy chose an explicit opt-in protocol
(__array_ufunc__, NEP 13) over blind reflected dispatch for exactly this
cross-semantic reason.
Proposal (small, non-behavioral):
- Add a short note to the
__or__/__ror__ docstrings stating that | is
a closed coercion operator, that NotImplemented is intentionally never
returned, and why (with the interop alternatives listed) — plus the
missing Raises: TypeError section.
- Add two characterization tests pinning the behavior: unsupported operands
raise the descriptive TypeError, and a foreign __ror__ is not
consulted.
Happy to open a PR for both if this direction is acceptable. If the current
behavior is instead considered a bug and reflected cooperation is desired,
I can share the experiments above in more detail — but based on them I'd
recommend documenting the status quo.
Context: found while reading the source to understand LCEL's operator
overloading; not blocking any production use case.
System Info
System Information
OS: Windows
OS Version: 10.0.26200
Python Version: 3.12.3 (tags/v3.12.3:f6650f9, Apr 9 2024, 14:05:25) [MSC v.1938 64 bit (AMD64)]
Package Information
langchain_core: 1.5.1
langsmith: 0.10.10
langchain_protocol: 0.0.18
Optional packages not installed
deepagents
deepagents-cli
Other Dependencies
anyio: 4.14.2
distro: 1.9.0
httpx: 0.28.1
jsonpatch: 1.33
orjson: 3.11.9
packaging: 26.2
pydantic: 2.13.4
pytest: 9.1.1
pyyaml: 6.0.3
requests: 2.34.2
requests-toolbelt: 1.0.0
sniffio: 1.3.1
tenacity: 9.1.4
typing-extensions: 4.16.0
uuid-utils: 0.17.0
websockets: 16.1.1
xxhash: 3.8.1
zstandard: 0.25.0
langchain-core installed from source (master checkout)
Submission checklist
Package (Required)
Related Issues / PRs
None found — searched issues and discussions for
__ror__,NotImplemented, andcoerce_to_runnable; all hits were user-side type mistakes, none discussed this design point.Reproduction Steps / Example Code (Python)
Error Message and Stack Trace (if applicable)
Description
To be precise about the word "bug": the runtime behavior itself appears
correct (argued below) — what I'm reporting is that this load-bearing
behavior is undocumented and untested: a dunder that raises has no
Raises:section,NotImplementedis never mentioned, and nothing pinsthe behavior against a well-meaning "convention compliance" patch.
What diverges from the data model. Per the Python docs, binary dunders
that cannot handle an operand should return
NotImplementedso theinterpreter tries the other operand's reflected method
(https://docs.python.org/3/library/constants.html#NotImplemented).
Runnable.__or__/__ror__instead propagate theTypeErrorraised bycoerce_to_runnable, so a third party's__ror__is never consulted.Builtins (
int,set,dict,frozenset) and other operator-overloadingstdlib code (
pathlib.Path.__truediv__) all returnNotImplementedhere.(Amusingly, mypy strict mode types
chain | Foreign()asstrviaForeign.__ror__— the static type system assumes the very protocol theruntime does not honor.)
Why I am NOT proposing to "fix" it. I prototyped the convention-compliant
variants and tested them against operands that are ubiquitous next to LCEL
code (NumPy arrays, i.e. embeddings). Each variant is worse than the status
quo:
NotImplemented(pure convention)chain | np.array([1,2,3])is intercepted by NumPy's elementwise__ror__and dies with a misleading per-element error ('RunnableLambda' and 'int'); common mistakes likechain | 5lose the current descriptive messageother.__ror__, re-raise on failurechain | np.array([f, g])(an object array of callables) silently succeeds, returning anndarrayofRunnableSequenceobjects instead of a clear errorThe root issue is that
|is a homonym: composition for LCEL, elementwisebitwise-or for the numeric ecosystem. The reflected-operator protocol
coordinates turn-taking, not meaning — opening it invites cross-semantic
accidents precisely from the neighbors LCEL users have in scope. Raising
immediately acts as a firewall. Notably, LCEL already relies on the other
side of this convention:
{"context": ...} | promptworks only becausedict.__or__returnsNotImplementedand defers toRunnable.__ror__—LCEL consumes the protocol without extending it, and the asymmetry is
coherent because coercion already covers every operand LCEL can
meaningfully accept. Third parties that genuinely want to interoperate have
sanctioned paths today: subclass
Runnable, be callable, or define__or__and stand on the left.Archaeology. The first released version containing
__or__(
v0.0.245, Jul 2023) already had today's shape — coercion plus this exactdescriptive
TypeError— and at both endpoints I checked (v0.0.245 andcurrent master) the protocol sentinel
NotImplementedappears zerotimes in the module (
NotImplementedError, unrelated, does appear). Ifound no comment, test, issue, or discussion about it in between. So the
firewall property looks emergent rather than designed. Comparable
operator-borrowing libraries pin this down: Django's
Qobjects failloudly on unknown operands, SQLAlchemy documents its operator overloads and
guards adjacent misuse, and NumPy chose an explicit opt-in protocol
(
__array_ufunc__, NEP 13) over blind reflected dispatch for exactly thiscross-semantic reason.
Proposal (small, non-behavioral):
__or__/__ror__docstrings stating that|isa closed coercion operator, that
NotImplementedis intentionally neverreturned, and why (with the interop alternatives listed) — plus the
missing
Raises: TypeErrorsection.raise the descriptive
TypeError, and a foreign__ror__is notconsulted.
Happy to open a PR for both if this direction is acceptable. If the current
behavior is instead considered a bug and reflected cooperation is desired,
I can share the experiments above in more detail — but based on them I'd
recommend documenting the status quo.
Context: found while reading the source to understand LCEL's operator
overloading; not blocking any production use case.
System Info
System Information
Package Information
Optional packages not installed
Other Dependencies
langchain-core installed from source (master checkout)