Python 3.14 加了个 t-string,字符串插值不再是直接把变量黏进去。

库里一改,有个直接后果:把可控插值从“立即拼接成最终字符串”变成了“先当成模板保存,必须显式渲染”。这一步改变,看起来笨一点,但能把许多安全处理的机会留在代码里,不再被语法糖偷偷吞掉。
先说个最直观的场景。我们有张普通的用户表 users,想按用户名查人。常见的做法是把客户端输入直接拼进 SQL,代码类似这样:
name = request.args.get(“name”)
sql = f”SELECT * FROM users WHERE name = '{name}'”
cursor.execute(sql)
如果有人把 name 塞成 ' OR '1'='1 ,拼出来的 SQL 会变成 SELECT * FROM users WHERE name = '' OR '1'='1',数据库就把整表都吐出来了。大家都知道这叫 SQL 注入。问题不在于 f-string 是不是有 bug,问题在于“把变量直接拼到字符串里”这个做法本身。
团队里想靠培训和 code review 去杜绝这种写法,实际操作很难。新人偶尔偷懒直接拼一拼,忙的时候也容易忘。t-string 的出场就是为了解这个痛点,把“安全”从意识层面上升到语言层级。
这玩意的用法和 f-string 看着像亲戚,但本质不一样。你在字符串前加个 t,得到的不是直接的 str,而是一个模板对象。也就是说,写了 t-string 后,解释器不会把变量插进去变成终态文本,变成了可以操作的结构。变量占位、模板结构都还在。你必须调用渲染函数或由框架在合适时机渲染,才能得到最终字符串。
这个设计带来两点好处。第一,渲染前你还有机会对变量做处理:转义、绑定参数、类型检查、白名单过滤、日志标记之类的,都可以在模板变成字符串前做。第二,框架层可以把渲染动作替换成更安全的操作,列如把插值当作参数传给数据库驱动,走参数化查询,而不是把值拼进去。框架一改,团队里每个成员都不用记着去做这件事了,安全就成了默认行为。
有人会觉得不方便:用起来的确 比 f-string 多一步,好像更麻烦。对,故意让你多做一步。正是这一步,给了你处理流程的空档。未来会有框架把这一步做得透明:视图层写 t-string,底层把占位转成参数绑定,开发者不用写额外的转义代码,但不安全的拼接方式也不会悄悄溜进来。
要点澄清下:t-string 不是要取代 f-string。f-string 依然擅长快速拼接、做表达式计算,写日常逻辑、做临时输出,f-string 更爽更方便。t-string 的定位是结构化模板,强调“不要自动替你决定变量如何变成文本”。下面列了两者在几项能力上的区别(用口语列一波):
– 快速拼接:f-string 可以,t-string 不行。
– 计算表达式:f-string 支持,t-string 不支持。
– 保留模板结构:f-string 没有,t-string 有。
– 支持把插值交给安全处理:f-string 不提供,t-string 提供。
再回到 SQL 的事儿,把刚才那段代码改成 t-string 的思路是这样的:把查询模板写成模板对象,不在业务层直接渲染,而是在数据库访问层统一渲染并绑定参数。伪代码像这样:
tpl = t”SELECT * FROM users WHERE name = {name}”
# 在数据库层做渲染或参数绑定
cursor.execute(db.render_and_bind(tpl))
渲染之前,你可以对 name 做类型检查或者把它放进参数数组,让数据库驱动把它当作参数传递。这样一来,不管客户端怎么搞,最终都不会被当成 SQL 片段去执行。
还有个细节很重大:f-string 的一个危险点是你看不出来模板里哪些是静态文本、哪些是表达式。直接把表达式写进去,变量的来源和是否被处理完全模糊了。t-string 把这个界限拉直了:模板结构清楚,变量是占位,变量的来源、有没有被转义这些事情必须在渲染点处理。
举个更实际的例子。许多 Web 框架里会把 URL 参数、表单、JSON 等数据直接塞给模板。如果模板能在语言层面把“等待渲染”的信息保留下来,框架可以在渲染时走统一的安全策略。数据库、日志、命令行等不同的消费方,都可以有不同的渲染策略。这比全靠每个开发者记规则强多了。
代码层面还有一个影响是测试和审计。模板作为对象存在,比单纯的一串字符串更容易静态分析。安全工具可以扫描模板里的占位位置,判断哪些变量来自不可信源,在哪些位置应该进行额外处理。f-string 把这些信息都揉成一团,静态分析难度更大。
说到局限,t-string 不能替你做所有事。它不会自动把表达式算出来,也不会替你决定哪种参数化方式最适合。它只是把“渲染”这件事交给你或框架,让处理更明确。日常打印、快速拼接、在 REPL 里临时表达计算,还是 f-string 最省事。
有趣的地方是,这次改变不是为了炫技。语言层面上增加这个能力,是在补上长期以来一个安全短板:当变量进了字符串并被外部系统使用,谁来负责?以前这份责任常常落在不够注意的代码上。目前语言给出一种更严谨的写法,迫使责任在渲染点明确。
下面给出一个示例流程,展示 t-string 从定义到渲染的样子:
tpl = t”UPDATE accounts SET balance = {amount} WHERE id = {user_id}”
# 在数据库适配层把模板拆成 SQL 模板 + 参数列表
sql_template, params = db.prepare(tpl)
cursor.execute(sql_template, params)
这里的 prepare 会把模板里的变量转成驱动可接受的参数,不会把数值直接黏进 SQL 字符串里。实际中,框架会把这套流程封装好,业务代码只写模板和变量,数据库层负责安全处理。
写这事儿的时候,我也曾觉得“又来一个语法”,好像是为难手快的人。深入看了设计初衷和几个典型用例后,就清楚这是有针对性的权衡。标准库把模板化的能力放在语法层,目的并不是限制表达力,而是明确边界,让关键的安全点可控。
最后给出一个渲染的直观例子(不做总结,仅示意):
tpl = t”SELECT * FROM users WHERE name = {name}”
rendered = tpl.render(escape=str_safe) # 渲染时显式指定处理方法
cursor.execute(rendered)
