In Python, what does `with obj as x` bind to `x`, and which two methods must `obj` define?
answer
- Two methods, no base class
- The `as` target is not the manager
- Whatever `__enter__` returns gets bound
- Special methods resolve on the type
- `return self` is a convention, not a rule
basics
~20 sx is bound to whatever __enter__ returns, not to obj itself. Any object usable in a with statement must define both __enter__ and __exit__; __exit__ runs on the way out whether the block succeeded or raised.
solid answer
~40 sThe `with` statement is sugar over a two-method protocol. Python calls `type(obj).__enter__(obj)` on entry and binds the **return value** of `__enter__` to the `as` target — the manager object itself only if `__enter__` does `return self`, which is the common convention but not a rule. On the way out Python calls `type(obj).__exit__(obj, exc_type, exc_value, tb)`, and that call is guaranteed on every exit path: normal fall-through, `return`, `break`, or a propagating exception. Both methods are looked up on the **type**, not the instance, so attaching `__enter__` to a single instance does nothing. An object missing either method raises `TypeError` saying it does not support the context manager protocol — and it raises before the block body runs.
code
python · 15 linesclass Tag:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"<{self.name}>")
return self.name.upper()
def __exit__(self, exc_type, exc_value, tb):
print(f"</{self.name}>")
return None
with Tag("section") as t:
print(t, type(t).__name__)go deeper
Be ready to state the two method names and to say plainly that as receives the return value of __enter__. Being able to sketch a five-line manager class on a whiteboard is the whole bar here.
Explain the desugaring: entry call, body inside a try, __exit__ in the finally path so it runs on return and break too. Know that a missing method fails before the block body ever executes.
Show judgement about what __enter__ should hand back — the manager for introspection afterwards, or a bare resource to keep the manager out of the block. Explain why with guarantees cleanup against exceptions but not against process death.
Own the API-shape decision: whether a subsystem exposes resources as context managers at all, whether entry returns a handle or self, and how that choice constrains callers who need to hold the resource across function boundaries.
### The protocol A *context manager* is any object whose **type** defines two methods: * `__enter__(self)` — runs before the block body. Its **return value** is what the `as` target receives. If there is no `as` clause the return value is simply discarded. * `__exit__(self, exc_type, exc_value, tb)` — runs after the block body, on every exit path. That is the whole contract. There is no base class to inherit from, no registration, no decorator required: a plain class with those two methods is a context manager, which is why `with` works uniformly across locks, files, timers, database transactions and one-off helpers you write in five lines. ### What `with` actually compiles to The language reference gives the equivalent source for `with EXPR as TARGET: BODY`: ```python mgr = (EXPR) enter = type(mgr).__enter__ exit = type(mgr).__exit__ value = enter(mgr) hit_except = False try: TARGET = value BODY except: hit_except = True if not exit(mgr, *sys.exc_info()): raise finally: if not hit_except: exit(mgr, None, None, None) ``` Four things fall straight out of that expansion. **1. `as` binds the return value, not the manager.** `TARGET = value`, where `value` came from `__enter__`. The reason `with` over a file gives you the file, and `with` over a lock gives you `True` rather than the lock, is simply what each `__enter__` chose to return. When you write your own manager and want the `as` target to be the manager, you must write `return self` explicitly; forget it and `__enter__` returns `None`, and the block gets `None` bound to a confident-looking name. **2. The lookup is on the type.** `enter = type(mgr).__enter__` — not `mgr.__enter__`. This is the general rule for special methods in CPython: implicit dunder invocations bypass the instance dictionary. Monkey-patching `obj.__enter__ = something` on one instance has no effect on `with obj:`; you have to patch the class. **3. Both lookups happen *before* the body runs.** An object that defines only `__enter__` fails immediately with a `TypeError` reporting that the object does not support the context manager protocol — you never see a partially executed block. **4. `__exit__` is a `finally`, not an `except`.** It runs on normal completion, on `return`/`break`/`continue` out of the block, and when an exception propagates. It does *not* run if the process dies outright — a hard interpreter exit or a signal that kills the process leaves cleanup undone, which is why `with` protects against exceptions, not against `kill -9`. ### The `as` target is a full assignment target `as` is not restricted to a bare name. It accepts any assignment target, so tuple unpacking works when `__enter__` returns a tuple: ```python with open_pair() as (reader, writer): ... ``` and attribute or subscript targets (`as self.conn`, `as slots[0]`) are legal too, though rarely good style. The unpacking happens *inside* the `try`, which matters: if unpacking raises, `__exit__` still runs. ### Convention: return `self`, or return a handle Two shapes are both idiomatic. Returning `self` lets the block call methods on the manager (`with Timer() as t: ...; t.elapsed`). Returning a *different* object hides the manager entirely and hands the block only the resource it needs — a connection, a cursor, an open handle. Choose deliberately: the second shape means the manager object is unreachable from inside the block, which is usually what you want when the manager exists only to sequence setup and teardown. ### Why interviewers ask this Hand-rolling a context manager is a five-minute whiteboard exercise, and two mistakes are extremely common: omitting `return self` from `__enter__`, and giving `__exit__` the wrong arity. Both are visible in ten seconds of reading, and both reveal whether the candidate has ever written a manager or only used one. ```python class Transaction: def __init__(self, conn): self.conn = conn def __enter__(self): self.conn.begin() return self.conn # the block gets the connection def __exit__(self, exc_type, exc_value, tb): if exc_type is None: self.conn.commit() else: self.conn.rollback() ``` That manager returns nothing truthy from `__exit__`, so any exception continues to propagate after the rollback — the default and usually the correct choice. ### One thing `with` does not do It does not catch anything on your behalf. The construct is a `try/finally`, not a `try/except`: the exception is handed to `__exit__` so cleanup can see it, and unless that method's return value explicitly says otherwise it carries on propagating once teardown is done. A block that must *handle* a failure still needs its own `try/except` around it. The manager guarantees that teardown happens; deciding what a failure means is the caller's policy, and mixing the two into one class is how managers become untestable. That division is the reason the protocol is only two methods long, and it is worth saying out loud when you are asked to hand-roll one.
- What happens if `__enter__` has no `return` statement at all?It returns `None`, so the `as` target is bound to `None`. The block then fails on the first attribute access with `AttributeError: 'NoneType' object has no attribute ...`, which is confusing because the traceback points at the block, not at the manager. Any manager whose block is meant to use it must end `__enter__` with `return self` or with the resource it hands out.
- Can you use `with` without an `as` clause, and when is that the right shape?Yes — `as` is optional and the `__enter__` return value is discarded. That is the right shape whenever entering has an effect the block does not need a handle for: taking a lock, redirecting output, changing a global setting, timing a region whose result you read from the manager afterwards. Omitting `as` also signals to a reader that the block does not interact with the manager.
- Why does patching `__enter__` onto a single instance fail to change `with` behaviour?Implicit special-method invocation looks the method up on the type, not through the instance dictionary — the desugaring literally reads `type(mgr).__enter__`. This is uniform across dunders (`__len__`, `__iter__`, `__eq__` behave the same way) and exists so CPython can cache and specialize those slots. To change the behaviour you patch the class, or use a subclass or a wrapper object.
with is a valet ticket: you hand over the car (__enter__) and get back whatever the valet chooses to give you — usually a ticket, not the car — and the return leg runs whether you leave happy or storm out.
saying these in an interview costs you the question
- Says `as` binds the context-manager object itself
- Thinks a base class must be inherited to use `with`
- Writes `__enter__` with no `return self`
- Believes `__exit__` only runs when an exception occurs
- Claims `with` catches exceptions by itself
- Patches the dunder on the instance and expects it to take effect