부록 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 / 2 → 2 (py2) | 5 / 2 → 2.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) == int | isinstance(x, int) |
| 클래스 boilerplate | def __init__(self, a, b): self.a=a; self.b=b | @dataclass |
| 타입 정보 | docstring에 :type x: int | x: 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") / Popen | subprocess.run(["ls"]) |
| 비동기 | 콜백 / 제너레이터 코루틴 | async def + await |
| 도구 체인 | pip install + venv + pyenv | uv add + uv run + uv python install |
단계별 변환 #
1. print가 함수가 됐다
#
print "hello"
print "x =", xprint("hello")
print("x =", x)옛 코드를 Python 3 인터프리터로 돌리면 즉시 SyntaxError. 기계적 변환이라 도구가 한 번에 잡습니다(아래 §“자동화” 참조).
2. 문자열 포맷팅 — % / .format() → f-string
#
세 세대가 공존합니다. 새 코드는 무조건 f-string입니다.
"hi %s, you are %d" % (name, age)"hi {}, you are {}".format(name, age)
"hi {name}, you are {age}".format(name=name, age=age)f"hi {name}, you are {age}"
f"{name = }, {age = }" # 디버깅용자세한 사용은 2장 변수, 기본 타입과 타입 힌트에서.
3. 정수 나눗셈 #
5 / 2 # 2 (정수끼리 나눗셈이 정수)
5.0 / 2 # 2.55 / 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. 예외 잡기 — , e → as e
#
try:
do_work()
except Exception, e:
print "failed:", etry:
do_work()
except Exception as e:
print(f"failed: {e}"), 문법은 Python 3에서 SyntaxError. as 통일.
추가로 Python 3.11부터는 ExceptionGroup + except*가 들어왔습니다. 동시에 발생한 여러 예외를 묶어 다루는 새 도구로, 본문 6장에서 다룹니다.
6. xrange 제거 — range가 이미 lazy
#
for i in xrange(10):
...for i in range(10):
...Python 3의 range가 옛 xrange의 동작을 흡수했습니다. 한 번에 리스트를 만들지 않고 lazy 하게 값을 내보냅니다.
7. 유니코드 — 모든 str이 유니코드
#
s = u"한글" # u 접두사 필요
isinstance(s, unicode)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)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 + bdef add(a: int, b: int) -> int:
"""첫 번째와 두 번째를 더해 반환합니다."""
return a + b타입 정보가 시그니처로 올라옵니다. IDE / 타입 체커 / 문서 생성기 전부 같은 정보를 참조합니다. docstring은 왜와 어떻게 쓰는가만 적습니다.
11. 컬렉션 타입 — typing import → 빌트인 제네릭
#
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()names: list[str] = []
ages: dict[str, int] = {}
point: tuple[float, float] = (0.0, 0.0)
unique: set[str] = set()from typing import List 자체가 사라지는 흐름입니다. 빌트인 타입에 그대로 []를 씁니다.
12. 옵션 타입 — Optional → T | None
#
from typing import Optional, Union
def find(id: int) -> Optional[str]:
...
def parse(s: str) -> Union[int, float, None]:
...def find(id: int) -> str | None:
...
def parse(s: str) -> int | float | None:
...짧고, 추가 import가 없습니다.
13. 경로 — os.path → pathlib.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 / Popen → subprocess.run
#
import os
os.system("ls -la /tmp") # 결과를 받지 못함, 보안 위험from subprocess import Popen, PIPE
proc = Popen(["ls", "-la", "/tmp"], stdout=PIPE, stderr=PIPE)
out, err = proc.communicate()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
#
import asyncio
@asyncio.coroutine
def fetch():
response = yield from get_url("https://example.com")
return responseasync def fetch():
response = await get_url("https://example.com")
return responseasync def / await 키워드가 표준이 됐고, @asyncio.coroutine 데코레이터는 deprecated 됐습니다. 본문 14장 비동기 입문, 18장 비동기 깊이에서 다룹니다.
자동화 — 손으로 다 옮길 필요는 없습니다 #
위 변환 대부분은 기계적입니다. 도구가 한 번에 잡아줍니다.
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 | NoneDict[K, V]→dict[K, V]super(ClassName, self)→super()단축- 옛
print문 → 함수형 - f-string 안의
.format()정리
--py312-plus 인자는 “내 코드가 Python 3.12 이상에서만 돈다고 가정하고 그 버전에서 사용 가능한 새 문법으로 모두 옮겨라"라는 의미입니다. 본 책 기준이라면 --py314-plus.
ruff의 UP 규칙 — pyupgrade의 ruff 통합 #
[tool.ruff.lint]
select = ["UP"] # pyupgrade 룰 활성uv run ruff check . --select UP --fixruff는 pyupgrade의 룰 대부분을 흡수했고, 다른 lint 룰과 한 번에 돌릴 수 있습니다. pre-commit으로 묶어 두면 옛 패턴이 새로 들어오는 것을 상시 막을 수 있습니다 — 30장 타입체커 설정과 CI 통합에서 정식 셋업을 다룹니다.
무엇은 자동화 못 하나 #
- 클래스 →
@dataclass변환 (의도 판단 필요) os.path→pathlib변환 (어디서 어떻게 쓰이는지 사람 판단)- 콜백 / 제너레이터 코루틴 →
async def(구조 변경) - docstring 타입 표기 → 어노테이션 (자동화 도구가 있긴 함, 정확도 한계)
위 4개는 사람이 손으로 옮기는 게 안전합니다.
옛 강좌 → 본 책 — 추천 경로 #
옛 파이썬 기초 강좌 21편의 독자라면 다음 순서를 권합니다.
- 본 부록 A 먼저 — 옛 강좌 시절의 패턴 중 새 책에서 어떻게 다른지 한눈에 본다.
- 본 책 1장 시작과 uv 셋업 — 도구 체인이 완전히 바뀌었으므로 환경부터 다시 깐다.
- 본 책 2장 변수, 기본 타입과 타입 힌트 — 가장 큰 차이인 “타입 힌트 우선"을 체득한다.
- 본 책 3~7장 — 옛 강좌에서 이미 익숙한 주제(제어 흐름, 컬렉션, 함수, 예외, 모듈)를 modern 어법으로 다시 본다. 익숙해서 빠르게 읽힌다.
- 본 책 2부 이후 — 옛 강좌에 없던 영역(dataclass, Protocol, 비동기, FastAPI, 운영)로 진입.
옛 강좌는 사이트에 영구 잔존합니다. 본 책으로 옮긴 뒤에도 가끔 옛 페이지를 다시 펴 보면서 “이 표현이 본 책에서는 이렇게 바뀌었구나"를 비교해 보세요.
연습문제 #
옛 코드 한 덩어리를 modern 스타일로 옮겨 보겠습니다.
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- 위 코드를 본 부록의 변환표대로 modern 스타일로 옮기세요. 힌트:
@dataclass, 타입 어노테이션,pathlib.Path,with open(...) as f:,list[User], f-string. - 옮긴 코드를
pyright로 검사해 에러가 없는지 확인하세요. pyupgrade --py314-plus old_user.py를 돌려 보고, 도구가 자동으로 처리한 부분과 사람이 직접 판단해야 했던 부분을 비교해 보세요.
한 줄 요약: 옛 파이썬 코드 대부분의 변환은 기계적이라
pyupgrade와ruff --select UP가 잡아준다. 사람이 직접 판단해야 하는 부분은@dataclass,pathlib,async/await, docstring 타입 정도. 옛 강좌 → 본 책의 권장 경로는 부록 A → 1장 → 2장 → 3~7장 빠른 읽기 → 2부 이후.