Pythonのwith文とは、「使い始めの準備」と「使い終わりの後片付け」を自動で実行してくれる構文です。 with open("memo.txt") as f: と書くと、ブロックを抜けた時点でファイルが必ず閉じられます。途中で例外が起きても、return で抜けても、後片付けは実行されます。

この「準備と後片付け」を受け持つオブジェクトをコンテキストマネージャと呼び、その正体は __enter__ と __exit__ という2つのメソッドを持つオブジェクトです。この記事では、with ありとなしのコードを並べ、3つの処理がどの順で呼ばれるかを print の実行結果で確かめます。 例外が起きたときに __exit__ の3つの引数へ何が入るか、True を返すと例外が外へ伝わらなくなる挙動も再現しました。自作クラス版と @contextmanager 版のコピペ用コード、括弧付きの複数 with、async with、with で使える標準機能の一覧表まで、すべて Python 3.14.7 で実行した結果つきでまとめています。

with文の早見表

知りたいこと 答え
基本の形 with 式 as 変数: の下に、字下げして本体を書く
呼ばれる順番 __enter__ → 本体 → __exit__(例外や return で抜けても __exit__ は呼ばれる)
as の右に入るもの __enter__ の戻り値(式そのものではない)。不要なら as は省略できる
__exit__ の引数 (exc_type, exc_value, traceback)。例外がなければ3つとも None
__exit__ の戻り値 True(真の値)を返すと例外を抑制する。False や None なら例外はそのまま外へ伝わる
複数まとめて書く with A() as a, B() as b:。入るのは左から、出るのは右から
複数行に分ける with (A() as a, B() as b): と括弧で囲む(3.10以降)
非同期の処理 async with。__aenter__ と __aexit__ が呼ばれる
自作する方法 クラスに2つのメソッドを書く、または @contextlib.contextmanager を関数に付ける
with を抜けたあとの変数 変数は残る。ファイルなら閉じた状態で残る

with でよく使う標準機能は次のとおりです(詳しい一覧は後半にあります)。

書き方 抜けるときに行われること
with open(path) as f: ファイルを閉じる
with lock: ロックを解放する
with sqlite3.connect(...) as con: コミット、例外時はロールバック(接続は閉じない)
with tempfile.TemporaryDirectory() as d: 一時フォルダを中身ごと削除する
with contextlib.suppress(FileNotFoundError): 指定した例外を抑制する
with contextlib.redirect_stdout(buf): 標準出力の向き先を元に戻す
with contextlib.ExitStack() as stack: 登録した後片付けを逆順に実行する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python with文の使い方|コンテキストマネージャと__enter__」

with文とは:ありとなしを並べて比べる

ファイルに書き込む処理を、with なしと with ありで並べます。

# with なし:閉じる処理を自分で書く
f = open("memo.txt", "w", encoding="utf-8")
try:
    f.write("hello\n")
finally:
    f.close()          # 例外が起きても必ず閉じる

# with あり:閉じる処理は書かなくてよい
with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("hello\n")

with なしで同じ安全さを得るには、try と finally を書いて close() を呼ぶ必要があります。close() を書き忘れたり、try の外に書いたりすると、途中で例外が起きたときにファイルが開いたままになります。with 文は、この「try / finally と後片付けの呼び出し」を1行にまとめたものと考えると、仕組みがつかみやすくなります。

比べる点 with なし(try/finally) with あり
行数 5行 2行
後片付けの書き忘れ 起こりうる 起こらない(自動で呼ばれる)
例外が起きたとき finally を書いていれば後片付けされる 必ず後片付けされる
後片付けの中身を知る必要 ある(close()、release() など) ない(オブジェクト側が知っている)
字下げの範囲 使っている範囲が読み取りにくい 字下げされた部分が「使っている範囲」

呼ばれる順番を実行結果で確かめる

__enter__ と __exit__ に print を入れたクラスを用意して、with ありと with なしを続けて実行します。

