目次
付録
  1. 34.付録A — 旧 Python コードをモダンスタイルに移す
34 章

付録A — 旧 Python コードをモダンスタイルに移す

2017年スタイルの Python コード(% 文字列、has_key、type() 比較など)を現代の Python スタイルに段階的に移すガイド。旧 Python 基礎講座 21編の読者が本書へ自然に読み継げるように。

本付録は、このサイトの 旧 Python 基礎講座 21編(2017年) を読んだか、Python 2.x 〜 3.5 の時代に学んでから止まっていた人のためのマイグレーションガイドです。

本文 1 〜 33章が「最初からモダンで始める」本だとすれば、本付録は 旧コードを手に持ったまま一段階ずつモダンに移す ための案内です。一緒に把握しておけば、旧講座の資料も捨てずに更新して使えます。

旧 / 新の 1:1 対応 — 1 ページの要約 #

次の表が本付録の圧縮版です。各項目は下でペアのコードと一緒に再度見ます。

領域旧コード(2017年前後)新コード(モダン)
出力print "hello" (py2)print("hello")
文字列フォーマット"hi %s" % name / .format()f"hi {name}"
整数除算5 / 22 (py2)5 / 22.5 / 整数は 5 // 2
dict キー確認d.has_key("k")"k" in d
例外捕捉except Exception, e:except Exception as e:
反復範囲xrange(10)range(10) (すでに lazy)
Unicodeu"한글"unicodeそのまま "한글"、すべての str が unicode
型検査type(x) == intisinstance(x, int)
クラス boilerplatedef __init__(self, a, b): self.a=a; self.b=b@dataclass
型情報docstring に :type x: intx: int アノテーション
コレクション型from typing import List + List[int]list[int] (3.9+)
Option 型Optional[int]`int
パスos.path.join(a, b)Path(a) / b
外部コマンドos.system("ls") / Popensubprocess.run(["ls"])
非同期コールバック / generator コルーチンasync def + await
ツールチェーンpip install + venv + pyenvuv add + uv run + uv python install

段階別の変換 #

1. print が関数になった #

旧 (Python 2)
print "hello"
print "x =", x
新 (Python 3)
print("hello")
print("x =", x)

旧コードを Python 3 インタプリタで動かすと即座に SyntaxError。機械的変換なのでツールが一発で捕まえます(下の §「自動化」参照)。

2. 文字列フォーマット — % / .format() → f-string #

3 世代が共存しています。新コードは無条件に f-string です。

旧 (% スタイル)
"hi %s, you are %d" % (name, age)
中間 (.format)
"hi {}, you are {}".format(name, age)
"hi {name}, you are {age}".format(name=name, age=age)
新 (f-string, 3.6+)
f"hi {name}, you are {age}"
f"{name = }, {age = }"   # デバッグ用

詳しい使い方は 第2章 変数、基本型と型ヒント で。

3. 整数除算 #

旧 (Python 2)
5 / 2     # 2  (整数同士の除算が整数)
5.0 / 2   # 2.5
新 (Python 3)
5 / 2     # 2.5  (常に float)
5 // 2    # 2    (整数除算は // で明示)

旧コードで / だけ見て整数結果を期待していたら、Python 3 では壊れます。意図が整数なら // で明示します。

4. dict メソッド — has_key 廃止 #

if d.has_key("user"):
    ...
if "user" in d:
    ...

in のほうが短く、dict 以外のコレクションにも一貫して動作します。

5. 例外捕捉 — , eas e #

旧 (Python 2)
try:
    do_work()
except Exception, e:
    print "failed:", e
新 (Python 3)
try:
    do_work()
except Exception as e:
    print(f"failed: {e}")

, 構文は Python 3 で SyntaxError。as に統一します。

加えて Python 3.11 から ExceptionGroup + except* が入りました。同時に発生した複数の例外をまとめて扱う新しい道具で、本文第6章で扱います。

6. xrange 廃止 — range がすでに lazy #

旧 (Python 2)
for i in xrange(10):
    ...
新 (Python 3)
for i in range(10):
    ...

Python 3 の range が旧 xrange の動作を吸収しました。一度にリストを作らず、lazy に値を出します。

7. Unicode — すべての str が Unicode #

旧 (Python 2)
s = u"한글"     # u プレフィックスが必要
isinstance(s, unicode)
新 (Python 3)
s = "한글"      # str がそのまま Unicode
isinstance(s, str)

unicode という型自体がありません。str = Unicode、bytes = バイトシーケンス。両者は自動変換されず、.encode() / .decode() で明示変換します。

8. 型検査 — type() 比較ではなく isinstance() #

if type(x) == int:
    ...
if isinstance(x, int):
    ...

isinstance は継承関係まで認識します。type() 比較は正確に同じ型でしか真にならず、継承を扱えません。そしてモダン Python では、そもそも実行時の型検査より 型ヒント + 静的検証 に寄せる流れです(第2章 型チェッカを参照)。

9. クラスの boilerplate — __init__ 数十行を @dataclass 1 行に #

class User(object):
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

    def __repr__(self):
        return "User(id=%r, name=%r, email=%r)" % (self.id, self.name, self.email)

    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return (self.id, self.name, self.email) == (other.id, other.name, other.email)
新 (dataclass, 3.7+)
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str

__init____repr____eq__ が自動で作られます。追加オプション(frozenslotskw_only)まですべて 1 行で。本文第8章 dataclass と __slots__ で詳しく扱います。

class User(object):(object) も旧時代の名残です。Python 3 ではすべてのクラスが自動で object を継承するので省略します。

10. 型情報 — docstring からアノテーションへ #

def add(a, b):
    """
    :param a: 一つ目の整数
    :type a: int
    :param b: 二つ目の整数
    :type b: int
    :rtype: int
    """
    return a + b
def add(a: int, b: int) -> int:
    """一つ目と二つ目を足して返します。"""
    return a + b

型情報がシグネチャに上がります。IDE / 型チェッカ / ドキュメント生成器がすべて同じ情報を見ます。docstring は なぜどう使うか だけ書きます。

11. コレクション型 — typing import → ビルトインジェネリック #

旧 (3.8 以下)
from typing import List, Dict, Tuple, Set

names: List[str] = []
ages: Dict[str, int] = {}
point: Tuple[float, float] = (0.0, 0.0)
unique: Set[str] = set()
新 (3.9+)
names: list[str] = []
ages: dict[str, int] = {}
point: tuple[float, float] = (0.0, 0.0)
unique: set[str] = set()

from typing import List 自体が消えていく流れです。ビルトイン型にそのまま [] を使います。

12. Option 型 — OptionalT | None #

旧 (3.9 以下)
from typing import Optional, Union

def find(id: int) -> Optional[str]:
    ...

def parse(s: str) -> Union[int, float, None]:
    ...
新 (3.10+)
def find(id: int) -> str | None:
    ...

def parse(s: str) -> int | float | None:
    ...

短く、追加の import がありません。

13. パス — os.pathpathlib.Path #

import os
config_path = os.path.join(os.path.dirname(__file__), "config", "app.toml")
with open(config_path, "r") as f:
    data = f.read()
from pathlib import Path
config_path = Path(__file__).parent / "config" / "app.toml"
data = config_path.read_text()

Path オブジェクトは / 演算子で結合でき、.read_text() / .write_text() / .exists() / .glob() などのメソッドを持ちます。os.path の関数を覚える必要が減ります。

14. 外部コマンド — os.system / Popensubprocess.run #

import os
os.system("ls -la /tmp")          # 結果を受け取れない、セキュリティリスク
中間 (Popen 手動)
from subprocess import Popen, PIPE
proc = Popen(["ls", "-la", "/tmp"], stdout=PIPE, stderr=PIPE)
out, err = proc.communicate()
新 (3.5+)
import subprocess
result = subprocess.run(
    ["ls", "-la", "/tmp"],
    capture_output=True,
    text=True,
    check=True,
)
print(result.stdout)

リストで引数を渡すのが基本 — シェルインジェクションのリスクが減ります。check=True は異常終了時に例外を発生させ、capture_output=True は stdout / stderr を収集、text=True は bytes ではなく str で受け取ります。

15. 非同期 — コールバック / generator コルーチン → async def + await #

旧 (asyncio の @coroutine + yield from, 3.4 〜 3.7)
import asyncio

@asyncio.coroutine
def fetch():
    response = yield from get_url("https://example.com")
    return response
新 (3.5+, 3.8 から推奨)
async def fetch():
    response = await get_url("https://example.com")
    return response

async def / await キーワードが標準になり、@asyncio.coroutine デコレータは deprecated になりました。本文第14章 非同期入門、第18章 非同期の深さで扱います。

自動化 — すべて手で移す必要はありません #

上の変換のほとんどは 機械的 です。ツールが一度に捕まえてくれます。

pyupgrade #

pyupgrade の適用
uv tool install pyupgrade
pyupgrade --py312-plus path/to/file.py

処理してくれる項目(一部):

  • % フォーマット → f-string 変換(単純なケース)
  • from typing import List の削除 + List[int]list[int] 置換
  • Optional[X]X | None
  • Dict[K, V]dict[K, V]
  • super(ClassName, self)super() の短縮
  • print 文 → 関数形式
  • f-string 内の .format() 整理

--py312-plus 引数は「自分のコードが Python 3.12 以上でだけ動くと仮定し、そのバージョンで使える新しい構文にすべて移せ」という意味です。本書基準であれば --py314-plus

ruff の UP ルール — pyupgrade の ruff 統合 #

pyproject.toml
[tool.ruff.lint]
select = ["UP"]    # pyupgrade ルール有効化
ruff で自動修正
uv run ruff check . --select UP --fix

ruff は pyupgrade のルールのほとんどを吸収しており、他の lint ルールと一緒に一度に回せます。pre-commit でまとめておけば、旧パターンが新たに入ってくるのを常時ブロックできます — 第30章 型チェッカ設定と CI 統合 で正式なセットアップを扱います。

自動化できないもの #

  • クラス → @dataclass 変換(意図判断が必要)
  • os.pathpathlib 変換(どこでどう使われているか人の判断)
  • コールバック / generator コルーチン → async def(構造変更)
  • docstring の型表記 → アノテーション(自動化ツールはあるが精度に限界)

上の 4 つは人が手で移すのが安全です。

旧講座 → 本書 — 推奨パス #

Python 基礎講座 21編 の読者には、次の順序を勧めます。

  1. 本付録 A から — 旧講座時代のパターンが新しい本でどう違うかを一目で見る。
  2. 本書第1章 Python の始まりと uv セットアップ — ツールチェーンが完全に変わったので、環境から入れ直す。
  3. 本書第2章 変数、基本型と型ヒント — 最も大きな差である「型ヒント優先」を身につける。
  4. 本書第 3 〜 7章 — 旧講座ですでに馴染みのあるテーマ(制御フロー、コレクション、関数、例外、モジュール)をモダンな書き方で再度見る。馴染みがあるので速く読める。
  5. 本書 2部以降 — 旧講座になかった領域(dataclass、Protocol、非同期、FastAPI、運用)に踏み込む。

旧講座はサイトに永続的に残ります。本書に移ったあとも時々旧ページを開き直して「この表現は本書ではこう変わったんだな」と比べてみてください。

練習問題 #

旧コードのまとまり一つを丸ごとモダンスタイルに移してみます。

旧コード — old_user.py
import os

class User(object):
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

    def __repr__(self):
        return "User(id=%r, name=%r)" % (self.id, self.name)

def load_users(path):
    """
    :param path: users.txt のパス
    :type path: str
    :rtype: List[User]
    """
    full = os.path.join(os.path.dirname(__file__), path)
    users = []
    f = open(full, "r")
    try:
        for line in f:
            parts = line.strip().split(",")
            if len(parts) != 3:
                continue
            id_str, name, email = parts
            users.append(User(int(id_str), name, email))
    finally:
        f.close()
    return users
  1. 上のコードを本付録の変換表どおりにモダンスタイルに移してください。ヒント: @dataclass、型アノテーション、pathlib.Pathwith open(...) as f:list[User]、f-string。
  2. 移したコードを pyright で検査し、エラーがないか確認してください。
  3. pyupgrade --py314-plus old_user.py を回してみて、ツールが自動で処理した部分と、人の手が必要だった部分を比べてみてください。

一行まとめ: 旧 Python コードのほとんどの変換は機械的なので pyupgraderuff --select UP が捕まえてくれます。人の手が必要なのは @dataclasspathlibasync/await、docstring 型くらいです。旧講座 → 本書の推奨パスは付録 A → 第1章 → 第2章 → 第 3 〜 7章を速読 → 2部以降。

X