Pythonの継続的なI/Oパフォーマンス改善に伴う内部バッファサイズの拡大により、NamedTemporaryFileへの書き込み直後にファイルパスを外部処理へ渡すと、データがディスクに反映されず「中身が空(0バイト)」として扱われる事象に遭遇。
事象 : 転送ファイルが0バイトになる
Python3.12からPython3.14へアップグレードしたところ、転送ファイルが0バイトになってしまうことがあった。
from tempfile import NamedTemporaryFile
with NamedTemporaryFile('wb') as temp_file:
temp_file.write(data)
# ↓転送処理
upload_file(temp_file.name)
The problem is with flushing. The file output is buffered for efficiency reasons, so you must flush it for the changes to be actually written to the file.
引用元 : Stack Overflow: Python NamedTemporaryFile appears empty even after data is written
原因 : デフォルトバッファサイズが引き上げられた
Pythonのバージョンアップに伴うI/O最適化によるデフォルトバッファサイズの引き上げがあった。
データがメモリ内のバッファに留まる閾値が上がり、小さいサイズのファイルは反映前に転送されてしまい中身がなくなる。
例えば、100KBのファイルはPython3.13以前で問題なく転送されていたけれど、Python3.14では内容が反映される前に転送されてしまうので0バイトになってしまう。
| 項目 | Python3.13以前 | Python3.14以降 |
|---|---|---|
| io.DEFAULT_BUFFER_SIZE | 8KB | 128KB |
| 動的な最適化(Linux等) 「バッファサイズ」を賢く自動調節する仕組み |
デバイスのブロックサイズ | max(min(blocksize,8MiB),128KB) |
gh-117151: Increase io.DEFAULT_BUFFER_SIZE from 8k to 128k and adjust open() on platforms where os.fstat() provides a st_blksize field (such as Linux) to use max(min(blocksize, 8 MiB), io.DEFAULT_BUFFER_SIZE) rather than always using the device block size. This should improve I/O performance. Patch by Romain Morotti.
引用元 : Changelog — Python 3.14.3
対応策
1. 明示的な flush() の実行
転送処理を呼び出す直前に、メモリ上のバッファ内容を強制的にディスクへ書き出す。
with NamedTemporaryFile('wb') as temp_file:
temp_file.write(data)
# 物理ディスクへの書き込みを強制実行
temp_file.flush()
upload_file(temp_file.name)
2. delete_on_close=Falseの指定(Python3.12以降)
「クローズ時」ではなく「withブロック終了時」にファイル削除されるようにする。
To manage the named file, it extends the parameters of TemporaryFile() with delete and delete_on_close parameters that determine whether and how the named file should be automatically deleted.
tempfile --- 一時ファイルやディレクトリの作成 — Python 3.14.3 ドキュメント
from tempfile import NamedTemporaryFile
# delete_on_close=False により close() 時の削除を抑止
with NamedTemporaryFile('wb', delete_on_close=False) as temp_file:
temp_file.write(data)
# クローズによってバッファを完全にフラッシュ
temp_file.close()
# 書き込み完了済みのパスを外部処理へ渡す
upload_file(temp_file.name)
# withブロック終了時にファイルは自動削除
3. delete=False による手動管理
自動削除自体を無効化し、クローズ後のファイルを保持する。
import os
from tempfile import NamedTemporaryFile
temp_file = NamedTemporaryFile('wb', delete=False)
try:
temp_file.write(data)
temp_file.close() # クローズによる書き込み完了の保証
upload_file(temp_file.name)
finally:
# 処理終了後に明示的に削除を実行
if os.path.exists(temp_file.name):
os.unlink(temp_file.name)
補足 : openpyxlのsave()はファイルのクローズまで一気にやる
openpyxlのsave()は「書き込み後にファイルを閉じる」処理までを完結させているので、バッファサイズの影響を受けずにデータがディスクへ反映される。
data = openpyxl.Workbook()
# ...ここでExcel内容を作成する処理がある...
with NamedTemporaryFile('wb') as temp_file:
data.save(temp_file.name)
upload_file(temp_file.name)
temp_file.write()を使う場合は、withブロックが開いている間はファイルが「オープン」されたままで、クローズ(=自動フラッシュ)が行われないため、バッファサイズを超えないと内容が反映されない。
しかし、data.save()は「オープン」→「書き込み」→「クローズ」まで完結させるので内容はディスクに反映される。
- save()メソッドは内部的に
- ZipFileを使用してアーカイブを作成し、
- データを書き込んだ後、即座に
archive.close()を実行してファイルを閉じる。