class Room:
    def __init__(self, name):
        self.name = name
        print(f"0. __init__   {name}")

    def __enter__(self):
        print("1. __enter__  入る準備")
        return self                      # as の右に入る値

    def __exit__(self, exc_type, exc_value, traceback):
        print("3. __exit__   後片付け", exc_type, exc_value, traceback)
        return False                     # 例外はそのまま外へ伝える


print("--- with あり")
with Room("会議室A") as room:
    print("2. 本体       ", room.name)
print("4. with の外")

print("--- with なし(try/finally で同じことを書く)")
room = Room("会議室B")
room.__enter__()
try:
    print("2. 本体       ", room.name)
finally:
    room.__exit__(None, None, None)
print("4. try の外")
--- with あり
0. __init__   会議室A
1. __enter__  入る準備
2. 本体        会議室A
3. __exit__   後片付け None None None
4. with の外
--- with なし(try/finally で同じことを書く)
0. __init__   会議室B
1. __enter__  入る準備
2. 本体        会議室B
3. __exit__   後片付け None None None
4. try の外

どちらも __init__ → __enter__ → 本体 → __exit__ → 外 の順で、まったく同じ出力になりました。with 文がしていることを順に並べると、次のようになります。

順番 with 文がすること 上のコードでの対応
1 with の右の式を評価して、コンテキストマネージャを得る Room("会議室A") で __init__ が動く
2 __enter__() を呼ぶ 「1. __enter__」が表示される
3 as があれば、__enter__() の戻り値を変数に入れる room に self が入る
4 字下げされた本体を実行する 「2. 本体」が表示される
5 __exit__() を呼ぶ。例外がなければ引数は3つとも None 「3. __exit__ … None None None」

今回開いた公式の言語リファレンス(複合文の with 文の節)でも、with 文は次のコードと「意味として等価」と説明されています。

manager = (EXPRESSION)
enter = manager.__enter__
exit = manager.__exit__
value = enter()
hit_except = False

try:
    TARGET = value
    SUITE
except:
    hit_except = True
    if not exit(*sys.exc_info()):
        raise
finally:
    if not hit_except:
        exit(None, None, None)

EXPRESSION が with の右の式、TARGET が as の変数、SUITE が本体です。例外が起きたら exit に例外の情報を渡し、戻り値が偽なら raise で例外を投げ直す、という流れが読み取れます。この部分が、次に見る「3つの引数」と「True を返すと抑制される」挙動の出どころです。

asに入るのは __enter__ の戻り値

as の右の変数に入るのは、with の右に書いた式そのものではなく、__enter__() が返した値です。open() が返すファイルオブジェクトは __enter__ で自分自身を返すので、どちらでも同じに見えます。違いがわかる例を実行します。

class ReturnsSelf:
    def __enter__(self):
        return self

    def __exit__(self, *exc):
        return False


class ReturnsNothing:
    def __enter__(self):
        pass                  # return を書いていない

    def __exit__(self, *exc):
        return False


class ReturnsTuple:
    def __enter__(self):
        return ("a.txt", 3)   # 自分以外の値を返してもよい

    def __exit__(self, *exc):
        return False


with ReturnsSelf() as x:
    print(type(x).__name__)

with ReturnsNothing() as x:
    print(x)

with ReturnsTuple() as (name, count):
    print(name, count)

with open(__file__, encoding="utf-8") as f:
    first = f.readline()
print(f.closed, len(first))        # with を抜けても変数 f と first は残る
ReturnsSelf
None
a.txt 3
True 19

自作クラスで as の変数が None になるときは、__enter__ に return self を書いていないことがほとんどです。また、with はスコープ(変数の有効範囲)を作りません。 ブロックを抜けたあとも f は残っていて、閉じた状態になっています。閉じたファイルに書き込もうとすると ValueError: I/O operation on closed file. になります。

例外が起きたときの後片付けメソッドの3引数

本体で例外が起きると、__exit__ には例外の情報が3つ渡されます。戻り値を False にした場合と True にした場合を比べます。

class Show:
    def __init__(self, swallow):
        self.swallow = swallow

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("  exc_type :", exc_type)
        print("  exc_value:", repr(exc_value))
        print("  traceback:", traceback)
        return self.swallow


print("A. 例外なし")
with Show(False):
    pass

print("B. 例外あり・False を返す")
try:
    with Show(False):
        1 / 0
        print("  この行は実行されない")
except ZeroDivisionError as e:
    print("  外側で受け取った:", e)

print("C. 例外あり・True を返す")
with Show(True):
    1 / 0
    print("  この行は実行されない")
print("  例外は外へ伝わらず、with の次の行へ進む")
A. 例外なし
  exc_type : None
  exc_value: None
  traceback: None
B. 例外あり・False を返す
  exc_type : <class 'ZeroDivisionError'>
  exc_value: ZeroDivisionError('division by zero')
  traceback: <traceback object at 0x7f03bb9a6e40>
  外側で受け取った: division by zero
C. 例外あり・True を返す
  exc_type : <class 'ZeroDivisionError'>
  exc_value: ZeroDivisionError('division by zero')
  traceback: <traceback object at 0x7f03bb9a6e80>
  例外は外へ伝わらず、with の次の行へ進む
引数 例外がないとき 例外があるとき 使い道
exc_type None 例外のクラス(ZeroDivisionError など) 例外の種類で処理を分ける
exc_value None 例外のインスタンス メッセージを取り出す、ログに残す
traceback None トレースバックオブジェクト 発生した場所を記録する

Trueを返すと例外が抑制される

C の結果のとおり、__exit__ が True(真と判定される値)を返すと、例外はそこで消えて、with の次の行から処理が続きます。 いわゆる「例外を握りつぶす」状態です。本体の残りの行(1 / 0 の次の print)は実行されません。

__exit__ の戻り値 例外の扱い 向いている場面
return を書かない(None) そのまま外へ伝わる ほとんどの場合はこれでよい
False そのまま外へ伝わる 「伝える」ことを明示したいとき
True 抑制される 特定の例外だけを意図して無視するとき

True を無条件に返すと、想定していない例外まで消えてしまい、原因を調べる手がかりがなくなります。抑制するときは、exc_type を調べて、対象の例外のときだけ True を返す形にします。

class IgnoreMissingKey:
    """KeyError だけを抑制し、ほかの例外は外へ伝える"""

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is not None and issubclass(exc_type, KeyError):
            print("  KeyError を抑制:", exc_value)
            return True
        return False


stock = {"pen": 3}

with IgnoreMissingKey():
    print(stock["note"])
print("続行")

try:
    with IgnoreMissingKey():
        print(stock["pen"] / 0)
except ZeroDivisionError as e:
    print("ZeroDivisionError は外へ:", e)
  KeyError を抑制: 'note'
続行
ZeroDivisionError は外へ: division by zero

なお、__exit__ の中で例外をもう一度 raise する必要はありません。 偽の値を返せば、with 文が元の例外をそのまま投げ直します。

自作する方法1:クラスで書く

処理時間を測るコンテキストマネージャを、クラスで書いた例です。そのままコピーして使えます。

import time


class Timer:
    """with ブロックの所要時間を測る"""

    def __init__(self, label="処理"):
        self.label = label
        self.elapsed = None

    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.elapsed = time.perf_counter() - self.start
        status = "完了" if exc_type is None else f"中断({exc_type.__name__})"
        print(f"{self.label}: {self.elapsed:.2f} 秒 {status}")
        return False


with Timer("集計") as t:
    total = sum(range(1_000_000))
    time.sleep(0.2)
print(total, t.elapsed > 0.2)

try:
    with Timer("読み込み"):
        time.sleep(0.1)
        raise ValueError("形式が違います")
