Tutorial#
Imagine: A fictional company has a standard way to do greetings. For this, it created a tag function to properly format greetings according to its standards.
Simple Tag Function#
We start with a tag function greet
that’s used as a prefix:
def greet(*args):
"""Uppercase and add exclamation."""
salutation = args[0].upper()
return f"{salutation}!"
If it looks like the f-
in f-strings – correct! You can then use this tag
function as a “tag” on a string:
>>> print(greet"Hello")
HELLO!
In the greet
function – a tag function – we see the first step into
tag strings. You’re given an *args
sequence for all the parts in the
string being tagged. We see how this PEP tokenizes/processes the string
being tagged, into datastructures to be easily handled.
We then see a usage – a tagged string in main
with greet"Hello"
. This
“tags” the Hello
string with the function greet
.
Interpolation#
That example showed the basics but had no dynamicism in it. f-strings make it easy to insert variables and expressions with extra instructions. We call these interpolations. Let’s see a super-simple example:
def greet(*args):
"""Handle an interpolation."""
salutation = args[0].strip()
# Second arg is an "interpolation" tuple.
getvalue = args[1][0]
recipient = getvalue().upper()
return f"{salutation} {recipient}!"
The second argument is the {name}
part, represented as a tuple. The
tuple’s first argument is a callable that evaluates in the scope where the
tag string happened. Calling it yields the value, thus by convention we call
this getvalue
.
This time, we’ll tag a string that inserts a variable:
>>> name = "World"
>>> print(greet"Hello {name}")
Hello WORLD!
Flexible Args#
Our greeting now expects a string followed by a single interpolation. But f-strings can have all kinds of things mixed in, even nested f-strings. Let’s teach our greeting to handle an arbitrary list of strings and interpolations.
In fact, let’s start adopting the jargon used in this proposal:
Decodeds are segments that are static strings
Interpolations are the structure representing an interpolation
The args are thus an arbitrary sequence of decodeds and interpolations, intermixed
Here’s the code to generalize args:
def greet(*args):
"""Handle arbitrary length of args."""
result = []
for arg in args:
match arg:
case str(): # Will need a string-like test
result.append(arg)
case getvalue, _, _, _: # This is an interpolation
result.append(getvalue().upper())
return f"{''.join(result)}!"
It uses Python 3.10 structural pattern matching to analyze each segment and determine “decodeds” and “interpolations”.
>>> print(greet"Hello {name} nice to meet you") # name is still World
Hello WORLD nice to meet you!
Interpolations#
We just said interpolations were represented by a data structure. Let’s look at them more carefully and see what they have to offer, while adding some typing.
An interpolation is a tuple with this shape:
class Interpolation(Protocol):
def __len__(self):
...
def __getitem__(self, index: int):
...
def getvalue(self) -> Callable[[], Any]:
...
expr: str
conv: Literal['a', 'r', 's'] | None
format_spec: str | None
It will likely be defined in the typing
module. Once imported, you can use it as a type hint for your tag string’s arguments:
def greet(*args: Decoded | Interpolation) -> str:
"""More about the interpolation."""
result = []
for arg in args:
match arg:
case str():
result.append(arg)
case getvalue, raw, conversion, formatspec:
gv = f"gv: {getvalue()}"
r = f"r: {raw}"
c = f"c: {conversion}"
f = f"f: {formatspec}"
result.append(", ".join([gv, r, c, f]))
return f"{''.join(result)}!"
Let’s add some typing information to our greet function.
>>> print(greet"Hello {name!r:s}") # name is still World
Hello gv: World, r: name, c: r, f: s!
Wrapup#
That’s a quick walkthrough tag strings. For a deeper dive, see the HTML templating tutorial.