unused return value¶
a call written as a statement of its own keeps whatever the call did on the way to its answer, and
throws the answer away. unused-return-value reports that
the classic instance is a method that looks like it changes its receiver and does not — str is
immutable, so the stripped string was the whole result
a function that returns None has no answer to throw away, so nothing is reported
neither is a call that does not come back at all (-> Never), nor one whose result is gradual —
there is nothing known to have been discarded
opting out¶
some results really are optional. @ignorable_return_value says so, and calls to that function stop
being reported
@ignorable_return_value
def prime_cache(key: str) -> bytes: ...
prime_cache("k") # ok — the call was for the caching
the marker is available without importing it, like the rest of basedpython's vocabulary, and it leaves no trace in the emitted python — nothing runs, the checker just reads it
on a class it covers every method the class body defines, and constructing the class. that is the shape of a fluent builder, where each step answers with the receiver it was given
@ignorable_return_value
class Query:
def where(self, clause: str) -> Query:
return self
@must_use_return_value
def rows(self) -> list[str]:
return []
q = Query()
q.where("id = 1") # ok — the class is marked
q.rows() # warning: `rows` is marked back
@must_use_return_value is the only way back inside a marked class. on a declaration that carries
both markers it wins, since it is the more specific of the two
discarding one result¶
the markers say what a declaration means, so they are the wrong tool for a single call — and for
a function someone else declared there is nothing to mark. assigning the result to _ says the
discard was meant, at the one place it was meant
import ast
def check(source: str) -> bool:
try:
_ = ast.parse(source) # called for the `SyntaxError`, not the tree
except SyntaxError:
return False
return True
nothing is reported for it: the result was used, by a name that says it is going nowhere
the standard library¶
the stdlib is checked like anything else, and the members whose result is idiomatically thrown away carry the marker in basedpython's own stubs
entries = [1, 2]
entries.pop() # ok — `pop` is for the removing
handle.write("done") # ok — the byte count is for a partial write
subprocess.run(["ls"]) # ok
"done".strip() # warning
the bar for a member is that discarding must be idiomatic, not merely common: path.read_text()
discarded is a bug, path.write_text(...) discarded is how it is written
a statement expression's value¶
the last expression of a branch is what a statement expression produces, so it is read by whatever the statement expression stands in, and nothing is reported for it. a call written earlier in the same branch is discarded like any other
def parse(text: str) -> int: ...
def f(c: bool) -> int:
return if c:
parse("1") # warning: this one goes nowhere
parse("2") # ok — this is the value
else:
0
coroutines¶
a coroutine that reaches the end of a statement is missing its await, which is
unused-awaitable's to report — so it is not reported twice. once awaited, what the coroutine
answered with is an ordinary result