except ValueError as e:
    print("受け取った:", e)
集計: 0.21 秒 完了
499999500000 True
読み込み: 0.10 秒 中断(ValueError)
受け取った: 形式が違います

クラス版は、結果を属性(t.elapsed)に残して with のあとで参照できる点が便利です。例外が起きても __exit__ が呼ばれるので、「中断」の行も表示されています。

自作する方法2:@contextmanagerで書く

contextlib.contextmanager を関数に付けると、yield の前が __enter__、yield の後が __exit__ に相当するコンテキストマネージャになります。yield で渡した値が as の変数に入ります。

from contextlib import contextmanager


@contextmanager
def room(name):
    print("1. 入る準備   (yield の前)")
    try:
        yield name.upper()               # as の右に入る値
    except ValueError as e:
        print("   ValueError を中で処理:", e)
    finally:
        print("3. 後片付け   (yield の後)")


with room("a-101") as r:
    print("2. 本体       ", r)

print("---")
with room("b-202") as r:
    raise ValueError("定員オーバー")
print("4. 続行")

print("---")
try:
    with room("c-303") as r:
        raise KeyError("鍵がない")
except KeyError as e:
    print("4. 外で受け取った KeyError:", e)
1. 入る準備   (yield の前)
2. 本体        A-101
3. 後片付け   (yield の後)
---
1. 入る準備   (yield の前)
   ValueError を中で処理: 定員オーバー
3. 後片付け   (yield の後)
4. 続行
---
1. 入る準備   (yield の前)
3. 後片付け   (yield の後)
4. 外で受け取った KeyError: '鍵がない'

本体で起きた例外は、yield の位置から投げられたかのように関数の中へ届きます。 そのため、except で受ければ抑制(クラス版で True を返すのと同じ)、受けなければ外へ伝わります。2つ目の例では ValueError が中で処理されて「4. 続行」まで進み、3つ目の例では KeyError が外へ伝わりました。

yieldをtry/finallyで囲む

@contextmanager 版でいちばん気をつけたいのが、try / finally の有無です。

from contextlib import contextmanager


@contextmanager
def no_finally():
    print("準備")
    yield
    print("後片付け")          # 例外のときは実行されない


@contextmanager
def with_finally():
    print("準備")
    try:
        yield
    finally:
        print("後片付け")      # 例外のときも実行される


for cm in (no_finally, with_finally):
    print("---", cm.__name__)
    try:
        with cm():
            raise RuntimeError("途中でエラー")
    except RuntimeError as e:
        print("受け取った:", e)
--- no_finally
準備
受け取った: 途中でエラー
--- with_finally
準備
後片付け
受け取った: 途中でエラー

try / finally を書かない版では、例外が起きたときに「後片付け」が表示されていません。 例外が yield の行で投げられ、その下の行へ進まないためです。後片付けが必要な処理は、必ず yield を try / finally で囲みます。

クラス版と@contextmanager版の使い分け

比べる点 クラス版 @contextmanager 版
書く量 多い(メソッド2つ) 少ない(関数1つ)
準備と後片付けの見通し 2つのメソッドに分かれる 上から下へ1つの流れで読める
例外の抑制 __exit__ で True を返す yield を try / except で囲む
状態を持つ・結果を残す 属性に持てる yield するオブジェクトに持たせる
同じインスタンスを繰り返し使う 作り方しだいで可能 1回の呼び出しにつき1回だけ
デコレータとして使う ContextDecorator を継承すれば可能 そのまま可能
向いている場面 状態やメソッドを持たせたい、ライブラリとして公開する 準備と後片付けだけの小さな処理

@contextmanager で作ったものは、関数のデコレータとしても使えます。

import time
from contextlib import contextmanager


@contextmanager
def timer(label):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.1f} 秒")


with timer("ブロック"):
    time.sleep(0.2)


@timer("関数まるごと")
def job():
    time.sleep(0.1)


