Why `__enter__` Must Return Something in Python
If `__enter__` returns nothing, Python assigns `None` to your `with` statement variable—leading to confusing AttributeErrors. Learn the core contract, the fix, and when you can break the rules.
Why Your Python Code Breaks When __enter__ Returns Nothing
We have all been there — writing a clean with statement, expecting everything to work smoothly, only to be hit by an AttributeError that makes no sense at first glance. But here is the thing: the __enter__ and __exit__ contract in Python is subtle, and misusing it can lead to bugs that are hard to spot.
Let me break this down for you. At PythonSkillset, we have seen many developers trip over this. And it is not your fault — the documentation could be a bit friendlier.
The Core Contract
When you write something like:
with open("file.txt") as f:
content = f.read()
What actually happens is this sequence:
- Python calls
__enter__()on the object - Whatever
__enter__()returns gets assigned to the variable afteras(in this case,f) - Your code executes inside the block
- When done or on exception,
__exit__()is called
Here is what trips people up: __enter__ must return something, but it does not have to return self.
The Common Mistake
Consider this code that looks perfectly reasonable:
class DatabaseConnection:
def __init__(self):
self.connection = None
def __enter__(self):
self.connection = connect_to_database()
# Whoops! Forgot to return anything
def __exit__(self, exc_type, exc_val, exc_tb):
self.connection.close()
If you use this as:
with DatabaseConnection() as db:
db.query("SELECT * FROM users")
Guess what? db will be None. And then your .query() call will produce an AttributeError that makes you scratch your head for hours. At PythonSkillset, we have debugged this exact scenario more times than we care to count.
The fix is simple, but non-obvious:
def __enter__(self):
self.connection = connect_to_database()
return self # This is crucial!
Why This Happens
Python does not force __enter__ to return anything. If you do not explicitly return, Python returns None by default. The with statement does not check whether the returned value makes sense — it just blindly assigns whatever __enter__ returns to your variable.
But Wait — Sometimes You Do Not Want self
Here is where it gets interesting. Sometimes you actually want __enter__ to return something different from the context manager itself. This is perfectly valid and useful:
class ManagedFile:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.file = open(self.filename, 'r')
return self.file # Return the file object, not self
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
Now you can use it like:
with ManagedFile("data.txt") as f:
for line in f: # f is the file object, not the ManagedFile instance
print(line)
This pattern is exactly what the built-in open() function uses — it returns the file object from __enter__.
The Exception to the Rule
There is one case where you can get away without returning anything: when you do not use the as clause at all.
with DatabaseConnection(): # No 'as db' here
# We know the connection exists, but we cannot access it directly
do_something_with_hidden_connection()
In this case, since you are not assigning the returned value, it does not matter that __enter__ returns None. But honestly, this is rare and usually indicates a design that could be improved.
Real-World Example from PythonSkillset
Last month, a reader named Sarah from a fintech startup reached out. Her team had:
class TransactionManager:
def __init__(self, db):
self.db = db
def __enter__(self):
self.transaction = self.db.begin_transaction()
# Missing return!
def __exit__(self, *args):
if args[1]: # An exception occurred
self.transaction.rollback()
else:
self.transaction.commit()
They could not figure out why db.begin_transaction() was getting called but their queries were failing. After we pointed out the missing return self, they had their "aha" moment.
The Golden Rule
Always ask yourself: What should the variable after as refer to?
- If it should be the context manager itself, return
selffrom__enter__. - If it should be some resource (file, connection, etc.), return that resource.
- If you are not using
as, you can technically return anything or nothing — but think twice about your design.
And remember, Python will not warn you. It will silently assign None and let you crash later. The contract is simple: __enter__ returns what you want to work with, __exit__ cleans up. Follow that, and your context managers will serve you well.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.