Why Your Python with‑Statement Fails: Hidden Pitfalls of __enter__ and __exit__ in Global Locks
This article explores the subtle bugs that arise when using Python's with‑statement for a global process lock, explains why exceptions raised in __enter__ are not caught by __exit__, and presents three practical solutions using contextlib utilities and simple control flow.
Problem Origin
Earlier I used with to implement a global process lock, hoping to achieve mutual exclusion across processes.
The lock itself relies on an external cache; on Redis it is implemented with SETNX, sometimes combined with cache‑penetration protection and random delays to reduce pressure on the cache.
I wrote unit tests that seemed to verify the code:
The tests passed perfectly.
The lock works by raising an exception in __enter__ and catching it in __exit__:
At first glance this looks fine because the unit tests succeed, but the implementation is flawed: the execution of __exit__ is not wrapped around __enter__, so an exception raised in __enter__ is never caught by __exit__.
The original tests passed only because there were two nested with statements; the outer with happened to catch the exception raised by the inner __enter__.
When I rewrote the test to reflect the real execution order, the test failed:
This issue also appeared in an AB‑testing scenario where __enter__ raised an exception and __exit__ attempted to catch it, but the exception never reached __exit__:
First Solution
Understanding the execution order of with suggests a simple fix: after __enter__ completes, call a helper function to verify the state. I rewrote ABContext accordingly:
Usage example:
However, this approach is not elegant; forgetting to call the helper defeats the purpose and the code loses its Pythonic simplicity.
Second Solution
Looking at the contextlib documentation, I found the now‑deprecated contextlib.nested, which allowed multiple context managers in a single with statement.
After Python 2.7 the same effect can be achieved directly with nested with clauses:
Using this idea, I let the first context’s __exit__ catch the exception while the second context’s __enter__ raises it:
Combined with the earlier ABContext usage, the test now passes:
Good, the unit test succeeds!
Third Solution
Sometimes the simplest fix is to avoid a second context altogether and use a plain if statement:
TIL
In summary, I learned several useful functions and decorators from contextlib, discovered that a with block can hold a single context, and realized that dynamically constructing multiple contexts still needs research—especially since the code block after with cannot be a tuple or list.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
