Pythonでprintの上書きをする方法

Pythonでprint出力を上書き表示する実装方法を解説。キャリッジリターン(\r)による一行上書きと、ANSIエスケープシーケンスを使った複数行の上書き、進捗バー表示への応用までサンプルコード付きで紹介。printの内部動作・バッファリングモード・flushの実測、f-string/.format()/%のパフォーマンス比較、UnicodeEncodeErrorの対処法も収録。

一行上書きする場合

一行を上書きする場合、\r(キャリッジリターン)を使用します。これはカーソルを行の先頭に戻す制御文字です。

print("\r"+"一行を上書きします!",end="")

end=""を使用して改行を行わないようにします。

カウントダウンタイマーの例

\rの実践的な使い方として、カウントダウンタイマーを実装してみます。

import time
import sys

def countdown(seconds):
    for i in range(seconds, 0, -1):
        # \rで行頭に戻り、上書き表示
        sys.stdout.write(f"\r残り {i:3d} 秒")
        sys.stdout.flush()
        time.sleep(1)
    sys.stdout.write("\r完了!          \n")

countdown(10)

ここでsys.stdout.writesys.stdout.flush()を使っています。printは内部的にsys.stdout.writeを呼び出しますが、sys.stdoutを直接使うことでバッファリングの制御がより明確になります。flush()を呼ぶことで、出力が即座に画面に反映されます。

注意点: 前回の出力より短い文字列を上書きすると、前の文字が残ってしまいます。上の例では"完了!"の後にスペースを入れて、前の表示を確実に消しています。

スピニングカーソルの実装

処理中であることを視覚的に示すスピニングカーソル(ローディングアニメーション)も\rで実装できます。

import sys
import time
import itertools

def spinning_cursor(duration=5):
    spinner = itertools.cycle(['|', '/', '-', '\\'])
    end_time = time.time() + duration
    while time.time() < end_time:
        sys.stdout.write(f"\r処理中... {next(spinner)}")
        sys.stdout.flush()
        time.sleep(0.1)
    sys.stdout.write("\r処理完了!     \n")

spinning_cursor(3)

itertools.cycleを使うことで、スピナー文字を無限に繰り返すことができます。

printの内部動作とバッファリング

print()は便利な高レベル関数ですが、実体は薄いラッパーです。CPythonの実際の挙動を再現すると、概念的には次のように動作します(Cで書かれた本物の実装ではなく、動作を再現した疑似コードです)。

import sys

def my_print(*objects, sep=" ", end="\n", file=None, flush=False):
    if file is None:
        file = sys.stdout
    file.write(sep.join(str(obj) for obj in objects) + end)
    if flush:
        file.flush()
  • sep: 複数の引数の間に挿入する文字列(デフォルトは半角スペース)
  • end: 出力の最後に付加する文字列(デフォルトは改行)。本記事でend=""を指定しているのは、この改行を消して\rによるカーソル移動の効果を活かすため
  • file: 書き込み先のストリーム。デフォルトはsys.stdoutだが、sys.stderrや任意のioオブジェクトを渡せる
  • flush: Trueにすると、この呼び出しの直後にストリームのバッファを強制的に吐き出す

重要なのは、print()は最終的にfile.write()を呼んでいるだけで、そのデータが実際に画面(またはファイル)へ反映されるタイミングは、書き込みそのものとは別の「バッファリングモード」に支配されるという点です。

3種類のバッファリングモード

CPythonのioモジュールは、ストリームの接続先に応じて自動的にバッファリング方式を切り替えます。

モード説明発動条件
行バッファリング(line buffering)改行\nを書き込むたびに自動でflushされる出力先が対話端末(isatty()True)である場合のsys.stdoutの既定動作
フルバッファリング(block buffering)内部バッファ(既定io.DEFAULT_BUFFER_SIZEバイト)が満杯になるか、プログラム終了・明示的なflush()/close()まで書き込みが保留される出力先がファイルやパイプなど非対話端末である場合のsys.stdoutの既定動作
無バッファリング(unbuffered)書き込みが即座に反映されるsys.stderrは常にこれに近い挙動。sys.stdoutをこの挙動にするにはpython -uまたは環境変数PYTHONUNBUFFERED=1を使う

手元のPython 3.14.6で確認したところ、フルバッファリング時の既定バッファサイズは次の通りでした。

>>> import io
>>> io.DEFAULT_BUFFER_SIZE
131072

