Python made the aesthetic choice to separate statements from expressions, and there is a broad (though non-mandatory) API convention in Python that functions/methods should either be used for their value OR for side effects.
Returning None encourages the caller to use it as a statement, and visually looks like “It’s not assigned to anything so surely this had a side effect, or the call would be useless?” And statement order is unambiguous.
Whereas returning a value allows use in middle of complex expressions, where order is harder to figure out, and one tends to assume refactors like extracting and naming a sub-expression are safe.
The canonical example is list.sort() method which returns None but mutates the list in-place.
Repeated requests to make it return the result for convenience were denied, to keep it obvious to the user that there was a side effect here; instead, a sorted() function was introduced which returns a new list, leaving the original list unmodified.
Can you elaborate why exactly you feel
(toto := tutu / "toto").mkdir()
is “not very pythonic”?
I’d say that’s because it only uses := to avoid an extra line; it’s trivial to rewrite as two lines, and the price of the statement/expression dichotomy is we don’t generally shy away from using multiple lines.
However, if you prefer a single line here, go ahead and use := like that! I’d argue it’s actually clearer than toto = (tutu / "toto").mkdir() because there is no doubt what value toto got — in the latter case one would have to look up “what does mkdir() return?”
It’s probably because Python’s older os.mkdir doesn’t return anything, which was probably because the POSIX command mkdir doesn’t return a directory either.
I hadthe same king of feeling with list.sort() actually. Thanks for explaining the rational behind these choices. It’s very reasonable and make it clearer for newcomers. Maybe I’m already too old and start having idiotic ideas.
That’s what people said when I requested it on SO I personally like it.
In most cases if I get an error during mkdir I want the code to stop because the folder won’t be there so I don’t see it as a issue but more a feature.
You still get the exception with the 2-line version, but unlike the one-liner, the variables will exist, so your error reporting can be more informative
Trying to do things in one line just because of an aesthetic sense is also not very pythonic.
The rule in Python is command-query separation - if the library communicates back to you primarily by changing something (mutating one of the arguments that was passed in, or modifying the external environment), that’s a “command” which should not alsoreturn a meaningful value (as a “query” would).
As @pf_moore points out, it also makes exception handling harder. If mkdir raises an exception, then the proposed new way of doing things would also not assign toto even if the exception is handled. In the exception handler, having toto assigned would be useful, in order to report the path that failed to be created, right?
API design is hard, though. If mkdir were to return an open file descriptor to the newly-created directory, that would make a lot of sense, and enable some elegant atomic operations (make a directory, then use os.open(..., dir_fd=...) to perform operations within that dir). So really, the only reason that this isn’t a good idea is that the resultant directory name (which was proposed to be the return value) can be completely calculated before the directory exists.
the deprecated pylib had a own name for combining command and query
localpath.ensure("subdir", dir=True)
but based on the knowledge gained from dealing with it, i would rather propose having a wrapper around path objects that manages “dwim” semantics, so path itself can be “dumb” and non-magical
To jump on what was explained earlier on list.sort and here path.mkdir why does the string.strip do have return statement ?
If I follow the logic presented by @cben, there should be no return statement and a stripped function ? So if there is an exception for string object why not the others ?
A string object cannot be modified in-place from within Python; therefore, every method it provides will create and return a new string instead. It just happens that they called that one “strip” when calling it “stripped” would make more sense (because it does return). But it’s way too late to change that now. Names are hard.