Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 750 – 템플릿 문자열

Author:
Jim Baker <jim.baker at python.org>, Guido van Rossum <guido at python.org>, Paul Everitt <pauleveritt at me.com>, Koudai Aono <koxudaxi at gmail.com>, Lysandros Nikolaou <lisandrosnik at gmail.com>, Dave Peck <davepeck at davepeck.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
08-Jul-2024
Python-Version:
3.14
Post-History:
09-Aug-2024, 17-Oct-2024, 21-Oct-2024, 18-Nov-2024
Resolution:
10-Apr-2025

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Template strings.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 사용자 정의 문자열 처리를 위한 템플릿 문자열을 도입합니다.

템플릿 문자열은 f-문자열을 일반화한 것으로, f 접두사 대신 t를 사용합니다. t-문자열은 str로 평가되는 대신 새로운 타입인 Template으로 평가됩니다:

template: Template = t"Hello {name}"

템플릿은 문자열과 그 보간된 값이 결합되기 전에 개발자가 이들에 접근할 수 있게 해 줍니다. 이를 통해 Python 언어에 유연한 문자열 처리 기능이 기본으로 제공되며, 안전성 검사, 웹 템플릿, 도메인 특화 언어 등이 가능해집니다.

다른 PEP와의 관계

Python은 Python 3.6에서 PEP 498을 통해 f-문자열을 도입했습니다. 이후 PEP 701에서 문법이 공식화되었으며, 몇 가지 제약도 함께 해제되었습니다. 이 PEP는 PEP 701을 기반으로 합니다.

PEP 498이 등장한 것과 거의 같은 시기에, “i-문자열”, 즉 “보간 템플릿 문자열”을 제공하기 위해 PEP 501이 작성되었습니다. 해당 PEP는 f-문자열에 대한 경험이 더 쌓일 때까지 보류되었습니다. 2023년 3월에 다른 저자가 이 PEP에 대한 작업을 재개하여, 템플릿 리터럴 문자열로서 “t-문자열”을 도입하고 PEP 701을 기반으로 구축했습니다.

이 PEP의 저자들은 이 PEP를 PEP 501의 갱신된 작업을 일반화하고 단순화한 것으로 봅니다. (해당 PEP 역시 최근에 이 PEP의 새로운 아이디어를 반영하도록 갱신되었습니다.)

동기

Python f-문자열은 사용하기 쉽고 매우 인기가 높습니다. 그러나 시간이 지나면서 개발자들은 f-문자열을 특정 사용 사례에 부적합하게 만드는 한계에 부딪혀 왔습니다. 특히 f-문자열은 보간된 값이 최종 문자열로 결합되기 전에 이를 가로채어 변환할 방법을 제공하지 않습니다.

그 결과, f-문자열을 부주의하게 사용하면 보안 취약점이 생길 수 있습니다. 예를 들어, sqlite3 모듈로 SQL 쿼리를 실행하는 사용자는 f-문자열을 사용해 SQL 표현식에 값을 삽입하고 싶은 유혹을 느낄 수 있는데, 이는 SQL 인젝션 공격으로 이어질 수 있습니다. 또는 HTML을 생성하는 개발자가 이스케이프되지 않은 사용자 입력을 문자열에 포함시켜 크로스 사이트 스크립팅(XSS) 취약점이 발생할 수도 있습니다.

더 넓게 보면, 보간된 값을 최종 문자열로 결합하기 전에 변환할 수 없다는 점은 보다 복잡한 문자열 처리 작업에서 f-문자열의 유용성을 제한합니다.

템플릿 문자열은 개발자가 문자열과 그 보간된 값에 접근할 수 있게 함으로써 이러한 문제를 해결합니다.

예를 들어, HTML을 생성하려 한다고 가정해 봅시다. 템플릿 문자열을 사용하면 콘텐츠를 자동으로 정제(sanitize)해 주는 html() 함수를 정의할 수 있습니다:

evil = "<script>alert('evil')</script>"
template = t"<p>{evil}</p>"
assert html(template) == "<p>&lt;script&gt;alert('evil')&lt;/script&gt;</p>"

마찬가지로, 이 가상의 html() 함수를 사용하면 개발자가 딕셔너리를 이용해 HTML 요소에 속성을 쉽게 추가할 수 있습니다:

attributes = {"src": "shrubbery.jpg", "alt": "looks nice"}
template = t"<img {attributes} />"
assert html(template) == '<img src="shrubbery.jpg" alt="looks nice" />'

이 두 예제 모두 f-문자열로는 불가능합니다. 템플릿 문자열은 보간된 값을 가로채어 변환하는 메커니즘을 제공함으로써 폭넓은 문자열 처리 사용 사례를 가능하게 합니다.

명세

템플릿 문자열 리터럴

이 PEP는 템플릿 문자열 리터럴을 정의하기 위한 새로운 문자열 접두사 t를 도입합니다. 이 리터럴은 표준 라이브러리 모듈 string.templatelib에 있는 새로운 타입 Template으로 평가됩니다.

다음 코드는 Template 인스턴스를 생성합니다:

from string.templatelib import Template
template = t"This is a template string."
assert isinstance(template, Template)

