Now that PEP 572 has been accepted, Python 3.8 is destined to have assignment expressions, so we can use an assignment expression in with, i.e.
with (f := open('file.txt')): for l in f: print(f) instead of
with open('file.txt') as f: for l in f: print(f) and it would work as before.
What use does the as keyword have with the with statement in Python 3.8? Isn't this against the Zen of Python: "There should be one -- and preferably only one -- obvious way to do it."?
When the feature was originally proposed, it wasn't clearly specified whether the assignment expression should be parenthesized in with and that
with f := open('file.txt'): for l in f: print(f) could work. However, in Python 3.8a0,
with f := open('file.txt'): for l in f: print(f) will cause
File "<stdin>", line 1 with f := open('file.txt'): ^ SyntaxError: invalid syntax but the parenthesized expression works.
The error is a simple typo: x = 0, which assigns 0 to the variable x, was written while the comparison x == 0 is certainly what was intended.
Assignment expression are written with a new notation (:=) . This operator is often called the walrus operator as it resembles the eyes and tusks of a walrus on its side. Assignment expressions allow you to assign and return a value in the same expression.
Python 3.8, released in October 2019, adds assignment expressions to Python via the := syntax. The assignment expression syntax is also sometimes called “the walrus operator” because := vaguely resembles a walrus with tusks. Assignment expressions allow variable assignments to occur inside of larger expressions.
TL;DR: The behaviour is not the same for both constructs, even though there wouldn't be discernible differences between the 2 examples.
You should almost never need := in a with statement, and sometimes it is very wrong. When in doubt, always use with ... as ... when you need the managed object within the with block.
In with context_manager as managed, managed is bound to the return value of context_manager.__enter__(), whereas in with (managed := context_manager), managed is bound to the context_manager itself and the return value of the __enter__() method call is discarded. The behaviour is almost identical for open files, because their __enter__ method returns self.
The first excerpt is roughly analogous to
_mgr = (f := open('file.txt')) # `f` is assigned here, even if `__enter__` fails _mgr.__enter__() # the return value is discarded exc = True try: try: BLOCK except: # The exceptional case is handled here exc = False if not _mgr.__exit__(*sys.exc_info()): raise # The exception is swallowed if exit() returns true finally: # The normal and non-local-goto cases are handled here if exc: _mgr.__exit__(None, None, None) whereas the as form would be
_mgr = open('file.txt') # _value = _mgr.__enter__() # the return value is kept exc = True try: try: f = _value # here f is bound to the return value of __enter__ # and therefore only when __enter__ succeeded BLOCK except: # The exceptional case is handled here exc = False if not _mgr.__exit__(*sys.exc_info()): raise # The exception is swallowed if exit() returns true finally: # The normal and non-local-goto cases are handled here if exc: _mgr.__exit__(None, None, None) i.e. with (f := open(...)) would set f to the return value of open, whereas with open(...) as f binds f to the return value of the implicit __enter__() method call.
Now, in case of files and streams, file.__enter__() will return self if it succeeds, so the behaviour for these two approaches is almost the same - the only difference is in the event that __enter__ throws an exception.
The fact that assignment expressions will often work instead of as is deceptive, because there are many classes where _mgr.__enter__() returns an object that is distinct from self. In that case an assignment expression works differently: the context manager is assigned, instead of the managed object. For example unittest.mock.patch is a context manager that will return the mock object. The documentation for it has the following example:
>>> thing = object() >>> with patch('__main__.thing', new_callable=NonCallableMock) as mock_thing: ... assert thing is mock_thing ... thing() ... Traceback (most recent call last): ... TypeError: 'NonCallableMock' object is not callable Now, if it were to be written to use an assignment expression, the behaviour would be different:
>>> thing = object() >>> with (mock_thing := patch('__main__.thing', new_callable=NonCallableMock)): ... assert thing is mock_thing ... thing() ... Traceback (most recent call last): ... AssertionError >>> thing <object object at 0x7f4aeb1ab1a0> >>> mock_thing <unittest.mock._patch object at 0x7f4ae910eeb8> mock_thing is now bound to the context manager instead of the new mock object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With