128 KiBという値で、長年(Python 2の時代から)使われていた8 KiBよりかなり大きくなっています。バルクI/O性能改善のための引き上げが CPythonのissue #117151 で議論され、実装されたものです。バッファが大きいほど、フルバッファリング時に「たまってから一気に出る」までの遅延は長くなり得ます。

sys.stderrが常にバッファリングされない設計になっているのは、エラーメッセージが遅延したり失われたりしないようにするためです。本記事のカウントダウンタイマーやスピナーの実装でsys.stdout.write()の直後に毎回sys.stdout.flush()を呼んでいたのは、まさにこの理由——sys.stdoutが非対話端末(ファイルへのリダイレクトやCI環境など)に接続されている場合、flush()を呼ばなければ\rで作った「上書き」の効果が画面に反映されず、プログラム終了時に一括で出力されてしまうからです。

実験:ターミナル vs リダイレクトで出力順序が変わる

これを実際に確認してみます。次のスクリプトはprint()sys.stdout経由)とsys.stderr.write()を交互に呼び出します。

import sys
import time

for i in range(5):
    print(f"stdout {i}")               # sys.stdout 経由(バッファリングモードは接続先次第)
    sys.stderr.write(f"stderr {i}\n")  # sys.stderr は常にバッファリングされない
    time.sleep(0.15)

このスクリプトの標準出力を疑似端末(pty)に接続した場合と、通常のパイプ(ファイルへのリダイレクトと同じ扱い)に接続した場合とで、到達時刻をタイムスタンプ付きで記録してみました(ptyモジュールで疑似端末を作り、selectでイベント到着時刻を計測)。

ケースA: 標準出力が対話端末(pty)に接続されている場合

t=0.018s [STDOUT] stdout 0
t=0.018s [STDERR] stderr 0
t=0.169s [STDOUT] stdout 1
t=0.169s [STDERR] stderr 1
t=0.321s [STDOUT] stdout 2
t=0.321s [STDERR] stderr 2
t=0.476s [STDOUT] stdout 3
t=0.476s [STDERR] stderr 3
t=0.631s [STDOUT] stdout 4
t=0.631s [STDERR] stderr 4

stdoutstderrがほぼ同じタイミングで交互に現れ、期待通りリアルタイムに出力されています(行バッファリング)。

ケースB: 標準出力を通常のパイプ/ファイルにリダイレクトした場合

t=0.020s [STDERR] stderr 0
t=0.172s [STDERR] stderr 1
t=0.327s [STDERR] stderr 2
t=0.481s [STDERR] stderr 3
t=0.632s [STDERR] stderr 4
t=0.787s [STDOUT] stdout 0
t=0.787s [STDOUT] stdout 1
t=0.787s [STDOUT] stdout 2
t=0.787s [STDOUT] stdout 3
t=0.787s [STDOUT] stdout 4

stderrは毎回即座に出ているのに対し、stdout側は5行すべてがプロセス終了時にまとめて出ています。これがフルバッファリングの実際の挙動です。手元で試すなら、python3 script.py > out.txtのようにリダイレクトし、ターミナルにはstderrだけがリアルタイムに流れ、out.txtの中身はプログラム終了後にしか埋まらないことを確認してみてください。

修正方法: 次のどちらかで、パイプ/ファイル出力でもリアルタイム性を保てます。

# 方法1: print() ごとに flush=True を指定
print(f"stdout {i}", flush=True)

# 方法2: インタプリタ起動時に -u(unbuffered)オプションを付ける
# $ python3 -u script.py > out.txt

実際に両方とも検証したところ、stdoutstderrが再びケースAと同じタイミングパターンで交互に出力されることを確認しました。このflush()(またはそれに相当する仕組み)こそが、本記事全体を通じて\rによるリアルタイム上書き表示が機能するための土台になっています。

複数行を上書きする場合

