본문으로 건너뛰기
김신건의 로그

[Python] 문자열 포매팅: %, .format, f-string, Template

· 수정 · 📖 약 1분 · 535자/단어 #python #string #formatting #f-string #stdlib
python string formatting, %s formatting, str.format, f-string, string.Template, format spec

정의

Python에는 4가지 문자열 포매팅 방법이 있다. 새 코드는 f-string 우선.

방법도입상태
% 포매팅2.x레거시, 일부 호환 유지
str.format3.0동적 포맷 시 유용
f-string3.6현대 권장
string.Template2.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)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기