Opening files, then reading/writing to it, then closing it is very common in Python:
# Open a file, then read it into a variable
with open("savefile") as f:
raw_savefile = f.read()
# Open a JSON config file, then decode into a variable
import json
with open("config.json") as f:
config = json.load(f) #or json.loads(f.read())
with open("config.json") as f:
print(f.read())
etc. I think this kind of compressed alternative syntax will be helpful:
raw_savefile = f.read() with open("savefile") as f
import json
config = json.load(f) with open("config.json") as f
print(json.load(f) with open("config.json") as f)
This may already exist or may be already suggested or a bad idea in general.
Generally, just saving a line is not considered sufficient justification for a new language feature. And in any case, you can do this on one line already:
with open("savefile") as f: raw_savefile = f.read()
import json
with open("config.json") as f: config = json.load(f)
with open("config.json") as f: print(json.load(f))
Youāll need significantly more justification if you want this proposal to go anywhere (and even then, expect a lot of pushback).
from pathlib import Path
config = json.loads(Path("config.json").read_bytes())
Feels like we get suggestions to āoptimizeā context manager syntax relatively often. To me it seems folks bring up cases where context managers arenāt really necessary in the first place.
read_bytes is a convenience function wrapping a context manager. I donāt think itās quite accurate to say that context managers are unnecessary; rather, their verbosity might be giving rise to wrapper functions.
There was at least one recent thread proposing this same idea: A "with" variant for use in expressions . I feel like that thread kind of derailed, though, after not getting a lot of support.
Youāre certainly right that thereās not a big difference between those examples and the way they would be written with the proposed syntax, but I donāt think those examples illustrate places where this change would have its impact. In the thread I linked above, the proposal was similarly dismissed (by someone else) as being ācode golf,ā but to me this isnāt really about syntax, but rather about making context managers available in expressions.
Iām generally in favor of making more structures available in expressions, mostly because I tend to write code with a lot of comprehensions. If Iām writing something and decide that, for example, I want to do a json.load inside of a comprehension[1], my natural tendency is going to be just to use open(filename) and assume that things will get cleaned up properly, rather than unrolling things into a loop so I can use the context manager, or adding an import so I can use the pathlib stuff. I think it would be nice to have the āsafeā version available as an expression, in the same way that we have lambdas and if expressions and walruses and all that good stuff.
So my reaction hasnāt changed since the last thread: Iāve wanted this feature at multiple points in the past, and Iām pretty sure I would use it regularly if it were added.
Then I suppose what is lacking is examples / pointers to those places where this would be useful and nobody can come up with good workaround to achieve similar benefits.
If alternatives to achieve same benefit for all examples provided were given, then there is no footing left.
If you pass a string or bytes, thereās no way to recover the original filename. So, if we ever decide to improve the error messages, you would need to pass it manually or use a context manager:
from pathlib import Path
import jsonyx as json
config = json.loads(Path("config.json").read_bytes())
# File "<string>", line 1
# [,]
# ^
# jsonyx.JSONSyntaxError: Expecting value
path = Path("config.json")
config = json.loads(path.read_bytes(), filename=path)
# File "/Users/wannes/Downloads/config.json", line 1
# [,]
# ^
# jsonyx.JSONSyntaxError: Expecting value
with open("config.json", "rb") as fp:
config = json.load(fp)
# File "/Users/wannes/Downloads/config.json", line 1
# [,]
# ^
# jsonyx.JSONSyntaxError: Expecting value
Thatās also much less efficient. Itās loading the file contents in memory, and then parsing it, while json.load would read and parse the file at the same time IIRC.
But thatās just one example (and counter-arguments). I believe it would be useful to find actual uses of with blocks in the wild where this proposal would be helpful, and list these findings here, as suggested by @dg-pb.
I mean, I agree. My point was that if you have a single statement in the body of a context manager it could/should be replaced with a more concise API, rather than adding a new feature to the language.
One other point is that I think itāll be extremely difficult to find examples in application or library code where the proposed construct would be particularly beneficial. Conciseness comes a poor second to clarity in production code, and transforming to a normal with statement, or adding a helper function, are easy refactorings. So you hit the problem that itās easy, and at least as good - if not better - to do with existing language constructs.
IMO, the only place this construct is likely to be useful is in interactive code - the REPL, or maybe in a Jupyter notebook. In those places, concise code and one-liners are far more compelling. Enough to justify new syntax? I donāt know - probably not in my view, but others may disagree.
This desire to have expression forms is something that I understand, and generally see as legitimate, but do not like for context managers in particular.
A context manager is, to me, an API which is about indentation. It says āthereās a resource to manage, and you could use try-finally to do it, but I want the indentation in Python to show that structure such that the dedent is analogous to the final cleanup.ā
I say this because, to the point about pathlib APIs, thereās always a way to rephrase your API so that the context manager disappears from the layer at which you operate.
You mention comprehensions, which I think are a great example. The way I would personally restructure such code is typically to turn the comprehension body into a generator. e.g., iter_parsed_json_files(). That also means that the generator can have try-except, match-case, or any other syntactic form that I find useful as I iterate on the code.
I guess I would lean on try-except as my point of comparison here. Ruby has an expression form of it, rescue. Should Python add a rescue soft keyword, to allow an expression to contain try-except? More generally, are there any tools which we have in the language which should not go into expressions? What are they and why?
Please donāt take this as a slippery slope argument! I just mean to ask probing questions about āwhatās special about context managers?ā
I say all of this as a huge fan of the walrus. It has a relatively small number of extremely compelling use cases in which it greatly enhances clarity, and a more moderate number where it adds concision without harming clarity. The argument for āwhy allow assignments in expressionsā starts (and pretty much ends) with ālet me show you these use casesā.
This is the use-case I had in mind, quick loops involving file operations in the REPL, which I tend to use a lot.
Agree completely. And just to be clear, Iām not arguing that my use case is big enough to be worth the trouble; just saying that I would probably use this if it existed.
Yes, the tension between comprehensions and loops is unfortunate. If this is such a common thing for you, you may want to propose putting the with directly into the comprehension:
y = [json.load(f)
for filename in filenames
with open(filename) as f]
Iām aware of the āthere should be one ā and preferably only one ā obvious way to do itā principle. Which does appear to argue against this proposal, because there is an obvious way to do it.
However, that principle offers less guidance when thereās a more obvious way to do it. At least in my code I suspect more than half the with statements would be more naturally written as an expression. It seems like a small quality of life improvement for many users in many circumstances weighs less heavily than making something very difficult much easier for a few people in uncommon circumstances?
I notice the walrus (:=) operator entered the language back in 2018, presumably with justifications very similar to those for what is being proposed here?
I think talking about the walrus is a completely different topic, and itās history is far from straight forward.
Just changing the syntax ācause the walrus changed syntax tooā is not really an argument. Most syntax sugars are for making long code be smaller. Here however I donāt really see how the current state is too long.
Well, itās correct that there are many statements that this could shorten. A simple GitHub code search suggests around 12.2% of Python with statements are simply storing f.read() into a variable, and ~7% are just storingjson.load(f) into a variable, for a total of around 19%. Adding with support in expression helps much more than just these examples, and these alone contribute to slightly less than one fifth of all with statements.
To me a with expressions are a sign that youāre doing something complicated. And itās more than fine that if you want to do something complicated, the code should look a little complicated too. To make hard things look simple is lying. (Or good programming. It depends )
Thereās a reason .open() is traditionally a context instead of us just using expressions like json.read(f.open()). I can use it as json.read(f.open()), and besides my linter complaining nothing goes wrong. So far
At the same time, you can write (as long as f is a Path instance) f.read_text() or json.loads(f.read_text()) if you want a simple expression. You lose something in that conversion. (Some fancy optimisation tricks mostly.) Thatās fine. I canāt think of any case where Iāve needed both the advantages of the context manager, and the advantages of the expression. If I ever do need both, Iāll write a small function.