job()
job()
ブロック: 0.2 秒
関数まるごと: 0.1 秒
関数まるごと: 0.1 秒

複数のwithをまとめて書く

2つ以上のコンテキストマネージャは、カンマで並べて1つの with に書けます。Python 3.10 以降は、全体を括弧で囲んで複数行に分けられます。

class Step:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print("enter", self.name)
        return self.name

    def __exit__(self, *exc):
        print("exit ", self.name)


print("--- カンマで並べる")
with Step("A") as a, Step("B") as b:
    print("body ", a, b)

print("--- 括弧で複数行(Python 3.10 以降)")
with (
    Step("A") as a,
    Step("B") as b,
    Step("C") as c,
):
    print("body ", a, b, c)

print("--- 入れ子(1つ目と同じ意味)")
with Step("A") as a:
    with Step("B") as b:
        print("body ", a, b)
--- カンマで並べる
enter A
enter B
body  A B
exit  B
exit  A
--- 括弧で複数行(Python 3.10 以降)
enter A
enter B
enter C
body  A B C
exit  C
exit  B
exit  A
--- 入れ子(1つ目と同じ意味)
enter A
enter B
body  A B
exit  B
exit  A

入るのは左(上)から、出るのは右(下)からです。皿を重ねて、上から順に片付けるのと同じ順番になります。カンマで並べた形は入れ子と同じ意味なので、B の準備で例外が起きても、先に入った A の __exit__ は呼ばれます。

入力ファイルを読みながら出力ファイルへ書く処理は、次のように書けます。

with (
    open("input.csv", encoding="utf-8") as src,
    open("output.csv", "w", encoding="utf-8") as dst,
):
    for line in src:
        dst.write(line.upper())

公式の言語リファレンスには、複数の式への対応は 3.1、括弧で囲んで複数行に分ける書き方は 3.10 で加わったと書かれています。3.9 以前でも動かす必要があるコードでは、行末のバックスラッシュで折り返すか、入れ子にします。

async withで非同期の後片付けをする

接続を開く・閉じるといった処理そのものが await を必要とするときは、async with を使います。呼ばれるメソッドは __aenter__ と __aexit__ で、どちらも async def で定義します。

import asyncio
from contextlib import asynccontextmanager


class AsyncRoom:
    async def __aenter__(self):
        print("1. __aenter__")
        await asyncio.sleep(0.1)         # 接続などの待ち時間
        return self

    async def __aexit__(self, exc_type, exc_value, traceback):
        await asyncio.sleep(0.1)
        print("3. __aexit__ ", exc_type)
        return False


@asynccontextmanager
async def async_room():
    print("1. 準備")
    try:
        yield "room"
    finally:
        await asyncio.sleep(0.1)
        print("3. 後片付け")


async def main():
    async with AsyncRoom() as r:
        print("2. 本体       ", type(r).__name__)
    async with async_room() as r:
        print("2. 本体       ", r)


asyncio.run(main())
1. __aenter__
2. 本体        AsyncRoom
3. __aexit__  None
1. 準備
2. 本体        room
3. 後片付け
比べる点 with async with
呼ばれるメソッド __enter__ / __exit__ __aenter__ / __aexit__
メソッドの定義 def async def
書ける場所 どこでも async def の中
デコレータで作る @contextmanager @asynccontextmanager
標準ライブラリの例 open()、threading.Lock() asyncio.Lock()、asyncio.TaskGroup()、asyncio.timeout()

async with に対応したオブジェクトを通常の with に渡すと、Python 3.14.7 では次のメッセージが出ます。取り違えに気づきやすい内容になっています。

TypeError: 'A' object does not support the context manager protocol (missed __exit__ method) but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?

withで使える標準機能の一覧

標準ライブラリには、with に渡せるオブジェクトが数多く用意されています。contextlib の各機能が加わったバージョンは、今回公式ドキュメントのページを開いて確かめました。

