一行上書きする場合
一行を上書きする場合、\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.writeとsys.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
stdoutとstderrがほぼ同じタイミングで交互に現れ、期待通りリアルタイムに出力されています(行バッファリング)。
ケース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
実際に両方とも検証したところ、stdoutとstderrが再びケース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-string | 104.5 ns/call | 524.4 ns/call |
.format() | 156.8 ns/call | 490.0 ns/call |
%書式化 | 131.1 ns/call | 399.0 ns/call |
結果は「常にf-stringが最速」という通説を裏付けませんでした。単純な2フィールドの埋め込みではf-stringが最速でしたが、本記事の進捗バーのように%6.1f%%のような幅・精度指定を含む複雑なフォーマットを4フィールド分行う場合は、%書式化が最速で、f-stringが最も遅いという逆転が起きています。f-stringは各{...}をコンパイル時に個別の書式化呼び出しへ展開するのに対し、%演算子は1回のCレベルの書式化処理にまとめられるため、と考えられます。
グラフで比較すると次のようになります。

結論: プログレスバーのように高頻度かつ複雑な書式指定を伴う文字列を組み立てる場面では、「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のコマンドプロンプトが既定で使うレガシーなコードページ(cp1252やcp932など、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) | tqdm | rich |
|---|---|---|---|
| 外部依存 | なし | pip install | pip 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 Guarantees
、
Python 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によるオーバーライト表示の基本的な挙動——本記事で解説した内容——に影響するような破壊的変更は見当たりませんでした。
関連記事
- Pythonでプログレスバーを自作する(tqdmなし) - この記事で紹介したキャリッジリターンやANSIエスケープシーケンスを活用して、プログレスバーを自作する方法を解説しています。
- PythonのMatplotlibで3Dアニメーション(GIF)を作成する方法 - Matplotlibを使ったリッチな出力・可視化の実践
- Python正規表現実践ガイド:reモジュールの基本からパフォーマンス最適化まで - Python標準ライブラリの実践的な活用法
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール