Skip to content

Report func-returns-value for decorated methods#21534

Open
jbbqqf wants to merge 1 commit into
python:masterfrom
jbbqqf:fix/14179-staticmethod-func-returns-value
Open

Report func-returns-value for decorated methods#21534
jbbqqf wants to merge 1 commit into
python:masterfrom
jbbqqf:fix/14179-staticmethod-func-returns-value

Conversation

@jbbqqf
Copy link
Copy Markdown

@jbbqqf jbbqqf commented May 21, 2026

Summary

Fixes #14179. Decorated functions — @staticmethod, @classmethod, and user-defined decorators that preserve a -> None return — were silently skipped by the [func-returns-value] check while their undecorated counterparts were not. The check is now consistent across all of them.

Why

ExpressionChecker.defn_returns_none handled FuncDef, OverloadedFuncDef, and Var, but not Decorator. A @staticmethod becomes a Decorator node whose synthetic var.is_inferred is True, so the existing Var branch deliberately skipped it. The function's -> None annotation is still propagated into decorator.var.type as a CallableType, so we can reuse it directly without losing precision for decorators that change the return type (e.g. one that turns () -> None into () -> int is correctly not flagged — verified in the test suite and ad-hoc).

What changed

  • mypy/checkexpr.py: add a Decorator branch to defn_returns_none that inspects decorator.var.type (the type the call site actually sees).
  • test-data/unit/check-errorcodes.test: new testErrorCodeFuncReturnsValueStaticAndClassMethod covering instance, static, class methods on both an instance and the class itself.

37 lines added, 0 removed across 2 files.

Reproduce BEFORE/AFTER yourself (copy-paste)

# Set up
git clone https://github.com/python/mypy.git /tmp/mypy-bisect && cd /tmp/mypy-bisect
git remote add fork https://github.com/jbbqqf/mypy.git && git fetch fork
python -m venv .venv && . .venv/bin/activate
pip install -e . >/dev/null

cat > /tmp/repro_14179.py <<'PY'
class Example:
    def instance_op(self) -> None: return None
    @staticmethod
    def static_op() -> None: return None
    @classmethod
    def class_op(cls) -> None: return None

ex = Example()
a = ex.instance_op()
b = ex.static_op()
c = ex.class_op()
PY

# BEFORE — only the instance method is flagged (the bug)
git checkout master >/dev/null 2>&1
pip install -e . --quiet
python -m mypy /tmp/repro_14179.py || true
# Expected: 1 error on instance_op only (static_op and class_op missed)

# AFTER — all three methods flagged consistently
git checkout fork/fix/14179-staticmethod-func-returns-value >/dev/null 2>&1
pip install -e . --quiet
python -m mypy /tmp/repro_14179.py || true
# Expected: 3 errors, one on each of instance_op / static_op / class_op

What I ran locally

$ pytest mypy/test/testcheck.py -x -k "testErrorCodeFuncReturnsValueStaticAndClassMethod"
12 workers [1 item]
.                                                                        [100%]
============================== 1 passed in 1.29s ===============================

$ pytest mypy/test/testcheck.py -x
================= 8035 passed, 26 skipped, 7 xfailed in 36.84s =================

The new regression test fails verbatim on origin/master:

Failed: Unexpected type checker output ... line 685
Expected:
  main:18: error: "instance_op" of "Example" does not return a value ...
  main:19: error: "static_op" of "Example" does not return a value ...  (diff)
  main:20: error: "class_op" of "Example" does not return a value ...   (diff)
  ...
Actual:
  main:18: error: "instance_op" of "Example" does not return a value ...

Edge cases considered

Case Behavior Verified?
@staticmethod def f() -> None flagged yes (new test)
@classmethod def f(cls) -> None flagged yes (new test)
Plain instance method -> None flagged (unchanged) yes (new test)
Custom decorator preserving () -> None via TypeVar bound to Callable flagged yes (ad-hoc)
Custom decorator returning Callable[..., int] wrapping a -> None function NOT flagged (correct) yes (ad-hoc)
@staticmethod def f() -> int NOT flagged (correct) yes (ad-hoc)
Existing @property, @overload, @abstractmethod tests unchanged yes (378 decorator-related tests still pass)