分類 書き方 入るとき 抜けるとき
ファイル open(path) ファイルを開く 閉じる
ファイル zipfile.ZipFile(path)、tarfile.open(path) 書庫を開く 閉じる
一時ファイル tempfile.TemporaryDirectory() 一時フォルダを作る 中身ごと削除する
一時ファイル tempfile.NamedTemporaryFile() 一時ファイルを作る 閉じて削除する
排他制御 threading.Lock()、threading.RLock() ロックを取得する 解放する
並行処理 concurrent.futures.ThreadPoolExecutor() 実行の枠を用意する すべての処理の完了を待って終了する
データベース sqlite3.connect(path) (何もしない) コミット。例外時はロールバック
数値 decimal.localcontext() 計算の設定を複製する 元の設定に戻す
通信 socket.socket()、urllib.request.urlopen(url) 接続を用意する 閉じる
プロセス subprocess.Popen(args) プロセスを起動する 入出力を閉じて終了を待つ
テスト unittest.mock.patch(target) 対象を差し替える 元に戻す
contextlib suppress(例外)(3.4) — 指定した例外を抑制する
contextlib redirect_stdout(f)(3.4)、redirect_stderr(f)(3.5) 出力先を切り替える 元に戻す
contextlib closing(obj) — obj.close() を呼ぶ
contextlib nullcontext(値)(3.7) 値をそのまま返す 何もしない
contextlib chdir(path)(3.11) カレントフォルダを移す 元に戻す
contextlib ExitStack()(3.3) — 登録した後片付けを逆順に実行する
contextlib AsyncExitStack()(3.7) — 非同期の後片付けも逆順に実行する

表のうち、open・Lock・一時フォルダ・sqlite3・contextlib の各機能(AsyncExitStack を除く)は下のコードで、ZipFile・ThreadPoolExecutor・decimal.localcontext は別のコードで実行して確かめました。tarfile、NamedTemporaryFile、RLock、socket、urlopen、subprocess.Popen、unittest.mock.patch、AsyncExitStack と、前の節の表にある asyncio の3つは今回は未実行で、一般に知られた仕様として載せています。

open・Lock・一時フォルダ

import os
import tempfile
import threading

with open("memo.txt", "w", encoding="utf-8") as f:
    f.write("hello\n")
print("closed:", f.closed)
try:
    f.write("追記")
except ValueError as e:
    print("ValueError:", e)

lock = threading.Lock()
with lock:
    print("with の中 locked:", lock.locked())
print("with の外 locked:", lock.locked())

with tempfile.TemporaryDirectory() as d:
    path = os.path.join(d, "work.txt")
    open(path, "w").close()
    print("with の中 exists:", os.path.exists(path))
print("with の外 exists:", os.path.exists(d))

os.remove("memo.txt")
closed: True
ValueError: I/O operation on closed file.
with の中 locked: True
with の外 locked: False
with の中 exists: True
with の外 exists: False

DB接続:sqlite3はコミットとロールバック

sqlite3 の接続を with に渡したときに行われるのは、コミットまたはロールバックであり、接続を閉じることではありません。 名前から「閉じてくれる」と思いやすい点なので、実行して確かめます。

import sqlite3
from contextlib import closing

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE item(name TEXT UNIQUE)")

with con:                                   # 正常に抜けるとコミット
    con.execute("INSERT INTO item VALUES ('pen')")

try:
    with con:                               # 例外で抜けるとロールバック
        con.execute("INSERT INTO item VALUES ('note')")
        con.execute("INSERT INTO item VALUES ('pen')")   # 重複
except sqlite3.IntegrityError as e:
    print("IntegrityError:", e)

print(con.execute("SELECT name FROM item").fetchall())   # with の後も接続は使える
con.close()

with closing(sqlite3.connect(":memory:")) as con2:       # 閉じたいときは closing
    print(con2.execute("SELECT 1").fetchone())
try:
    con2.execute("SELECT 1")