템플릿 문자열 리터럴은 PEP 701의 전체 문법을 지원합니다. 여기에는 보간 안에 템플릿 문자열을 중첩하는 기능과 유효한 모든 따옴표(', ", ''', """)를 사용하는 기능이 포함됩니다. 다른 문자열 접두사와 마찬가지로 t 접두사는 따옴표 바로 앞에 와야 합니다. f-문자열과 마찬가지로 소문자 t와 대문자 T 접두사를 모두 지원합니다. f-문자열과 마찬가지로 t-문자열은 u나 b 접두사와 결합할 수 없습니다.

또한 f-문자열과 t-문자열은 결합할 수 없으므로 ft 접두사는 유효하지 않습니다. t-문자열은 r 접두사와 결합할 수 있습니다. 자세한 내용은 아래의 Raw Template Strings 절을 참조하십시오.

Template 타입

템플릿 문자열은 새로운 불변 타입인 string.templatelib.Template의 인스턴스로 평가됩니다:

class Template:
    strings: tuple[str, ...]
    """
    A non-empty tuple of the string parts of the template,
    with N+1 items, where N is the number of interpolations
    in the template.
    """

    interpolations: tuple[Interpolation, ...]
    """
    A tuple of the interpolation parts of the template.
    This will be an empty tuple if there are no interpolations.
    """

    def __new__(cls, *args: str | Interpolation):
        """
        Create a new Template instance.

        Arguments can be provided in any order.
        """
        ...

    @property
    def values(self) -> tuple[object, ...]:
        """
        Return a tuple of the `value` attributes of each Interpolation
        in the template.
        This will be an empty tuple if there are no interpolations.
        """
        ...

    def __iter__(self) -> Iterator[str | Interpolation]:
        """
        Iterate over the string parts and interpolations in the template.

        These may appear in any order. Empty strings will not be included.
        """
        ...

strings와 interpolations 속성을 통해 리터럴의 문자열 부분과 모든 보간에 접근할 수 있습니다:

name = "World"
template = t"Hello {name}"
assert template.strings[0] == "Hello "
assert template.interpolations[0].value == "World"

Interpolation 타입

Interpolation 타입은 템플릿 문자열 안의 표현식을 나타냅니다. Template과 마찬가지로 이것은 string.templatelib 모듈에 있는 새로운 클래스입니다:

class Interpolation:
    value: object
    expression: str
    conversion: Literal["a", "r", "s"] | None
    format_spec: str

    __match_args__ = ("value", "expression", "conversion", "format_spec")

    def __new__(
        cls,
        value: object,
        expression: str = "",
        conversion: Literal["a", "r", "s"] | None = None,
        format_spec: str = "",
    ):
        ...

Interpolation 타입은 얕은 불변(shallow immutable)입니다. 그 속성은 다시 할당할 수 없습니다.

value 속성은 보간을 평가한 결과입니다:

name = "World"
template = t"Hello {name}"
assert template.interpolations[0].value == "World"

템플릿 문자열 리터럴로부터 보간이 생성될 때 expression 속성에는 보간의 원본 텍스트가 담깁니다:

name = "World"
template = t"Hello {name}"
assert template.interpolations[0].expression == "name"

개발자가 Interpolation을 명시적으로 생성할 때는 expression 속성의 값을 선택적으로 제공할 수 있습니다. 문자열로 저장되기는 하지만, 이 값은 유효한 Python 표현식이어야 합니다. 값을 제공하지 않으면 expression 속성의 기본값은 빈 문자열("")입니다.

대부분의 템플릿 처리 코드에서는 expression 속성을 사용하지 않을 것으로 예상합니다. 이 속성은 완전성을 위해, 그리고 디버깅과 인트로스펙션에 사용하기 위해 제공됩니다. 템플릿 문자열을 처리하는 방법에 대한 자세한 내용은 Common Patterns Seen in Processing Templates 절과 Examples 절을 모두 참조하십시오.

conversion 속성은 사용할 선택적 변환으로, repr(), str(), ascii() 변환에 각각 대응하는 r, s, a 중 하나입니다. f-문자열과 마찬가지로 다른 변환은 지원하지 않습니다:

name = "World"
template = t"Hello {name!r}"
assert template.interpolations[0].conversion == "r"

변환을 제공하지 않으면 conversion은 None입니다.

format_spec 속성은 포맷 명세입니다. f-문자열과 마찬가지로 이것은 값을 표시하는 방법을 정의하는 임의의 문자열입니다:

value = 42
template = t"Value: {value:.2f}"
assert template.interpolations[0].format_spec == ".2f"

f-문자열의 포맷 명세는 그 자체로 보간을 포함할 수 있습니다. 이는 템플릿 문자열에서도 허용되며, format_spec은 즉시 평가된 결과로 설정됩니다:

value = 42
precision = 2
template = t"Value: {value:.{precision}f}"
assert template.interpolations[0].format_spec == ".2f"

형식 명세가 제공되지 않으면 format_spec은 기본값으로 빈 문자열("")이 됩니다. 이는 파이썬 내장 함수 format()의 format_spec 매개변수와 일치합니다.

f-문자열과 달리, conversion과 format_spec 속성을 어떻게 해석할지는 템플릿을 처리하는 코드가 결정합니다. 그러한 코드가 이 속성들을 반드시 사용해야 하는 것은 아니지만, 속성이 있는 경우에는 이를 존중해야 하며 가능한 한 f-문자열의 동작과 일치시켜야 합니다. 예를 들어 {value:.2f}를 사용하는 템플릿 문자열을 처리할 때 값이 소수점 둘째 자리까지 반올림되지 않는다면 놀라운 일일 것입니다.

Template.values 프로퍼티

Template.values 프로퍼티는 템플릿에 있는 각 Interpolation의 value 속성에 접근하기 위한 단축 방법이며, 다음과 동등합니다:

@property
def values(self) -> tuple[object, ...]:
    return tuple(i.value for i in self.interpolations)

Template 내용 이터레이션

Template.__iter__() 메서드는 템플릿의 전체 내용에 접근하는 간단한 방법을 제공합니다. 이 메서드는 문자열 부분과 보간을 나타나는 순서대로 산출하며, 빈 문자열은 생략합니다.

__iter__() 메서드는 다음과 동등합니다:

def __iter__(self) -> Iterator[str | Interpolation]:
    for s, i in zip_longest(self.strings, self.interpolations):
        if s:
            yield s
        if i:
            yield i

다음 예제는 __iter__() 메서드가 동작하는 모습을 보여 줍니다:

assert list(t"") == []

assert list(t"Hello") == ["Hello"]

name = "World"
template = t"Hello {name}!"
contents = list(template)
assert len(contents) == 3
assert contents[0] == "Hello "
assert contents[1].value == "World"
assert contents[1].expression == "name"
assert contents[2] == "!"

Template.strings에 있을 수 있는 빈 문자열은 __iter__() 메서드의 출력에 포함되지 않습니다:

first = "Eat"
second = "Red Leicester"
template = t"{first}{second}"
contents = list(template)
assert len(contents) == 2
assert contents[0].value == "Eat"
assert contents[0].expression == "first"
assert contents[1].value == "Red Leicester"
assert contents[1].expression == "second"

# However, the strings attribute contains empty strings:
assert template.strings == ("", "", "")

템플릿 처리 코드는 요구 사항과 편의에 따라 strings, interpolations, values, __iter__()를 어떤 조합으로든 사용할 수 있습니다.

템플릿 문자열 처리

개발자는 템플릿 문자열을 처리하는 임의의 코드를 작성할 수 있습니다. 예를 들어 다음 함수는 템플릿의 정적 부분은 소문자로, 보간은 대문자로 렌더링합니다:

from string.templatelib import Template, Interpolation

def lower_upper(template: Template) -> str:
    """Render static parts lowercased and interpolations uppercased."""
    parts: list[str] = []
    for item in template:
        if isinstance(item, Interpolation):
            parts.append(str(item.value).upper())
        else:
            parts.append(item.lower())
    return "".join(parts)

name = "world"
assert lower_upper(t"HELLO {name}") == "hello WORLD"

템플릿 문자열을 특정한 방식으로 처리해야 한다는 요구 사항은 없습니다. 템플릿을 처리하는 코드가 문자열을 반환해야 할 의무는 없습니다. 템플릿 문자열은 유연한 범용 기능입니다.

템플릿 문자열을 처리하는 방법에 대한 자세한 내용은 Common Patterns Seen in Processing Templates 섹션을 참조하십시오. 실제로 동작하는 자세한 예제는 Examples 섹션을 참조하십시오.

템플릿 문자열 연결

템플릿 문자열은 +를 사용한 명시적 연결을 지원합니다. 두 Template 인스턴스의 연결은 Template.__add__()를 통해 지원됩니다:

name = "World"

assert isinstance(t"Hello " + t"{name}", Template)
assert (t"Hello " + t"{name}").strings == ("Hello ", "")
assert (t"Hello " + t"{name}").values[0] == "World"

두 템플릿 문자열 리터럴의 암묵적 연결도 지원됩니다:

name = "World"
assert isinstance(t"Hello " t"{name}", Template)
assert (t"Hello " t"{name}").strings == ("Hello ", "")
assert (t"Hello " t"{name}").values[0] == "World"

Template과 str의 암묵적 연결과 명시적 연결은 모두 금지됩니다. str을 정적 문자열 부분으로 취급해야 할지, 보간으로 취급해야 할지 모호하기 때문입니다.

Template과 str을 결합하려면 개발자가 str을 어떻게 취급할지 명시적으로 결정해야 합니다. str을 정적 문자열 부분으로 의도했다면 Template으로 감싸야 합니다. str을 보간 값으로 의도했다면 Interpolation으로 감싸서 Template 생성자에 전달해야 합니다. 예를 들면 다음과 같습니다:

name = "World"

# Treat `name` as a static string part
template = t"Hello " + Template(name)

# Treat `name` as an interpolation
template = t"Hello " + Template(Interpolation(name, "name"))

템플릿(Template)과 보간(Interpolation)의 동등성

Template과 Interpolation 인스턴스는 객체 동일성(is)으로 비교됩니다.

Template 인스턴스는 템플릿 처리 코드에서 사용하도록 만들어졌으며, 이 코드는 문자열이나 다른 어떤 타입이든 반환할 수 있습니다. 그러한 타입은 필요에 따라 자체적인 동등성 의미를 제공할 수 있습니다.

순서 비교 미지원

Template과 Interpolation 타입은 순서 비교를 지원하지 않습니다. 이는 사전식 순서 비교를 지원하는 Python의 다른 모든 문자열 리터럴 타입과 다른 점입니다. 보간은 임의의 값을 담을 수 있으므로, 보간에는 자연스러운 순서가 없습니다. 따라서 Template 타입과 Interpolation 타입 모두 표준 비교 메서드를 구현하지 않습니다.

디버그 지정자(=) 지원

디버그 지정자 =는 템플릿 문자열에서 지원되며 f-문자열에서와 비슷하게 동작하지만, 구현상의 제약으로 인해 약간의 차이가 있습니다.

구체적으로, t'{value=}'는 t'value={value!r}'로 처리됩니다. 첫 번째 정적 문자열이 ""에서 "value="로 다시 작성되고, conversion의 기본값은 r이 됩니다:

name = "World"
template = t"Hello {name=}"
assert template.strings[0] == "Hello name="
assert template.interpolations[0].value == "World"
assert template.interpolations[0].conversion == "r"

변환을 명시적으로 지정하면 그대로 유지됩니다: t'{value=!s}'는 t'value={value!s}'로 처리됩니다.

변환 없이 형식 문자열만 지정하면 conversion은 None으로 설정됩니다: t'{value=:fmt}'는 t'value={value:fmt}'로 처리됩니다.

디버그 지정자에서는 공백이 보존되므로, t'{value = }'는 t'value = {value!r}'로 처리됩니다.

원시 템플릿 문자열

원시 템플릿 문자열은 rt(또는 tr) 접두사를 사용하여 지원됩니다:

trade = 'shrubberies'
template = rt'Did you say "{trade}"?\n'
assert template.strings[0] == r'Did you say "'
assert template.strings[1] == r'"?\n'

이 예에서 \n은 줄바꿈 문자가 아니라 두 개의 개별 문자(백슬래시 뒤에 ‘n’)로 처리됩니다. 이는 Python의 원시 문자열 동작과 일관됩니다.

일반 템플릿 문자열과 마찬가지로 원시 템플릿 문자열의 보간도 정상적으로 처리되므로, 원시 문자열 동작과 동적 콘텐츠를 함께 사용할 수 있습니다.

보간 표현식 평가

보간의 표현식 평가는 PEP 498에서와 같습니다:

문자열에서 추출된 표현식은 템플릿 문자열이 나타난 문맥에서 평가됩니다. 즉, 표현식은 지역 변수와 전역 변수를 포함하여 자신의 렉시컬 스코프에 완전히 접근할 수 있습니다. 함수 호출과 메서드 호출을 포함하여 유효한 Python 표현식이라면 무엇이든 사용할 수 있습니다.

템플릿 문자열은 f-문자열과 마찬가지로 왼쪽에서 오른쪽으로 즉시 평가됩니다. 즉, 보간은 지연되거나 람다로 감싸지지 않고, 템플릿 문자열이 처리될 때 곧바로 평가됩니다.

예외

t-문자열 리터럴에서 발생하는 예외는 f-문자열 리터럴에서 발생하는 예외와 같습니다.

Template.__str__() 구현 없음

Template 타입은 별도로 특화된 __str__() 구현을 제공하지 않습니다.

이는 Template 인스턴스가 템플릿 처리 코드에서 사용하도록 만들어졌기 때문이며, 이 코드는 문자열을 반환할 수도 있고 다른 타입을 반환할 수도 있습니다. 템플릿(Template)을 문자열로 변환하는 표준적인 방법은 없습니다.

Template과 Interpolation 타입은 모두 유용한 __repr__() 구현을 제공합니다.

string.templatelib 모듈

string 모듈은 패키지로 바뀌며, Template과 Interpolation 타입을 담은 새로운 templatelib 하위 모듈이 추가됩니다. 이 PEP가 구현된 후에는 이 새 모듈을 convert()와 같은 관련 함수에 사용하거나, 셸 스크립트 헬퍼처럼 앞으로 추가될 수 있는 템플릿 처리 코드에 사용할 수 있습니다.

예시

PEP의 이 절에 있는 모든 예제에는 완전히 테스트된 참조 구현이 있으며, 이 구현은 공개 pep750-examples git 저장소에서 확인할 수 있습니다.

예제: t-문자열로 f-문자열 구현하기

t-문자열을 사용하면 f-문자열을 쉽게 “구현”할 수 있습니다. 즉, f-문자열 리터럴과 거의 같은 방식으로 Template을 처리하여 같은 결과를 반환하는 f(template: Template) -> str 함수를 작성할 수 있습니다:

name = "World"
value = 42
templated = t"Hello {name!r}, value: {value:.2f}"
formatted = f"Hello {name!r}, value: {value:.2f}"
assert f(templated) == formatted

f() 함수는 !r과 같은 변환 지정자와 :.2f와 같은 형식 지정자를 모두 지원합니다. 전체 코드는 꽤 간단합니다:

from string.templatelib import Template, Interpolation

def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
    if conversion == "a":
        return ascii(value)
    elif conversion == "r":
        return repr(value)
    elif conversion == "s":
        return str(value)
    return value

def f(template: Template) -> str:
    parts = []
    for item in template:
        match item:
            case str() as s:
                parts.append(s)
            case Interpolation(value, _, conversion, format_spec):
                value = convert(value, conversion)
                value = format(value, format_spec)
                parts.append(value)
    return "".join(parts)

Note

예제 코드

fstring.py와 test_fstring.py를 참조하십시오.

예제: 구조화된 로깅

구조화된 로깅을 사용하면 개발자는 JSON과 같이 기계가 읽을 수 있는 형식으로 데이터를 기록할 수 있습니다. t-문자열을 사용하면 개발자는 로그 문 하나만으로 사람이 읽을 수 있는 메시지와 함께 구조화된 데이터를 쉽게 기록할 수 있습니다.

템플릿 문자열로 구조화된 로깅을 구현하는 두 가지 방식을 소개합니다.

방식 1: 사용자 정의 로그 메시지

파이썬 로깅 쿡북에는 구조화된 로깅을 구현하는 방법에 관한 짧은 절이 있습니다.

로깅 쿡북은 간단한 텍스트 메시지와 별도의 값 딕셔너리로 생성되는 새로운 “메시지” 클래스인 StructuredMessage를 만들 것을 제안합니다:

message = StructuredMessage("user action", {
    "action": "traded",
    "amount": 42,
    "item": "shrubs"
})
logging.info(message)

# Outputs:
# user action >>> {"action": "traded", "amount": 42, "item": "shrubs"}

StructuredMessage.__str__() 메서드는 사람이 읽을 수 있는 메시지와 값을 모두 형식화하고, 이를 합쳐 최종 문자열을 만듭니다. (전체 예제는 로깅 쿡북을 참조하십시오.)

템플릿 문자열을 사용하면 StructuredMessage의 개선된 버전을 구현할 수 있습니다:

import json
from string.templatelib import Interpolation, Template
from typing import Mapping

class TemplateMessage:
    def __init__(self, template: Template) -> None:
        self.template = template

    @property
    def message(self) -> str:
        # Use the f() function from the previous example
        return f(self.template)

    @property
    def values(self) -> Mapping[str, object]:
        return {
            item.expression: item.value
            for item in self.template
            if isinstance(item, Interpolation)
        }

    def __str__(self) -> str:
        return f"{self.message} >>> {json.dumps(self.values)}"

_ = TemplateMessage  # optional, to improve readability
action, amount, item = "traded", 42, "shrubs"
logging.info(_(t"User {action}: {amount:.2f} {item}"))

# Outputs:
# User traded: 42.00 shrubs >>> {"action": "traded", "amount": 42, "item": "shrubs"}

템플릿 문자열을 사용하면 사용자 정의 메시지 클래스를 더 우아하게 정의할 수 있습니다. 템플릿 문자열을 사용하면 개발자가 형식 문자열과 값 딕셔너리를 서로 일치하게 유지하는지 더 이상 확인할 필요가 없습니다. 템플릿 문자열 리터럴 하나만 있으면 됩니다. TemplateMessage 구현은 Interpolation.expression과 Interpolation.value 속성에서 각각 구조화된 키와 값을 자동으로 추출할 수 있습니다.

방식 2: 사용자 정의 포매터

사용자 정의 메시지는 구조화된 로깅에 적합한 방식이지만 다소 번거로울 수 있습니다. 이 방식을 사용하려면 개발자는 작성하는 모든 로그 메시지를 사용자 정의 클래스로 감싸야 합니다. 이는 잊어버리기 쉽습니다.

다른 방법은 사용자 정의 logging.Formatter 클래스를 정의하는 것입니다. 이 방법은 더 유연하며 최종 출력을 더 세밀하게 제어할 수 있습니다. 특히 하나의 템플릿 문자열을 받아 여러 형식(사람이 읽을 수 있는 형식과 JSON)으로 서로 다른 로그 스트림에 출력할 수 있습니다.

사람이 읽을 수 있는 출력을 위한 MessageFormatter와 JSON 출력을 위한 ValuesFormatter라는 두 가지 간단한 포매터를 정의합니다:

import json
from logging import Formatter, LogRecord
from string.templatelib import Interpolation, Template
from typing import Any, Mapping


class MessageFormatter(Formatter):
    def message(self, template: Template) -> str:
        # Use the f() function from the previous example
        return f(template)

    def format(self, record: LogRecord) -> str:
        msg = record.msg
        if not isinstance(msg, Template):
            return super().format(record)
        return self.message(msg)


class ValuesFormatter(Formatter):
    def values(self, template: Template) -> Mapping[str, Any]:
        return {
            item.expression: item.value
            for item in template
            if isinstance(item, Interpolation)
        }

    def format(self, record: LogRecord) -> str:
        msg = record.msg
        if not isinstance(msg, Template):
            return super().format(record)
        return json.dumps(self.values(msg))

그런 다음 로거를 구성할 때 이 포매터들을 사용할 수 있습니다:

import logging
import sys

logger = logging.getLogger(__name__)
message_handler = logging.StreamHandler(sys.stdout)
message_handler.setFormatter(MessageFormatter())
logger.addHandler(message_handler)

values_handler = logging.StreamHandler(sys.stderr)
values_handler.setFormatter(ValuesFormatter())
logger.addHandler(values_handler)

action, amount, item = "traded", 42, "shrubs"
logger.info(t"User {action}: {amount:.2f} {item}")

# Outputs to sys.stdout:
# User traded: 42.00 shrubs

# At the same time, outputs to sys.stderr:
# {"action": "traded", "amount": 42, "item": "shrubs"}

이 방법은 구조화된 로깅에 사용자 정의 메시지를 사용하는 방법에 비해 몇 가지 장점이 있습니다:

  • 개발자는 t-문자열을 사용자 정의 클래스로 감싸지 않고도 직접 로그로 남길 수 있습니다.
  • 사람이 읽을 수 있는 출력과 구조화된 출력을 서로 다른 로그 스트림으로 보낼 수 있습니다. 이는 구조화된 데이터를 사람이 읽을 수 있는 데이터와 별도로 처리하는 로그 집계 시스템에 유용합니다.

Note

예제 코드

logging.py와 test_logging.py를 참고하십시오.

예제: HTML 템플릿 처리

이 PEP에는 짧은 HTML 템플릿 처리 예제가 여러 개 포함되어 있습니다. 사실 Motivation 섹션(및 이 PEP의 다른 몇 곳)에서 언급한 “가상의” html() 함수는 실제로 존재하며, pep750-examples 저장소에서 사용할 수 있습니다. 템플릿 문자열로 복잡한 문법을 파싱하려고 생각하고 계신다면 이 함수가 유용하기를 바랍니다.

하위 호환성

f-문자열과 마찬가지로, 템플릿 문자열을 사용하면 이전 버전과 문법적으로 하위 호환되지 않습니다.

보안 영향

보간과 관련하여 템플릿 문자열을 다룰 때의 보안 영향은 다음과 같습니다:

  1. 스코프 조회는 f-문자열과 같습니다(렉시컬 스코프). 이 모델은 실제로 잘 작동하는 것으로 입증되었습니다.
  2. Template 인스턴스를 처리하는 코드는 모든 보간이 안전한 방식으로 처리되도록 보장할 수 있으며, 여기에는 보간이 나타나는 문맥을 고려하는 것도 포함됩니다.

교육 방법

템플릿 문자열의 대상 독자는 여러 부류입니다:

  • 템플릿 문자열과 처리 함수를 사용하는 개발자
  • 템플릿 처리 코드 작성자
  • 템플릿 문자열로 흥미로운 기능을 구축하는 프레임워크 작성자

개발자에게 가르치는 일은 간단할 것으로 기대합니다. 언뜻 보기에 템플릿 문자열은 f-문자열과 똑같아 보입니다. 문법이 익숙하며 스코프 규칙도 동일하게 유지됩니다.

개발자가 가장 먼저 알아야 할 점은 템플릿 문자열 리터럴이 문자열로 평가되지 않는다는 것입니다. 대신 새로운 타입인 Template으로 평가됩니다. 이 타입은 템플릿 처리 코드에서 사용하도록 만든 단순한 타입입니다. 개발자는 처리 함수를 호출하고 나서야 원하는 결과를 얻습니다. 그 결과는 보통 문자열이지만, 처리 코드는 물론 임의의 타입을 반환할 수도 있습니다.

개발자는 템플릿 문자열이 f-문자열이나 str.format() 같은 다른 문자열 포매팅 방법과 어떤 관계인지도 이해하고 싶어 할 것입니다. 개발자는 각 방법을 언제 사용할지 결정해야 합니다. 단순한 문자열만 필요하고 보안상 영향이 없다면 f-문자열이 가장 좋은 선택일 가능성이 높습니다. 형식 문자열을 사용하는 대부분의 경우, 템플릿 문자열 생성을 감싸는 함수로 대체할 수 있습니다. 형식 문자열을 사용자 입력, 파일 시스템 또는 데이터베이스에서 가져오는 경우, 원한다면 이를 Template 인스턴스로 변환하는 코드를 작성할 수 있습니다.

개발자는 t-문자열이 거의 항상 처리 함수와 함께 사용된다는 것을 알게 되므로, Template 타입의 세부 사항까지 반드시 이해할 필요는 없습니다. 디스크립터나 데코레이터와 마찬가지로, t-문자열 처리 함수를 작성하는 개발자보다 t-문자열을 사용하는 개발자가 훨씬 많을 것으로 예상합니다.

시간이 지나면 소수의 더 숙련된 개발자는 자신만의 템플릿 처리 코드를 작성하기를 원할 것입니다. 처리 코드를 작성하려면 형식 문법의 관점에서 생각해야 하는 경우가 많습니다. 개발자는 Template 인스턴스의 strings와 interpolation 속성을 다루는 방법과 보간을 문맥에 따라 처리하는 방법을 배워야 합니다. 더 정교한 문법이라면 추상 구문 트리(AST) 같은 중간 표현으로 파싱해야 할 가능성이 높습니다. 훌륭한 템플릿 처리 코드는 적절한 경우 형식 지정자와 변환을 처리합니다. 프로덕션 수준의 템플릿 처리 코드를 작성하는 일은 큰 작업이 될 수 있습니다. HTML 템플릿을 지원하는 코드가 그 예입니다.

템플릿 문자열은 프레임워크 작성자의 도구 상자에 강력한 새 도구를 더해 줄 것으로 기대합니다. 템플릿 문자열의 기능은 템플릿 엔진 같은 기존 도구와 겹치지만, t-문자열은 그 로직을 언어 자체로 옮깁니다. 문자열 처리 작업에 Python의 모든 능력과 범용성을 활용할 수 있게 되면 프레임워크 작성자에게 새로운 가능성이 열립니다.

왜 또 다른 템플릿 방식이 필요합니까?

Python 생태계에는 이미 Jinja처럼 널리 채택된 성숙한 템플릿 언어가 있습니다. 새로운 템플릿 시스템을 만들 수 있도록 지원하는 이유는 무엇입니까?

템플릿이 개발자가 만드는 소프트웨어의 일부라기보다 디자이너의 커스터마이징이나 CMS처럼 사용자가 만든 콘텐츠에 가까운 경우에는 여전히 Jinja 같은 프로젝트가 필요합니다.

프런트엔드 개발의 흐름은 템플릿을 소프트웨어의 일부로, 그리고 개발자가 작성하는 것으로 다뤄 왔습니다. 이런 개발자는 최신 언어 기능과 좋은 도구 지원을 원합니다. PEP 750은 정적이지 않은 부분이 Python인 DSL을 구상합니다. 즉, 스코프 규칙, 타입 지정, 표현식 문법 등이 Python과 같습니다.

템플릿 처리에서 흔히 볼 수 있는 패턴

구조적 패턴 매칭

구조적 패턴 매칭으로 Template을 순회하는 방식이 많은 템플릿 함수 구현에서 모범 사례가 될 것으로 예상합니다:

from string.templatelib import Template, Interpolation

def process(template: Template) -> Any:
    for item in template:
        match item:
            case str() as s:
                ... # handle each string part
            case Interpolation() as interpolation:
                ... # handle each interpolation

처리 코드에서는 Interpolation 타입의 속성에 대해 하위 매칭을 하는 경우도 흔합니다:

match arg:
    case Interpolation(int()):
        ... # handle interpolations with integer values
    case Interpolation(value=str() as s):
        ... # handle interpolations with string values
    # etc.

메모이제이션

템플릿 함수는 템플릿의 정적 부분과 동적 부분을 모두 효율적으로 처리할 수 있습니다. Template 객체의 구조 덕분에 효과적으로 메모이제이션할 수 있습니다:

strings = template.strings  # Static string parts
values = template.values  # Dynamic interpolated values

이렇게 분리되어 있으므로 처리된 정적 부분은 캐시해 두고, 동적 부분은 필요에 따라 삽입할 수 있습니다. 템플릿 처리 코드 작성자는 정적인 strings를 캐시 키로 사용할 수 있으며, 이렇게 하면 유사한 템플릿을 반복해서 사용할 때 성능이 크게 향상됩니다.

중간 표현으로 파싱하기

템플릿을 처리하는 코드는 템플릿 문자열을 AST와 같은 중간 표현으로 파싱할 수 있습니다. 많은 템플릿 처리 라이브러리가 이 방식을 사용할 것으로 예상합니다.

예를 들어, 앞서 가정한 html() 함수(Motivation 절 참조)는 str을 반환하는 대신 같은 패키지에 정의된 HTML Element를 반환할 수 있습니다:

@dataclass(frozen=True)
class Element:
    tag: str
    attributes: Mapping[str, str | bool]
    children: Sequence[str | Element]

    def __str__(self) -> str:
        ...


def html(template: Template) -> Element:
    ...

그러면 str(element)를 호출하여 HTML을 렌더링할 수 있으며, 그 전까지는 Element를 다양한 방식으로 조작할 수 있습니다.

보간의 문맥 의존적 처리

가상의 html() 함수를 계속 예로 들면, 이 함수를 문맥에 따라 다르게 동작하도록 만들 수 있습니다. 보간은 템플릿 안에서 나타나는 위치에 따라 다르게 처리될 수 있습니다.

예를 들어, html() 함수는 여러 종류의 보간을 지원할 수 있습니다:

attributes = {"id": "main"}
attribute_value = "shrubbery"
content = "hello"
template = t"<div {attributes} data-value={attribute_value}>{content}</div>"
element = html(template)
assert str(element) == '<div id="main" data-value="shrubbery">hello</div>'

{attributes} 보간은 HTML 태그의 문맥에서 나타나고 대응하는 속성 이름이 없으므로, 속성의 딕셔너리로 취급됩니다. {attribute_value} 보간은 단순한 문자열 값으로 취급되며, 최종 문자열에 포함되기 전에 따옴표로 감싸집니다. {content} 보간은 안전하지 않을 수 있는 콘텐츠로 취급되며, 최종 문자열에 포함되기 전에 이스케이프됩니다.

중첩된 템플릿 문자열

html() 함수에서 한 걸음 더 나아가, 중첩된 템플릿 문자열을 지원할 수도 있습니다. 이렇게 하면 더 단순한 템플릿들을 조합하여 더 복잡한 HTML 구조를 만들 수 있습니다:

name = "World"
content = html(t"<p>Hello {name}</p>")
template = t"<div>{content}</div>"
element = html(template)
assert str(element) == '<div><p>Hello World</p></div>'

{content} 보간은 Element 인스턴스이므로, 최종 문자열에 포함되기 전에 이스케이프할 필요가 없습니다.

다음과 같이 깔끔하게 단순화하는 방법도 생각해 볼 수 있습니다. html() 함수에 Template 인스턴스가 전달되면, 중첩된 템플릿에 대해 자기 자신을 재귀적으로 호출하여 이를 Element로 자동 변환할 수 있습니다.

템플릿의 중첩과 합성은 템플릿 처리 코드에서 흔한 패턴이 될 것이며, 적절한 경우에는 단순한 문자열 연결보다 우선적으로 사용될 것으로 예상합니다.

지연 평가를 위한 접근 방식

f-문자열과 마찬가지로, t-문자열 리터럴의 보간은 즉시 평가됩니다. 하지만 지연 평가가 바람직한 경우도 있습니다.

어떤 보간 하나의 평가 비용이 크다면, 템플릿 문자열 리터럴 안에서 그 보간을 lambda로 명시적으로 감쌀 수 있습니다:

name = "World"
template = t"Hello {(lambda: name)}"
assert callable(template.interpolations[0].value)
assert template.interpolations[0].value() == "World"

물론 이는 템플릿 처리 코드가 호출 가능 객체인 보간 값을 예상하고 처리한다는 것을 전제로 합니다. (이터레이터, 어웨이터블 등을 함께 지원하는 것도 생각해 볼 수 있습니다.) 이는 이 PEP의 요구 사항은 아니지만, 템플릿 처리 코드에서 흔히 쓰이는 패턴입니다.

전반적으로, 커뮤니티가 템플릿 문자열에서 보간을 지연 평가하는 모범 사례를 발전시켜 나가고, 적절한 경우에는 널리 쓰이는 라이브러리들이 템플릿 처리 코드에서 호출 가능 객체나 어웨이터블 값을 지원하기를 바랍니다.

비동기 평가를 위한 접근 방식

지연 평가와 밀접하게 관련된 것이 비동기 평가입니다.

f-문자열과 마찬가지로, 보간 안에서는 await 키워드를 사용할 수 있습니다:

async def example():
    async def get_name() -> str:
        await asyncio.sleep(1)
        return "Sleepy"

    template = t"Hello {await get_name()}"
    # Use the f() function from the f-string example, above
    assert f(template) == "Hello Sleepy"

더 정교한 템플릿 처리 코드는 이를 활용하여 보간에서 비동기 작업을 수행할 수 있습니다. 예를 들어, “똑똑한” 처리 함수는 보간이 어웨이터블(awaitable)일 것을 예상하고 템플릿 문자열을 처리하기 전에 이를 await할 수 있습니다:

async def example():
    async def get_name() -> str:
        await asyncio.sleep(1)
        return "Sleepy"

    template = t"Hello {get_name}"
    assert await async_f(template) == "Hello Sleepy"

이는 async_f()의 템플릿 처리 코드가 비동기적이며 보간의 값을 await할 수 있다고 가정합니다.

Note

예제 코드

afstring.py 및 test_afstring.py를 참조하십시오.

템플릿 재사용 방법

개발자가 서로 다른 값으로 템플릿 문자열을 여러 번 재사용하고 싶다면, Template 인스턴스를 반환하는 함수를 작성할 수 있습니다:

def reusable(name: str, question: str) -> Template:
    return t"Hello {name}, {question}?"

template = reusable("friend", "how are you")
template = reusable("King Arthur", "what is your quest")

물론 이는 f-문자열을 재사용하는 방식과 다르지 않습니다.

형식 문자열과의 관계

오래된 str.format() 메서드는 나중에 값을 서식화하는 데 사용할 수 있는 형식 문자열을 받습니다:

alas_fmt = "We're all out of {cheese}."
assert alas_fmt.format(cheese="Red Leicester") == "We're all out of Red Leicester."

조금 너그럽게 보면, 형식 문자열을 일종의 함수 정의로 생각할 수 있습니다. str.format()의 호출은 일종의 함수 호출로 볼 수 있습니다. t-문자열에서 이에 해당하는 것은 Template 인스턴스를 반환하는 표준 Python 함수를 정의하기만 하면 됩니다:

def make_template(*, cheese: str) -> Template:
    return t"We're all out of {cheese}."

template = make_template(cheese="Red Leicester")
# Using the f() function from the f-string example, above
assert f(template) == "We're all out of Red Leicester."

make_template() 함수 자체는 형식 문자열에 대응하는 것으로 생각할 수 있습니다. make_template()의 호출은 str.format()의 호출에 대응합니다.

물론 파일 시스템이나 데이터베이스 같은 외부 소스에서 형식 문자열을 불러오는 경우가 흔합니다. 다행히 Template과 Interpolation은 단순한 Python 타입이므로, 구식 형식 문자열을 받아 그에 상응하는 Template 인스턴스를 반환하는 함수를 작성할 수 있습니다:

def from_format(fmt: str, /, *args: object, **kwargs: object) -> Template:
     """Parse `fmt` and return a `Template` instance."""
     ...

 # Load this from a file, database, etc.
 fmt = "We're all out of {cheese}."
 template = from_format(fmt, cheese="Red Leicester")
 # Using the f() function from the f-string example, above
 assert f(template) == "We're all out of Red Leicester."

이것은 개발자가 이전에 형식 문자열을 사용했을 법한 곳에서 템플릿 문자열을 사용할 수 있게 해 주는 강력한 패턴입니다. 형식 문자열의 전체 문법을 지원하는 from_format()의 전체 구현은 예제 저장소에서 확인할 수 있습니다.

Note

예제 코드

format.py 및 test_format.py를 참조하십시오.

참조 구현

PEP 750의 CPython 구현은 여기에서 사용할 수 있습니다.

참조 구현을 기반으로 만들어진 예제와 테스트의 공개 저장소도 있습니다. 템플릿 문자열을 직접 사용해 보고 싶다면, 이 저장소에서 시작하는 것이 좋습니다.

기각된 아이디어

이 PEP는 여러 차례 중요한 개정을 거쳤습니다. 또한 PEP 501의 개정판들과 Discourse 토론 양쪽에서 꽤 많은 흥미로운 아이디어가 검토되었습니다.

검토되었으나 거부된 아이디어 중 가장 중요한 것들을 문서화하고자 합니다.

임의의 문자열 리터럴 접두사

JavaScript의 태그드 템플릿 리터럴에서 영감을 받아, 이 PEP의 이전 버전에서는 리터럴 문자열 앞에 임의의 “태그” 접두사를 붙일 수 있었습니다:

my_tag'Hello {name}'

이 접두사는 “태그 함수”라고 불리는 특별한 호출 가능 객체였습니다. 태그 함수는 템플릿 문자열의 각 부분을 인자 목록으로 전달받았습니다. 그런 다음 태그 함수는 문자열을 처리하고 임의의 값을 반환할 수 있었습니다:

def my_tag(*args: str | Interpolation) -> Any:
    ...

이 접근 방식은 여러 가지 이유로 거부되었습니다:

  • 완전히 일반적인 형태로 구현하기에는 너무 복잡하다고 판단되었습니다. JavaScript는 템플릿 문자열 앞에 임의의 표현식이 올 수 있도록 허용하는데, 이는 Python에서 구현하기에 상당히 어려운 과제입니다.
  • 향후 새로운 문자열 접두사를 도입할 수 없게 만들었습니다.
  • 네임스페이스를 불필요하게 오염시키는 것으로 보였습니다.

단일 t 접두사를 사용한 것은 더 단순하고 더 Python다운 접근 방식이며, f-문자열을 일반화한다는 템플릿 문자열의 역할에 더 부합하기 때문입니다.

보간의 지연 평가

이 PEP의 초기 버전에서는 보간을 지연 평가해야 한다고 제안했습니다. 모든 보간은 암묵적인 람다로 “감싸졌습니다”. 보간에는 즉시 평가되는 value 속성 대신, 보간의 값을 결정하는 getvalue() 메서드가 있었습니다:

class Interpolation:
    ...
    _value: Callable[[], object]

    def getvalue(self) -> object:
        return self._value()

이 방식은 여러 가지 이유로 거부되었습니다:

  • 템플릿 문자열 사용 사례의 압도적인 다수는 자연스럽게 즉시 평가를 필요로 합니다.
  • 지연 평가는 f-문자열의 동작에서 크게 벗어나는 것입니다.
  • 암묵적인 람다 감싸기는 타입 힌트와 정적 분석에 어려움을 초래합니다.

무엇보다도, 지연 평가가 필요한 많은 경우에 암묵적인 람다 감싸기를 대신할 수 있는 (불완전하더라도) 실행 가능한 대안이 존재합니다. 자세한 내용은 위의 Approaches to Lazy Evaluation 절을 참조하십시오.

지연 평가는 이 PEP에서는 거부되었지만, 커뮤니티에서 이 아이디어를 계속 탐구하기를 바랍니다.

Template과 Interpolation을 프로토콜로 만들기

이 PEP의 초기 버전에서는 Template 및 Interpolation 타입을 클래스가 아닌 런타임에 검사 가능한 프로토콜로 만들 것을 제안했습니다.

결국 클래스를 사용하는 편이 더 간단하다고 판단했습니다.

Template과 Interpolation의 __eq__와 __hash__ 재정의

이 PEP의 이전 버전들에서는 Template과 Interpolation 타입이 __eq__와 __hash__를 자체적으로 구현해야 한다고 제안했습니다.

Templates는 strings와 interpolations가 같으면 같은 것으로 간주되었고, Interpolations는 value, expression, conversion, format_spec이 모두 같으면 같은 것으로 간주되었습니다. 보간의 해싱은 튜플 해싱과 비슷했습니다. Interpolation은 value가 해시 가능한 경우에, 그리고 그 경우에만 해시 가능했습니다.

이렇게 정의된 Template.__hash__는 템플릿 처리 코드에서 캐시 키로 유용하지 않았기 때문에 이 방식은 거부되었습니다. 개발자에게 혼란을 줄 수 있다는 점도 우려했습니다.

__eq__와 __hash__의 이러한 구현을 제거하면 다음과 같은 단언문을 작성할 수 없게 됩니다:

name = "World"
assert t"Hello " + t"{name}" == t"Hello {name}"

Template 인스턴스는 후속 코드에서 빠르게 처리되도록 의도되었으므로, 이러한 단언문의 유용성은 제한적이라고 판단했습니다.

추가 Decoded 타입

이 PEP의 초기 버전에서는 템플릿 문자열의 “정적 문자열” 부분을 나타내기 위해 Decoded라는 추가 타입을 제안했습니다. 이 타입은 str에서 파생되었으며, 문자열의 원본 텍스트를 제공하는 raw 속성 하나를 추가로 가지고 있었습니다. 저희는 이 제안을 거부하고, 일반 str을 사용하면서 r과 t 접두사를 함께 쓸 수 있게 하는 더 단순한 접근 방식을 택했습니다.

Template과 Interpolation의 최종 위치

이 PEP의 이전 버전들에서는 Template과 Interpolation 타입을 types, collections, collections.abc, 심지어 새로운 최상위 모듈인 templatelib에 두는 방안을 제안했습니다. 최종적으로 이들을 string.templatelib에 두기로 결정했습니다.

원본 템플릿 리터럴의 완전한 재구성 지원

이 PEP의 이전 버전들에서는 Template 인스턴스로부터 원본 템플릿 문자열의 텍스트를 완전히 재구성할 수 있게 하려고 시도했습니다. 이는 지나치게 복잡하다는 이유로 거부되었습니다. 템플릿 리터럴 소스와 그 기반이 되는 AST 사이의 매핑은 일대일이 아니며, 원본 소스 텍스트로 왕복 변환하는 데에는 몇 가지 제약이 있습니다.

첫째, Interpolation.format_spec은 제공되지 않으면 기본값이 ""입니다:

value = 42
template1 = t"{value}"
template2 = t"{value:}"
assert template1.interpolations[0].format_spec == ""
assert template2.interpolations[0].format_spec == ""

다음으로, 디버그 지정자인 =는 특수한 경우로 취급되어 AST가 생성되기 전에 처리됩니다. 따라서 t"{value=}"와 t"value={value!r}"를 구별할 수 없습니다:

value = 42
template1 = t"{value=}"
template2 = t"value={value!r}"
assert template1.strings[0] == "value="
assert template1.interpolations[0].expression == "value"
assert template1.interpolations[0].conversion == "r"
assert template2.strings[0] == "value="
assert template2.interpolations[0].expression == "value"
assert template2.interpolations[0].conversion == "r"

마지막으로, f-문자열의 형식 지정자는 임의의 중첩을 허용합니다. 이 PEP와 참조 구현에서는 지정자를 즉시 평가하여 Interpolation의 format_spec을 설정하므로, 원래의 표현식은 사라집니다. 예를 들면 다음과 같습니다:

value = 42
precision = 2
template1 = t"{value:.2f}"
template2 = t"{value:.{precision}f}"
assert template1.interpolations[0].format_spec == ".2f"
assert template2.interpolations[0].format_spec == ".2f"

저희는 이러한 제약이 실제로 큰 문제가 되지는 않을 것으로 예상합니다. 원본 템플릿 문자열 리터럴을 얻어야 하는 개발자는 언제든지 inspect.getsource() 또는 이와 유사한 도구를 사용할 수 있습니다.

템플릿 연결 금지

이 PEP의 이전 버전들에서는 Template 인스턴스가 연결을 지원하지 않아야 한다고 제안했습니다. 이 제안은 거부되었고, 대신 여러 Template 인스턴스의 연결을 허용하기로 했습니다.

연결의 한 가지 형태 또는 모든 형태를 거부하는 쪽을 지지하는 타당한 인자(논거)들이 있습니다. 즉, 연결을 거부하면 잠재적인 버그의 한 부류를 차단할 수 있다는 것인데, 특히 템플릿 문자열이 연결이 항상 같은 의미를 갖지는 않는(또는 아무 의미도 갖지 않는) 복잡한 문법을 담는 경우가 많을 것이라는 관점에서 보면 더욱 그렇습니다.

게다가 이 PEP의 가장 초기 버전들에서는 JavaScript의 태그된 템플릿 리터럴에 더 가까운 문법을 제안했는데, 이 문법에서는 임의의 호출 가능 객체를 문자열 리터럴의 접두사로 사용할 수 있었습니다. 그 호출 가능 객체가 연결을 지원하는 타입을 반환한다는 보장은 없었습니다.

결국 저희는 새로운 문자열 타입이 연결을 지원하지 않을 때 개발자가 느낄 당혹감이, 연결을 지원함으로써 생길 수 있는 이론적인 해악보다 클 가능성이 높다고 판단했습니다.

이 PEP는 두 Templates의 연결은 지원하지만, Template과 str의 연결은 지원하지 않습니다. 이는 str을 정적 문자열로 취급해야 할지 보간으로 취급해야 할지가 모호하기 때문입니다. 개발자는 위에서 설명한 것처럼 str을 다른 Template과 연결하기 전에 Template 인스턴스로 감싸야 합니다.

저희는 템플릿 문자열을 사용하는 코드가 연결보다는 중첩과 조합을 통해 더 큰 템플릿을 구성하는 경우가 더 많을 것으로 예상합니다.

임의의 변환 값

Python은 변환 타입 값으로 r, s, a만 허용합니다. 다른 값을 지정하려고 하면 SyntaxError가 발생합니다.

이론적으로는 템플릿 함수가 다른 변환 유형을 처리하도록 선택할 수도 있습니다. 하지만 이 PEP는 PEP 701을 충실히 따릅니다. 허용되는 값에 대한 변경은 별도의 PEP에서 다루어야 합니다.

Interpolation에서 conversion 제거

이 PEP의 초안을 작성하는 동안, Interpolation에서 conversion 속성을 제거하고 Interpolation.value가 설정되기 전에 변환을 즉시 수행하도록 명시하는 방안을 검토했습니다.

이는 템플릿 처리 코드를 작성하는 작업을 단순화하기 위한 것이었습니다. conversion 속성은 확장성이 제한적입니다(타입이 Literal["r", "s", "a"] | None으로 지정됩니다). 사용자 정의 형식 지정자로 더 잘 달성할 수 없는 상당한 가치나 유연성을 이 속성이 템플릿 문자열에 더해 주는지는 분명하지 않습니다. 형식 지정자와 달리, 파이썬의 format() 내장 함수에 해당하는 것이 없습니다. (대신 Examples 섹션에 convert()의 예시 구현을 포함합니다.)

결국 f-문자열과의 호환성을 유지하고 향후 확장성을 확보하기 위해 Interpolation 타입에 conversion 속성을 유지하기로 결정했습니다.

대체 보간 기호

이 PEP의 초기 단계에서는 템플릿 문자열의 보간에 대체 기호를 허용하는 방안을 검토했습니다. 예를 들어, i18n이나 기타 목적에 유용할 수 있다는 생각에서 ${name}을 {name}의 대안으로 허용하는 방안을 검토했습니다. 자세한 내용은 Discourse 스레드를 참조하십시오.

이 방안은 t-문자열 문법을 f-문자열 문법에 최대한 가깝게 유지하는 쪽을 택하면서 기각되었습니다.

Template의 대체 구조

이 PEP를 개발하는 동안 Template 타입에 대해 여러 대체 구조를 검토했습니다. 그중 다수는 문자열과 보간을 모두 담는 단일 args 튜플에 초점을 맞추었습니다. 다음과 같은 변형안이 있었습니다.

  • args는 tuple[str | Interpolation, ...]` 타입의 튜플이었으며, 첫 번째와 마지막 항목이 문자열이고 문자열과 보간이 항상 번갈아 나온다는 약속이 있었습니다. 이는 args가 항상 비어 있지 않으며, 이웃한 보간 사이에 빈 문자열이 삽입된다는 것을 의미했습니다. 이 방안은 번갈아 나타나는 구조를 타입 시스템으로 표현할 수 없고, 저희가 제공하고자 하는 보장도 아니었기 때문에 기각되었습니다.
  • args는 여전히 tuple[str | Interpolation, ...]이었지만 교차 배치를 지원하지 않았습니다. 그 결과 빈 문자열이 시퀀스에 추가되지 않았습니다. 더 이상 args[::2]로 정적 문자열을 얻을 수 없었고, 대신 문자열과 보간을 구별하려면 인스턴스 검사나 구조적 패턴 매칭을 사용해야 했습니다. 이 방안은 향후 성능 최적화의 여지가 더 적다는 이유로 기각되었습니다.
  • args의 타입은 Sequence[tuple[str, Interpolation | None]]으로 지정되었습니다. 각 정적 문자열은 이웃한 보간과 짝을 이루었습니다. 마지막 문자열 부분에는 대응하는 보간이 없었습니다. 이 방안은 지나치게 복잡하다는 이유로 기각되었습니다.

템플릿의 “종류”를 기술하는 메커니즘

t-문자열이 널리 쓰이게 된다면, 템플릿 문자열에 담긴 콘텐츠의 “종류”(“sql”, “html”, “css” 등)를 나타내는 방법이 있으면 유용할 수 있습니다. 이를 통해 린터, 포매터, 타입 검사기, IDE 같은 도구에서 강력한 새 기능을 구현할 수 있습니다. (예를 들어 black이 t-문자열 안의 HTML을 포매팅하거나, mypy가 특정 속성이 HTML 태그에 유효한지 검사하는 모습을 상상해 보십시오.) 흥미로운 가능성이지만, 이 PEP는 구체적인 메커니즘을 제안하지 않습니다. 시간이 지나면서 커뮤니티가 이 목적을 위한 관례를 만들어 가기를 바랍니다.

바이너리 템플릿 문자열

t-문자열과 바이트의 조합(tb)은 이 PEP의 범위를 벗어나는 것으로 간주합니다. 하지만 f-문자열과 달리, t-문자열과 바이트를 조합할 수 없는 근본적인 이유는 없습니다. 향후 PEP에서 이를 지원하는 방안을 검토할 수 있습니다.

감사의 글

템플릿 문자열로 이어진 아이디어를 발전시키는 과정에서 기여해 주신 Ryan Morshead에게 감사드립니다. 수년 전에 비슷한 아이디어를 다룬 Dropbox의 pyxl에도 특별히 감사를 표합니다. Andrea Giammarchi는 이 PEP의 초기 초안에 대해 사려 깊은 피드백을 주었습니다. 마지막으로, tagged 라이브러리에 대한 선구적인 작업을 해 주신 Joachim Viide에게 감사드립니다. Tagged는 템플릿 문자열의 전신이었을 뿐만 아니라, GitHub 이슈 댓글 하나로 이 모든 작업이 시작된 곳이기도 합니다!