Notes

  • The fix is intentionally tight: it routes the Decorator case through the same return-type check the other branches use, rather than e.g. peering at decorator.func.type (which would over-trigger for decorators that genuinely change the return type).
  • Added a short code comment with the issue link so a future reviewer reading defn_returns_none cold doesn't have to rediscover why Decorator needs special handling.

🤖 Disclosure: I'm a data engineer; this PR was prepared with help from an AI assistant (Claude) — I reviewed every change, wrote the regression test first, and validated locally on the full testcheck.py suite before pushing. Happy to iterate on review feedback.

Decorated functions — most commonly @staticmethod and @classmethod, but
also any user decorator that preserves a '-> None' return type — were
silently skipped by the func-returns-value check because Decorator
nodes were not handled in defn_returns_none. The synthetic Var carries
the decorated callable type, so use its return type directly (the Var
is flagged as inferred so the existing Var branch declined to use it).

Fixes python#14179.
@github-actions
Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

spark (https://github.com/apache/spark)
+ python/pyspark/sql/profiler.py:80: error: "zero" of "PStatsParam" does not return a value (it only ever returns None)  [func-returns-value]
+ python/pyspark/sql/profiler.py:80: error: "zero" of "MemUsageParam" does not return a value (it only ever returns None)  [func-returns-value]

colour (https://github.com/colour-science/colour)
+ colour/colorimetry/tests/test_tristimulus_values.py:1716: error: "assert_allclose" does not return a value (it only ever returns None)  [func-returns-value]
+ colour/colorimetry/tests/test_tristimulus_values.py:1730: error: "assert_allclose" does not return a value (it only ever returns None)  [func-returns-value]
+ colour/colorimetry/tests/test_tristimulus_values.py:1765: error: "assert_allclose" does not return a value (it only ever returns None)  [func-returns-value]
+ colour/colorimetry/tests/test_tristimulus_values.py:1779: error: "assert_allclose" does not return a value (it only ever returns None)  [func-returns-value]

zulip (https://github.com/zulip/zulip)
+ zerver/tests/test_message_fetch.py:2416: error: "sort" of "list" does not return a value (it only ever returns None)  [func-returns-value]
+ zerver/tests/test_message_fetch.py:2436: error: "sort" of "list" does not return a value (it only ever returns None)  [func-returns-value]

egglog-python (https://github.com/egraphs-good/egglog-python)
+ python/egglog/exp/any_expr.py:665: error: "setitem" does not return a value (it only ever returns None)  [func-returns-value]
+ python/egglog/exp/any_expr.py:668: error: "delitem" does not return a value (it only ever returns None)  [func-returns-value]

pandera (https://github.com/pandera-dev/pandera)
+ tests/pandas/test_decorators.py:756: error: "optional_in" does not return a value (it only ever returns None)  [func-returns-value]

pandas (https://github.com/pandas-dev/pandas)
+ pandas/core/series.py:3978: error: "_update_inplace" of "NDFrame" does not return a value (it only ever returns None)  [func-returns-value]
+ pandas/core/series.py:6806: error: "_update_inplace" of "NDFrame" does not return a value (it only ever returns None)  [func-returns-value]
+ pandas/core/frame.py:8338: error: "_update_inplace" of "NDFrame" does not return a value (it only ever returns None)  [func-returns-value]
+ pandas/core/frame.py:8348: error: "_update_inplace" of "NDFrame" does not return a value (it only ever returns None)  [func-returns-value]
+ pandas/core/frame.py:8363: error: "_update_inplace" of "NDFrame" does not return a value (it only ever returns None)  [func-returns-value]

jax (https://github.com/google/jax)
+ jax/_src/environment_info.py:63: error: "print" does not return a value (it only ever returns None)  [func-returns-value]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistency for [func-returns-value]: Error not reported for staticmethods

1 participant