except sqlite3.ProgrammingError as e:
    print("ProgrammingError:", e)
IntegrityError: UNIQUE constraint failed: item.name
[('pen',)]
(1,)
ProgrammingError: Cannot operate on a closed database.

2回目の with では note の追加も取り消され、テーブルには pen だけが残りました。接続を閉じるところまで任せたいときは、contextlib.closing() で包みます。

suppress・redirect_stdout・nullcontext・chdir

import contextlib
import io
import os
import sys
import tempfile

# suppress: 指定した例外だけを抑制する
with contextlib.suppress(FileNotFoundError):
    os.remove("no_such_file.txt")
    print("この行は実行されない")
print("suppress の後も続行")

# redirect_stdout: print の出力を文字列として受け取る
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
    print("画面には出ない")
print("受け取った文字列:", repr(buf.getvalue()))


# nullcontext: with を使う場合と使わない場合を1つの書き方にまとめる
def export(path=None):
    cm = open(path, "w", encoding="utf-8") if path else contextlib.nullcontext(sys.stdout)
    with cm as out:
        out.write("result\n")


export()

# chdir(3.11 以降): カレントフォルダを一時的に移す
here = os.getcwd()
with contextlib.chdir(tempfile.gettempdir()):
    print("with の中:", os.getcwd())
print("元に戻った:", os.getcwd() == here)
suppress の後も続行
受け取った文字列: '画面には出ない\n'
result
with の中: /tmp
元に戻った: True

suppress は、try / except / pass の3行を1行で書けるものです。os.remove() で例外が起きた時点でブロックを抜けるので、その下の行は実行されません。抑制したい1行だけを with の中に入れると、意図が伝わりやすくなります。

ExitStack:開く数が実行時に決まるとき

開くファイルの数がリストの長さで決まる場合、with にカンマで並べる書き方は使えません。ExitStack は、あとから登録した後片付けを、抜けるときに逆順でまとめて実行します。

import contextlib
import os

names = ["a.txt", "b.txt", "c.txt"]
for n in names:
    with open(n, "w", encoding="utf-8") as f:
        f.write(n + "\n")

with contextlib.ExitStack() as stack:
    files = [stack.enter_context(open(n, encoding="utf-8")) for n in names]
    stack.callback(print, "callback: 最後に登録したものから先に実行される")
    print("with の中:", [f.closed for f in files])
    print([f.readline().strip() for f in files])
print("with の外:", [f.closed for f in files])

for n in names:
    os.remove(n)
with の中: [False, False, False]
['a.txt', 'b.txt', 'c.txt']
callback: 最後に登録したものから先に実行される
with の外: [True, True, True]

enter_context() はコンテキストマネージャの __enter__ を呼んで戻り値を返し、__exit__ を登録します。callback() には、後片付けとして呼びたい関数と引数を渡せます。

つまずきやすい点とエラーメッセージ

with 文で出会いやすい動きを、実行して確かめた結果とあわせてまとめます。エラーメッセージは Python 3.14.7 のものです。

class Trace:
    def __enter__(self):
        return self

    def __exit__(self, *exc):
        print("  __exit__ が呼ばれた")


def find():
    with Trace():
        return "return の値"


print("A. with の中で return")
print(" ", find())

print("B. ループの中で continue / break")
for i in range(2):
    with Trace():
        if i == 0:
            continue
        break

print("C. with に渡せないオブジェクト")
try:
    with 123:
        pass
except TypeError as e:
    print(" ", e)

print("D. 後片付けの中で別の例外")


class Broken:
    def __enter__(self):
        return self

    def __exit__(self, *exc):
        raise KeyError("後片付け中の例外")


try:
    with Broken():
        raise ValueError("本体の例外")
except Exception as e:
    print(" ", type(e).__name__, e, "/ 元の例外:", repr(e.__context__))
A. with の中で return
  __exit__ が呼ばれた
  return の値