複数行を上書きするような表示を行う場合、特殊なエスケープシーケンス\033[nA(カーソルをn行上に移動)などを利用します。

print("最初の行")
print("二番目の行")
# カーソルを1行上に移動し、上書き
print("\033[1A新しい二番目の行")

ANSIエスケープシーケンス一覧

ターミナル上のカーソル制御や装飾に使えるANSIエスケープシーケンスの主要なものを表にまとめます。

カーソル操作

シーケンス説明使用例
\033[nAカーソルをn行上に移動print("\033[2A")
\033[nBカーソルをn行下に移動print("\033[1B")
\033[nCカーソルをn列右に移動print("\033[5C")
\033[nDカーソルをn列左に移動print("\033[3D")
\033[2K現在の行をクリアprint("\033[2K", end="")
\033[Jカーソル以降をクリアprint("\033[J", end="")
\033[Hカーソルを画面左上に移動print("\033[H")
\033[{r};{c}Hカーソルを行r, 列cに移動print("\033[5;10H")

テキスト装飾(色・スタイル)

シーケンス説明
\033[0mリセット(装飾解除)
\033[1m太字
\033[4m下線
\033[31m赤色テキスト
\033[32m緑色テキスト
\033[33m黄色テキスト
\033[34m青色テキスト
\033[41m赤色背景
\033[42m緑色背景

例えば、色付きのステータス表示は以下のように書けます。

# 成功時は緑、失敗時は赤で表示
def print_status(message, success=True):
    color = "\033[32m" if success else "\033[31m"
    reset = "\033[0m"
    print(f"{color}{message}{reset}")

print_status("テスト通過", success=True)
print_status("テスト失敗", success=False)

ダウンロード進捗表示の実装例

\rとANSIエスケープシーケンスを組み合わせて、実用的なダウンロード進捗表示を実装してみます。

import sys
import time

def download_progress(total_size, chunk_size=1024):
    """ダウンロード進捗をプログレスバーで表示する"""
    downloaded = 0
    bar_length = 40

    while downloaded < total_size:
        downloaded += chunk_size
        if downloaded > total_size:
            downloaded = total_size

        # 進捗率の計算
        progress = downloaded / total_size
        filled = int(bar_length * progress)
        bar = "█" * filled + "░" * (bar_length - filled)

        # MB単位で表示
        dl_mb = downloaded / (1024 * 1024)
        total_mb = total_size / (1024 * 1024)

        # 色付きで進捗表示
        if progress < 0.5:
            color = "\033[33m"  # 黄色
        else:
            color = "\033[32m"  # 緑色
        reset = "\033[0m"

        sys.stdout.write(
            f"\r{color}[{bar}]{reset} "
            f"{progress:6.1%} "
            f"({dl_mb:.1f}/{total_mb:.1f} MB)"
        )
        sys.stdout.flush()
        time.sleep(0.01)  # ダウンロードのシミュレーション

    sys.stdout.write("\n")
    print("ダウンロード完了!")

# 10MBのファイルをシミュレーション
download_progress(10 * 1024 * 1024, chunk_size=100 * 1024)

複数行リアルタイム更新の例

ANSIエスケープシーケンスを組み合わせると、複数のタスクの進捗を同時にリアルタイム更新できます。

import sys
import time
import random

def multi_task_progress():
    """複数タスクの進捗を同時更新する"""
    tasks = ["データ取得", "前処理  ", "学習    ", "評価    "]
    progress = [0] * len(tasks)

    # 初期表示
    for task in tasks:
        print(f"  {task}: [{'░' * 30}]   0%")

    while not all(p >= 100 for p in progress):
        # ランダムに進捗を更新
        for i in range(len(tasks)):
            if progress[i] < 100:
                progress[i] = min(100, progress[i] + random.randint(0, 5))

        # カーソルをタスク数分上に移動
        sys.stdout.write(f"\033[{len(tasks)}A")

        for i, task in enumerate(tasks):
            filled = int(30 * progress[i] / 100)
            bar = "█" * filled + "░" * (30 - filled)
            if progress[i] >= 100:
                color = "\033[32m"  # 完了: 緑
            else:
                color = "\033[33m"  # 進行中: 黄
            reset = "\033[0m"
            print(f"  {task}: {color}[{bar}]{reset} {progress[i]:3d}%")

        time.sleep(0.1)

multi_task_progress()

文字列フォーマットのパフォーマンス比較:f-string vs .format() vs %

ここまでの進捗表示の例は、いずれもループの中で毎回文字列を組み立てています(download_progressは最短10ms間隔、multi_task_progressはタスク数分の更新のたびに)。「f-stringが一番速い」という主張はよく見かけますが、実際に計測してみましょう。

import timeit

progress = 0.6428
dl_mb = 6.428
total_mb = 10.0
bar = "#" * 25 + "-" * 15

def with_fstring():
    return f"\r[{bar}] {progress:6.1%} ({dl_mb:.1f}/{total_mb:.1f} MB)"

def with_format():
    return "\r[{}] {:6.1%} ({:.1f}/{:.1f} MB)".format(bar, progress, dl_mb, total_mb)

def with_percent():
    return "\r[%s] %5.1f%% (%.1f/%.1f MB)" % (bar, progress * 100, dl_mb, total_mb)

N = 2_000_000
for name, fn in [("f-string", with_fstring), (".format()", with_format), ("%-formatting", with_percent)]:
    best = min(timeit.repeat(fn, number=N, repeat=5))
    print(f"{name:14s}: {best / N * 1e9:.1f} ns/call")

Python 3.14.6での実測結果(5回計測のベスト値、200万回呼び出し)です。

手法シンプルな2フィールド(f"[{name}] step {i}"進捗バー行(4フィールド・幅/精度指定あり)
f-string104.5 ns/call524.4 ns/call
.format()156.8 ns/call490.0 ns/call
%書式化131.1 ns/call399.0 ns/call

結果は「常にf-stringが最速」という通説を裏付けませんでした。単純な2フィールドの埋め込みではf-stringが最速でしたが、本記事の進捗バーのように%6.1f%%のような幅・精度指定を含む複雑なフォーマットを4フィールド分行う場合は、%書式化が最速で、f-stringが最も遅いという逆転が起きています。f-stringは各{...}をコンパイル時に個別の書式化呼び出しへ展開するのに対し、%演算子は1回のCレベルの書式化処理にまとめられるため、と考えられます。

グラフで比較すると次のようになります。

f-string・.format()・%書式化のベンチマーク比較。シンプルな2フィールドではf-stringが最速だが、進捗バーのような幅・精度指定を含む複雑な書式ではf-stringが最も遅くなる

結論: プログレスバーのように高頻度かつ複雑な書式指定を伴う文字列を組み立てる場面では、「f-stringが常に最速」と決めつけず、実ワークロードでベンチマークを取ることをお勧めします。とはいえ、いずれの手法も1回あたり数百ナノ秒のオーダーであり、time.sleep(0.01)(10ms = 10,000,000 ns)を挟むような本記事の進捗表示用途では、どの手法を選んでも体感差は出ません。パフォーマンスが問題になるのは、秒間数十万回以上文字列を組み立てるようなホットループの場合です。

クロスプラットフォーム対応

ANSIエスケープシーケンスはLinuxやmacOSのターミナルではそのまま動作しますが、Windowsの従来のコマンドプロンプト(cmd.exe)では対応していません

Windowsでの対応方法

Windows 10以降のWindows Terminalや PowerShellではANSIエスケープシーケンスがサポートされていますが、古い環境との互換性を保つにはcoloramaライブラリを使います。

# pip install colorama
from colorama import init, Fore, Style

# WindowsでANSIエスケープを有効化
init()

# coloramaを使った色付き出力
print(Fore.GREEN + "成功" + Style.RESET_ALL)
print(Fore.RED + "エラー" + Style.RESET_ALL)

colorama.init()を呼ぶだけで、WindowsでもANSIエスケープシーケンスが正しく動作するようになります。Linux/macOSでは何もしない(副作用がない)ため、クロスプラットフォームなコードに適しています。

os.nameによる分岐

coloramaを使わない場合は、os.nameで分岐する方法もあります。

import os
import sys

def supports_ansi():
    """ANSIエスケープシーケンスが使えるか判定"""
    if os.name == "nt":
        # Windows 10 build 10586以降はサポート
        return os.environ.get("WT_SESSION") is not None  # Windows Terminal
    return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()

if supports_ansi():
    GREEN = "\033[32m"
    RESET = "\033[0m"
else:
    GREEN = ""
    RESET = ""

print(f"{GREEN}ステータス: OK{RESET}")

エンコーディングの落とし穴:UnicodeEncodeError

進捗バーの/や、ステータス表示に絵文字(✅など)を使う場合、環境によってはUnicodeEncodeErrorが発生することがあります。原因はsys.stdoutのエンコーディングが、書き込もうとしている文字をサポートしていないためです。典型的には、Windowsのコマンドプロンプトが既定で使うレガシーなコードページ(cp1252cp932など、UTF-8ではないもの)で発生します。

実際に、sys.stdoutのエンコーディングをcp1252に強制して再現してみます。

import sys

print("stdout encoding:", sys.stdout.encoding)
print(f"進捗 100% 完了 ✅")
$ PYTHONIOENCODING=cp1252 python3 encoding_bug.py
stdout encoding: cp1252
Traceback (most recent call last):
  File "encoding_bug.py", line 4, in <module>
    print(f"進捗 100% 完了 ✅")
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-1: character maps to <undefined>

cp1252は日本語や絵文字を表現できないコードページのため、print()内部のfile.write()がエンコードに失敗し、そのまま例外として送出されます。

修正方法1: 環境変数PYTHONIOENCODINGでエンコーディングを明示的にUTF-8へ固定する。

$ PYTHONIOENCODING=utf-8 python3 encoding_bug.py
stdout encoding: utf-8
進捗 100% 完了 ✅

修正方法2: スクリプト内でsys.stdout.reconfigure()を呼び、実行環境のロケールによらずUTF-8を強制する(Python 3.7以降)。

import sys

sys.stdout.reconfigure(encoding="utf-8")
print("stdout encoding after reconfigure:", sys.stdout.encoding)
print(f"進捗 100% 完了 ✅")
stdout encoding after reconfigure: utf-8
進捗 100% 完了 ✅

両方とも実際に検証済みで、cp1252のままでは確実にUnicodeEncodeErrorが発生し、PYTHONIOENCODING=utf-8またはsys.stdout.reconfigure(encoding="utf-8")のいずれでも解消することを確認しました。Windows環境で配布するCLIツールでは、sys.stdout.reconfigure()をエントリーポイントの先頭で呼んでおくと安全です。

ライブラリ比較:手動 vs tqdm vs rich

用途に応じて適切な方法を選びましょう。

特性手動(\r / ANSI)tqdmrich
外部依存なしpip installpip install
プログレスバー自作が必要1行で実装可能1行で実装可能
複数バー同時表示自作が必要サポートサポート
テーブル表示不可不可サポート
色付き出力ANSIコード手動限定的Rich Markup で簡単
Windows対応colorama必要自動対応自動対応
カスタマイズ性完全に自由中程度高い
学習コスト低い(原理理解向き)低い中程度
適したケース軽量な用途、学習目的ループ処理リッチなCLIアプリ

シンプルな上書き表示には本記事の方法を、ループ処理のプログレスバーには tqdm を、リッチなCLIアプリには rich の利用を検討してください。

モダンなPython出力ライブラリ

printの上書き以外にも、リッチな出力を実現するライブラリがあります。

richライブラリ

rich はターミナルに色付きテキスト、テーブル、プログレスバーなどを表示できるライブラリです。

from rich.console import Console
from rich.table import Table

console = Console()

# プログレス表示
from rich.progress import track
import time

for i in track(range(100), description="Processing..."):
    time.sleep(0.01)

# ステータス表示(スピナー付き)
with console.status("Computing..."):
    time.sleep(2)
    console.print("[bold green]Done![/bold green]")

tqdmによるプログレスバー

tqdm は簡潔な記述でプログレスバーを表示できます。上書き表示の代替として広く使われています。

from tqdm import tqdm
import time

for i in tqdm(range(100)):
    time.sleep(0.01)

最近のCPythonにおける関連動向

print/sys.stdoutのバッファリング仕様そのものに影響する変更が最近のCPythonにあるか調査しました。

  • フリースレッド版(GILなしビルド): PEP 703に基づくフリースレッドビルドは、Python 3.13で実験的に導入され、Python 3.14(2025年10月リリース)で正式サポートとなりました。シングルスレッド時のオーバーヘッドも3.13の約35〜40%から3.14では約5〜10%まで縮小しています。ただし、CPython公式ドキュメント( Thread Safety GuaranteesPython support for free threading )を確認した限り、print()sys.stdoutのバッファリング・フラッシュのセマンティクスに関する変更は明記されていません。フリースレッド化はPythonバイトコードの並行実行方式を変えるものであり、本記事で解説したストリームごとのバッファリングモード判定ロジック自体は影響を受けないと考えられます。
  • デフォルトバッファサイズの拡大: 前述の通り、io.DEFAULT_BUFFER_SIZEは手元の3.14.6環境で128 KiB(131072バイト)でした。長年8 KiBだったものが、バルクI/Oの性能改善のため引き上げられた変更です( cpython issue #117151 )。フルバッファリング時に「たまる量」が増えたことになりますが、行バッファリング/フルバッファリングという仕組み自体は変わっていません。

現時点では、print()\rによるオーバーライト表示の基本的な挙動——本記事で解説した内容——に影響するような破壊的変更は見当たりませんでした。

関連記事


関連ツール