Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Python

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.

July 2026 5 min read 1 views 0 hearts

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:

  1. Python calls __enter__() on the object
  2. Whatever __enter__() returns gets assigned to the variable after as (in this case, f)
  3. Your code executes inside the block
  4. 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 self from __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.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.