B. ループの中で continue / break
  __exit__ が呼ばれた
  __exit__ が呼ばれた
C. with に渡せないオブジェクト
  'int' object does not support the context manager protocol (missed __exit__ method)
D. 後片付けの中で別の例外
  KeyError '後片付け中の例外' / 元の例外: ValueError('本体の例外')
症状・メッセージ 原因 直し方
as の変数が None __enter__ が何も返していない return self を書く
TypeError: 'int' object does not support the context manager protocol (missed __exit__ method) with に渡した値が __enter__ / __exit__ を持たない(3.11.15 では括弧の部分がないメッセージ) 関数の呼び出し忘れ(open と open(...))がないか確かめる
ValueError: I/O operation on closed file. with を抜けたあとでファイルを使っている 読み書きを with の中に移す
例外が起きたのに何も表示されない __exit__ が真の値を返している return する値を見直す。抑制は例外の種類を絞る
@contextmanager で後片付けが動かない yield を try / finally で囲んでいない finally に後片付けを書く
RuntimeError: generator didn't yield @contextmanager の関数が yield に到達していない どの経路でも1回 yield するようにする
RuntimeError: generator didn't stop yield が2回以上実行された yield は1回にする
AttributeError: '_GeneratorContextManager' object has no attribute 'args' @contextmanager で作ったオブジェクトを変数に入れ、2回 with に渡した with のたびに関数を呼び直す
本体の例外が別の例外に置き換わる __exit__ の中で別の例外が起きた(D の結果) 後片付けの中の処理を try で囲む。元の例外は __context__ に残る
sqlite3 の接続が閉じていない with はコミットとロールバックだけを行う closing() で包むか close() を呼ぶ

このほか、__enter__ の中で例外が起きた場合は、本体も __exit__ も実行されません(実行して確かめました)。準備の途中で確保したものがあるときは、__enter__ の中で try を使って片付けます。

よくある質問

Q. with文を使わずにopenするとどうなりますか? A. 動作はしますが、close() を自分で呼ぶ必要があります。途中で例外が起きると close() まで進まないため、try / finally で囲む書き方が必要になります。with を使えば、その3〜4行を書かずに同じ安全さが得られます。

Q. コンテキストマネージャとは何ですか? A. __enter__ と __exit__ の2つのメソッドを持ち、with 文に渡せるオブジェクトのことです。open() が返すファイルオブジェクトや threading.Lock() がその例で、自分でクラスを書くか @contextmanager を使えば自作できます。

Q. asは必ず書く必要がありますか? A. いいえ。with lock: や with suppress(KeyError): のように、__enter__ の戻り値を使わないときは省略できます。

Q. with文の中で定義した変数は外でも使えますか? A. 使えます。with は新しいスコープを作らないので、ブロックの中で代入した変数も as の変数も、抜けたあとに残ります。ただしファイルなどは閉じた状態になっています。

まとめ

  • with 文は「try / finally と後片付けの呼び出し」を1行にまとめた構文で、例外や return で抜けても後片付けが実行されます。
  • 呼ばれる順番は __enter__ → 本体 → __exit__ です。as の変数には __enter__ の戻り値が入ります。
  • 例外が起きると __exit__ に (exc_type, exc_value, traceback) が渡り、例外がなければ3つとも None です。
  • __exit__ が真の値を返すと例外は抑制されます。 抑制するときは例外の種類を絞ります。
  • 自作はクラス版と @contextmanager 版の2通りです。@contextmanager 版は yield を try / finally で囲みます。
  • 複数の with は、入るのは左から、出るのは右からです。3.10 以降は括弧で囲んで複数行に書けます。
  • sqlite3 の with は接続を閉じません。 標準機能ごとに「抜けるときに何が行われるか」を一覧表で確かめてから使うと安心です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python with文の使い方|コンテキストマネージャと__enter__」

株式会社RJCでは、Pythonのwith文とコンテキストマネージャの仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?