I’d really love a no_discard or a must_use decorator.
I’ve seen multiple times in our codebases that there were bugs because people were working with immutable data and in one place, instead of using state = state.with_xyz(5) they had just state.with_xyz(5).
I don’t really think it’s worth making, as users can basically do anything with functions.
For example I sometimes did
try:
int(val)
except ValueError: ...
which is a totally valid use case IMO.
Also, when we were to do x = function_with_no_discard(), what guarantees whetherx is used later on or not. And even there it could again be dynamic (using exec, manipulating the namespace, using it conditionally, …).
We’ll never know wether to throw an error or not.
If we, for example do func(function_with_no_discard(), …) some random (keyword) argument being given can influence wether the return value is actually used. Should checkers really go that deep into the function (perhaps even more ones later on) just to check that? What if the result is only used if some random API, returns something specific.
Python, generally is way to dynamic for type checkers to not have to guess sometimes. But introducing even more cases where they’d have to guess doesn’t make it better.
And those making that kind of error a lot of times are, mostly beginners. Once you use type-checkers, you’ll automatically know wether the return value is None or not.
If the result is assigned to a variable, even if it _, or if the variable is never used, that’s still counted as used. If x isn’t used, you’ll probably get another warning about an unused variable so there is no need for duplicate warnings. If you assigned it to _ that shows you made a conscience effort to discard it so it’s probably fine.
I get this problem each time I require to remind myself if the function I call is an in-place modifier instead of a transform-and-return function. But that latter case is the regular case and the problem arises when an in-place modificator is involved.
One of these statements is nonsense, I can never remember which one :
list2 = sort(list1)
list3 = list1.sorted()
Imo the special rule should be stated on the ‘invalid return’ rather than on the ‘nodiscard’ one.
In this regard, this topic is related : "Wrong" Special Form
Agreed, and I want to underline that I really don’t think you need to invent an esoteric use-case to arrive at common real-world examples that this could help with.
This exact goof-up still occasionally bites me, particularly if I’ve just come from reading pseudocode or a different language and my attention slips for a second:
foo = "this is a string"
# ...
foo.replace(" ", "_")
# ...
print(foo)
That call to str.replace is perfectly valid code. And unless I’ve misconfigured something, neither pylint nor Pylance report that there’s anything wrong with it.
But I can remember a couple of times where the call to replace was one small step of a larger string-processing function and there weren’t any other calls to replace nearby, so the call looked correct at first glance and didn’t stand out as “not like the others”; as a result, the bug took an embarrassing amount of time to find.
I don’t have the technical chops to boldly assert that the no_discard decorator is the best, perfect solution to this, but I can at least say that some way to flag these as “you probably goofed this” would save real time for real people.
Just want to add a comment in favor of adding must_use or nodiscard . I work in a code base where we’ve found it useful to use a result type Result[T, E] as a return type in many places, because it serves as a strong nudge for the caller to handle the error and can be (mostly) enforced with type checking.
I say mostly, because while our Python Result type is similar to Rust’s Result type or C++'s std::expected, both of those benefit from a build-time check that the value is used, but the ability to do that that in Python is lacking. If the caller simply discards the Result value that is returned, there’s no feedback that an error case is being ignored. Adding must_use to Python’s typing library would complete Python’s ability to support this style of error handling.
This implementation does not have much to do with the current proposal as it performs a runtime check, whereas the proposal is for static type checking. In addition there is nothing stopping a developer from currently calling your .unwrap() method and then not using the value it returns.
I’m curious why you think it’s unrelated. Someone mentioned there’s no support for it in Python and while this very basic and lacks features that it should have, it demonstrates a potential path towards runtime support which could lead to mypy support. And there’s nothing stopping you from calling .unwrap() in rust and ignoring it either. That is actually a valid solution. Sometimes you really don’t care about a result, but the bigger problem is when you don’t realize that there’s a result that maybe you should care about. Making a lot of noise to try to force the developer to deal with the result, at least by actively throwing it away, helps catch those cases.
.unwrap() is entirely unrelated to #[must_use] in Rust. .unwrap() is common on container types like Result and Option to assert that the value is successful or present respectively, and to panic otherwise.
Option<T> isn’t annotated with #[must_use] so it can be ignored, unwrap or not. Result<T, E> is annotated with #[must_use] so it cannot be ignored without let _ or unwrapping.
#[must_use]
fn foo(i: u16) -> u16 {
i * i
}
fn opt(i: u16) -> Option<u16> {
Some(i)
}
fn result(i: u16) -> Result<u16, String> {
Ok(i)
}
fn main() {
opt(1).unwrap(); // NOT a compile time _warning_
opt(2); // NOT a compile time _warning_
result(3); // Compile time _warning_ because Result is annotated with #[must_use]
result(4).unwrap(); // NOT a compile time warning because it uses the Result, then ignores the Ok value
foo(3); // Compile time _warning_ because we annotated it with #[must_use]
let _ = foo(4); // NOT a compile time _warning_ because we explicitly throwaway the result with `let _`
println!("Hello World");
}
It sounds like calling it unwrap might confuse people that have difficulty understanding that Python and rust aren’t the same thing. Perhaps you can suggest a better name, because I only know enough rust to know that I never want to touch it again beyond the one app I have in production. What I built here is just a PoC demonstrating one way to implement runtime detection of missing error handling.