목차
부록
  1. 34.부록 A — 옛 파이썬 코드를 modern 스타일로 옮기기
34 장

부록 A — 옛 파이썬 코드를 modern 스타일로 옮기기

2017년 스타일 파이썬 코드(% 문자열, has_key, type() 비교 등)를 현대 파이썬 스타일로 한 단계씩 옮기는 가이드. 옛 파이썬 기초 강좌 21편의 독자가 본 책으로 자연스럽게 이어 읽을 수 있게.

본 부록은 이 사이트의 옛 파이썬 기초 강좌 21편(2017년)을 읽었거나, 파이썬 2.x ~ 3.5 시절에 배운 뒤 멈춰 있던 사람의 전환을 돕는 부록입니다.

본문 1~33장이 “처음부터 modern으로 시작하는” 책이라면, 본 부록은 옛 코드를 손에 든 채로 한 단계씩 modern으로 옮기는 안내입니다. 같이 알고 있으면 옛 강좌의 자료도 버리지 않고 갱신해 쓸 수 있습니다.

옛 / 새 1:1 대응 — 한 페이지 요약 #

다음 표가 본 부록의 압축본입니다. 각 항목은 아래에서 짝 코드와 함께 다시 보겠습니다.

영역옛 코드 (2017 전후)새 코드 (modern)
출력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)
유니코드u"한글", 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+)
옵션 타입Optional[int]`int
경로os.path.join(a, b)Path(a) / b
외부 명령os.system("ls") / Popensubprocess.run(["ls"])
비동기콜백 / 제너레이터 코루틴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 #

세 세대가 공존합니다. 새 코드는 무조건 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. 유니코드 — 모든 str이 유니코드 #

옛 (Python 2)
s = u"한글"     # u 접두사 필요
isinstance(s, unicode)
새 (Python 3)
s = "한글"      # 그냥 str 이 곧 유니코드
isinstance(s, str)

unicode라는 타입 자체가 없습니다. str = 유니코드, bytes = 바이트 시퀀스. 둘은 서로 자동 변환되지 않으며 .encode() / .decode()로 명시 변환.

8. 타입 검사 — type() 비교 대신 isinstance() #

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

isinstance는 상속 관계까지 인식합니다. type() 비교는 정확히 같은 타입에서만 참이라 상속을 다루지 못합니다. 그리고 modern 파이썬에서는 애초에 런타임 타입 검사보다 타입 힌트 + 정적 검증으로 옮기는 흐름입니다(2장 타입 체커 참조).

9. 클래스 boilerplate — __init__ 수십 줄을 @dataclass 한 줄로 #

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__가 자동으로 만들어집니다. 추가 옵션(frozen, slots, kw_only)까지 다 한 줄로. 본문 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. 옵션 타입 — 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. 비동기 — 콜백 / 제너레이터 코루틴 → 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 변환 (어디서 어떻게 쓰이는지 사람 판단)
  • 콜백 / 제너레이터 코루틴 → async def (구조 변경)
  • docstring 타입 표기 → 어노테이션 (자동화 도구가 있긴 함, 정확도 한계)

위 4개는 사람이 손으로 옮기는 게 안전합니다.

옛 강좌 → 본 책 — 추천 경로 #

파이썬 기초 강좌 21편의 독자라면 다음 순서를 권합니다.

  1. 본 부록 A 먼저 — 옛 강좌 시절의 패턴 중 새 책에서 어떻게 다른지 한눈에 본다.
  2. 본 책 1장 시작과 uv 셋업 — 도구 체인이 완전히 바뀌었으므로 환경부터 다시 깐다.
  3. 본 책 2장 변수, 기본 타입과 타입 힌트 — 가장 큰 차이인 “타입 힌트 우선"을 체득한다.
  4. 본 책 3~7장 — 옛 강좌에서 이미 익숙한 주제(제어 흐름, 컬렉션, 함수, 예외, 모듈)를 modern 어법으로 다시 본다. 익숙해서 빠르게 읽힌다.
  5. 본 책 2부 이후 — 옛 강좌에 없던 영역(dataclass, Protocol, 비동기, FastAPI, 운영)로 진입.

옛 강좌는 사이트에 영구 잔존합니다. 본 책으로 옮긴 뒤에도 가끔 옛 페이지를 다시 펴 보면서 “이 표현이 본 책에서는 이렇게 바뀌었구나"를 비교해 보세요.

연습문제 #

옛 코드 한 덩어리를 modern 스타일로 옮겨 보겠습니다.

옛 코드 — 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. 위 코드를 본 부록의 변환표대로 modern 스타일로 옮기세요. 힌트: @dataclass, 타입 어노테이션, pathlib.Path, with open(...) as f:, list[User], f-string.
  2. 옮긴 코드를 pyright로 검사해 에러가 없는지 확인하세요.
  3. pyupgrade --py314-plus old_user.py를 돌려 보고, 도구가 자동으로 처리한 부분과 사람이 직접 판단해야 했던 부분을 비교해 보세요.

한 줄 요약: 옛 파이썬 코드 대부분의 변환은 기계적이라 pyupgraderuff --select UP가 잡아준다. 사람이 직접 판단해야 하는 부분은 @dataclass, pathlib, async/await, docstring 타입 정도. 옛 강좌 → 본 책의 권장 경로는 부록 A → 1장 → 2장 → 3~7장 빠른 읽기 → 2부 이후.

X