[Python] 문자열 포매팅: %, .format, f-string, Template
python string formatting, %s formatting, str.format, f-string, string.Template, format spec
포맷 스펙:
정의
Python에는 4가지 문자열 포매팅 방법이 있다. 새 코드는 f-string 우선.
| 방법 | 도입 | 상태 |
|---|---|---|
% 포매팅 | 2.x | 레거시, 일부 호환 유지 |
str.format | 3.0 | 동적 포맷 시 유용 |
| f-string | 3.6 | 현대 권장 |
string.Template | 2.4 | 안전한 사용자 입력 처리 |
f-string (PEP 498)
name = "Alice"
age = 30
f"Hello, {name}" # 'Hello, Alice'
f"{name} is {age}" # 'Alice is 30'
f"{age + 5}" # '35' (표현식)
f"{name.upper()}" # 'ALICE' (메서드)
f"{[1, 2, 3]!r}" # '[1, 2, 3]' (repr)
f"{age!s}" # '30' (str)
# 3.8+ 디버깅 헬퍼
f"{name=}" # "name='Alice'"
f"{age * 2 =}" # 'age * 2 =60'
가장 빠르고 (컴파일 타임 처리), 읽기 쉽고, 표현력 좋음.
포맷 스펙: :
f"{number:format_spec}"
정렬·폭·채움
f"{name:>10}" # ' Alice' (우측 정렬, 폭 10)
f"{name:<10}" # 'Alice ' (좌측)
f"{name:^10}" # ' Alice ' (가운데)
f"{name:*>10}" # '*****Alice' (채움 문자 *)
f"{42:05d}" # '00042' (0 패딩, 정수, 폭 5)
진법
f"{255:b}" # '11111111' (2진)
f"{255:o}" # '377' (8진)
f"{255:x}" # 'ff' (16진 소문자)
f"{255:X}" # 'FF' (대문자)
f"{255:#x}" # '0xff' (접두)
부동소수
pi = 3.14159265
f"{pi:.2f}" # '3.14' (소수점 2자리)
f"{pi:.4f}" # '3.1416'
f"{pi:8.2f}" # ' 3.14' (폭 8, 소수 2)
f"{pi:e}" # '3.141593e+00' (지수)
f"{pi:.2%}" # '314.16%'
f"{0.5:.1%}" # '50.0%'
f"{1234567:,}" # '1,234,567' (천 단위 구분)
f"{1234567:_}" # '1_234_567'
결합
f"{name:>10.5}" # ' Alice' (우측 정렬 + 최대 5자)
f"{pi:0>10.3f}" # '000003.142'
동적 포맷
f-string은 컴파일 타임이라 포맷 spec을 변수로 받기는 까다로움. format() 함수가 깔끔.
fmt = ".3f"
value = 3.14159
f"{value:{fmt}}" # 가능
format(value, fmt) # 더 명확
폭도 변수로:
w = 10
f"{name:>{w}}"
str.format
"Hello, {}".format(name)
"{} is {}".format(name, age)
"{0} {1} {0}".format("hi", "world") # 위치
"{name} is {age}".format(name="Alice", age=30) # 키워드
"{u.name} ({u.age})".format(u=user) # 속성
"{0[0]}".format([1, 2]) # 인덱스
"{:.2f}".format(3.14159) # 포맷 spec
f-string보다 장황하나 동적 포맷에 강함.
template = "User: {name}, Age: {age}"
template.format(name="Alice", age=30)
사용자가 템플릿을 제공하는 경우 (단, 신뢰할 수 없는 입력엔 Template 사용).
% 포매팅 (레거시)
"Hello, %s" % name
"%d items, %.2f total" % (count, total)
"%(name)s: %(age)d" % {"name": "Alice", "age": 30}
C의 printf 스타일. 여전히 logging 모듈에서 권장 (lazy 평가).
logger.debug("User %s logged in", user) # logging은 % 스타일
string.Template (안전)
사용자가 템플릿을 제공할 때 임의 표현식 실행을 차단.
from string import Template
t = Template("Hello, $name! You are $age years old.")
print(t.substitute(name="Alice", age=30))
# safe_substitute: 누락 키 무시
t.safe_substitute(name="Alice") # 'Hello, Alice! You are $age years old.'
f-string과 .format은 임의 속성/메서드 접근 가능 → 사용자 입력으로는 위험.
# WRONG
user_template.format(secret=secret_object)
# user_template = "{secret.__class__.__init__.__globals__}" → 메모리 누출
# CORRECT
Template(user_template).safe_substitute(secret=...) # 단순 $ 치환만
자주 보는 포맷 패턴
표 출력
print(f"{'Name':<10} {'Age':>5} {'Score':>7}")
for name, age, score in data:
print(f"{name:<10} {age:>5d} {score:>7.2f}")
진행률
def progress(current, total, width=40):
pct = current / total
filled = int(width * pct)
bar = "#" * filled + "-" * (width - filled)
print(f"\r[{bar}] {pct:.1%} ({current}/{total})", end="")
로그 메시지
logger.info(
"request_id=%s user=%s duration=%.3fms",
req_id, user_id, duration,
)
logging은 % 스타일이 lazy → 디스에이블된 레벨에서 포매팅 비용 없음.
통화
def fmt_krw(amount):
return f"{amount:,}원"
fmt_krw(1234567) # '1,234,567원'
locale 모듈로 정식 처리 가능하지만 보통 간단한 f-string으로 충분.
datetime
from datetime import datetime
now = datetime.now()
f"{now:%Y-%m-%d %H:%M:%S}" # '2026-06-20 14:30:00'
f-string 내에서 datetime의 __format__이 strftime처럼 동작.
클래스 포맷
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
def __format__(self, spec):
if spec == "s":
return f"{self.amount:,} {self.currency}"
if spec == "compact":
return f"{self.amount}{self.currency}"
return f"{self.amount}"
m = Money(1000, "USD")
f"{m:s}" # '1,000 USD'
f"{m:compact}" # '1000USD'
객체에 __format__ 정의하면 f-string 포맷 spec 처리 가능.
성능
f-string > str.format > % > Template.
벤치마크에서 f-string이 가장 빠름. logging의 %는 lazy 평가라 비활성화된 레벨에서 우위.
f-string 새 기능 (3.12+, PEP 701)
# 3.12+ 이전: 같은 인용부호 중첩 불가
f'{"hello"}' # 3.12 이전 OK
f"{f"hi {name}"}" # 3.12+ OK (같은 인용부호 중첩)
# 백슬래시 OK (3.12+)
f"{ '\\n'.join(items) }"
# 멀티라인 표현식 OK (3.12+)
f"{
[x ** 2 for x in items]
}"
3.12 이전엔 임시 변수로 회피.
함정
1. 신뢰할 수 없는 입력
template = user_input
template.format(secret_obj) # 위험
Template(template).safe_substitute(...) # 안전
2. % 의 단일 vs 튜플
"%s" % "hello" # OK
"%s" % ("hello",) # OK
"%s" % ("a", "b") # TypeError (인자 너무 많음)
"%s %s" % ("a", "b") # OK
% 포매팅은 다중 인자에 튜플 필요.
3. dict 포매팅
"%(name)s" % {"name": "Alice"} # OK
"{name}".format(name="Alice") # OK
f"{name}" # 변수 name 필요 (dict X)
d = {"name": "Alice"}
f"{d['name']}" # OK
f-string은 변수 스코프, .format/dict 더 유연.
4. format spec과 컬론
f"{x:{spec}}" # OK
f"{d['name']:>10}" # OK
f"{x:{':'}}" # 헷갈림 → 임시 변수
복잡한 spec은 변수로.
정리
- 새 코드 → f-string
- 동적 템플릿 →
str.format - 사용자 입력 →
string.Template.safe_substitute - logging →
%(lazy)
💬 댓글