10 个 f-string 技巧:让你的 Python 代码少写一半

f-string 从 Python 3.6 就有了,但绝大多数教程还停留在「基本用法」这一层。3.12 的一次底层重写(PEP 701),让它的能力翻了几倍。

10 个 f-string 技巧:让你的 Python 代码少写一半

你还在写 "{}".format(name) 吗?或者更老土的 "%s" % name?每次多打十几个字符,换来的代码更难读、更慢。

我接手过一个项目,日志系统全是用 % 格式化拼出来的。一个请求的日志少则 5 个变量,多则 20 个,格式字符串和变量列表隔着十万八千里,改一个变量名要数 sdf 数到眼花。

f-string 从 Python 3.6 就有了,但绝大多数教程还停留在「基本用法」这一层。3.12 的一次底层重写(PEP 701),让它的能力翻了几倍。下面这些技巧,我赌你至少有 3 个没见过。

先说结论

f-string 是 Python 字符串格式化的事实标准——更快、更短、更强。但如果你只知道 f"{name}",那你只用了它 10% 的能力。

从调试神器 = 语法,到 3.12 解锁的同引号嵌套、反斜杠、海象运算符,到动态格式控制的嵌套写法——这些技巧每一个都能让你少写几行样板代码。

下面是我日常开发中用得最多的 10 个技巧,每个都有可运行的代码,拿来就能贴进你的项目。

技巧 1:调试神器——= 语法,一行搞定变量名+值

排 bug 时,你一定写过这种代码:

1
2
x = 42
print(f"x = {x}")  # x = 42

变量名打了一遍,变量本身又打一遍,纯属重复劳动。Python 3.8 给了一个语法糖:

1
2
3
4
5
x = 42
print(f"{x=}")        # x=42
print(f"{x=!r}")      # x=42(repr 形式)
print(f"{x*2=}")      # x*2=84
print(f"{type(x)=}")  # type(x)=<class 'int'>

= 直接把表达式本身和结果一起输出。调试循环、列表推导时尤其好用,不用再纠结变量名叫什么。这是我用得最频繁的 f-string 技巧,没有之一。

技巧 2:数字格式化——千分位、百分比、进制一步到位

格式化数字的需求,f-string 的 format spec 基本全能覆盖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
revenue = 1234567890
print(f"{revenue:,}")    # 1,234,567,890(千分位)

ratio = 0.8567
print(f"{ratio:.2%}")    # 85.67%(百分比,保留2位小数)

print(f"{255:#010x}")    # 0x000000ff(十六进制,补零到8位)

page = 42
print(f"Page {page:08d}")  # Page 00000042(零填充)

不需要 locale 模块,不需要 format() 函数,不需要 "{:.2%}".format()。一个冒号搞定。

技巧 3:对齐与填充——终端表格、日志排版利器

左对齐 <、右对齐 >、居中 ^,还能自定义填充字符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 成绩表对齐
for name, score in [("Alice", 95), ("Bob", 87), ("Charlie", 100)]:
    print(f"{name:<10} | {score:>5}")
# Alice       |    95
# Bob         |    87
# Charlie     |   100

# 居中+自定义填充
print(f"{'Title':*^30}")
# ************Title*************

做 CLI 工具、日志输出、数据报表时,这几个符号比手写 ljust()/rjust()/center() 干净十倍。

技巧 4:日期格式化——告别 strftime

strftime%Y-%m-%d 可以直接写进 f-string:

1
2
3
4
5
from datetime import datetime

now = datetime.now()
print(f"{now:%Y-%m-%d %H:%M:%S}")  # 2026-06-26 08:34:28
print(f"{now:%Y年%m月%d日}")         # 2026年06月26日

少一层方法调用,可读性更好。日志、时间戳、报表里到处都能用。

技巧 5:性能——f-string 到底快多少

很多人说「f-string 比 % 快 3 倍」,这个说法过时了。我在 Python 3.13 上实测,差距没那么夸张,但 f-string 确实最快:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import timeit

N = 500000
name, age = "Alice", 30

# f-string
timeit.timeit("f'{name} is {age}'", globals=globals(), number=N)
# 0.025s

# % 格式化
timeit.timeit("'%s is %d' % (name, age)", globals=globals(), number=N)
# 0.035s(慢 1.4x)

# .format()
timeit.timeit("'{} is {}'.format(name, age)", globals=globals(), number=N)
# 0.037s(慢 1.5x)

f-string 在编译时就生成了格式化指令,运行时直接求值,不需要像 .format() 那样在运行时解析模板字符串。复杂表达式(字典访问、属性访问)时优势更大。

对于多数项目,性能差异微乎其微——一次请求多花几微秒,用户根本感知不到。但既然 f-string 又快又短又好读,没有理由不用它。选 f-string 不是因为「快」,而是因为「快且好」,性能只是锦上添花。

技巧 6:Python 3.12 革命——PEP 701 做了什么

这是 f-string 自 3.6 引入以来最大的语法改革。3.12 之前,f-string 有一堆莫名其妙的限制。3.12 一次性全解开了:

同引号嵌套——以前 f-string 内外必须用不同引号,3.12 后同引号也行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 3.12 前:外双内单,或外单内双
f"{'hello'}"   # ✅ 合法
f'{"hello"}'   # ✅ 合法

# 3.12 前:同引号嵌套 → 语法错误
# f"{"hello"}"  # ❌ SyntaxError
# f'{'hello'}'  # ❌ SyntaxError

# 3.12 后:同引号完全合法
f"{"hello"}"   # ✅ 'hello'

反斜杠支持——以前 f-string 内部不能出现反斜杠:

1
2
3
4
5
6
7
8
9
# 3.12 前:报错!
f"{'\n'.join(names)}"

# 3.12 后:完全合法
names = ["Alice", "Bob", "Charlie"]
f"{'\n'.join(names)}"
# Alice
# Bob
# Charlie

海象运算符——在 f-string 内部赋值:

1
2
f"{(result := sum([1, 2, 3]))}"  # '6'
f"{result=}"                      # result=6

多行表达式——f-string 内部的表达式可以跨行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 3.12 后:换行合法
items = [
    {"name": "Alice", "price": 99.5},
    {"name": "Bob", "price": 150},
]
f"Total: {
    sum(
        item['price'] for item in items
    )
}"  # 'Total: 249.5'

注释支持——f-string 内部可以写注释,方便复杂表达式做标注。

错误信息也大幅改善——3.12 之前 f-string 出错只告诉你「这行有问题」,3.12 之后能精确指出 f-string 内部哪个表达式出错,省下大量排查时间。

一句话:如果你还在用 3.11 或更早,升级到 3.12+ 会让你的 f-string 体验质变。

技巧 7:嵌套 f-string——动态控制格式

f-string 里可以嵌套 f-string,实现「运行时决定格式」的效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 用变量控制宽度和精度
width = 15
precision = 3
value = 3.14159265

print(f"{value:{width}.{precision}f}")
#           3.142

# 生成格式化列表
items = [1, 2, 3, 4, 5]
print(f"{' | '.join(f'{x:02d}' for x in items)}")
# 01 | 02 | 03 | 04 | 05

第一个例子用嵌套 {width}{precision} 动态控制浮点数格式。第二个例子在 join 里嵌套 f-string 生成格式化字符串。这在 3.12 之前需要混用引号才能写,现在一气呵成。

技巧 8:字典访问——3.12 前后的引号规则变化

f-string 里访问字典,引号规则在 3.12 做了改变:

1
2
3
4
5
6
7
data = {"name": "Alice", "age": 30}

# 3.12 后:外层和 key 可以用同一种引号
f"{data['name']} is {data['age']}"  # Alice is 30

# 3.12 后:同引号也行
f"{data["name"]}"  # 3.12+ 才合法

3.12 之前,如果 f-string 用双引号 f"...",里面的字典 key 必须用单引号 data['name']。一旦你从别处复制过来一段双引号 key 的字典,就得手动改引号,很烦。

3.12 之后这个限制取消了,因为解析器从正则匹配重写成了完整的语法树解析,能正确处理任意深度的嵌套引号。

实际建议:不管什么版本,内外用不同引号永远安全。如果你的项目还要兼容 3.11 及以下,养成 f"{}"key['xxx'] 的习惯。

另一个实用场景——嵌套数据结构访问:

1
2
3
4
5
6
7
8
9
# API 响应嵌套字典
response = {
    "user": {"name": "Alice", "profile": {"city": "上海"}},
    "status": "active",
}

# 直接在 f-string 里逐层访问
print(f"用户 {response['user']['name']} 来自 {response['user']['profile']['city']}")
# 用户 Alice 来自 上海

技巧 9:实战场景——日志、错误消息、数据表格

这些是我在实际项目里用得最多的模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 日志格式化
level, message = "INFO", "Request completed"
print(f"[{level:>5}] {message}")
# [ INFO] Request completed

# 错误消息(带 repr)
expected, actual = [1, 2, 3], [1, 2]
print(f"Expected {len(expected)} items, got {len(actual)}")
# Expected 3 items, got 2

# 数据展示表格
print("Name        Score  Grade")
for name, score, grade in [("Alice", 95, "A"), ("Bob", 87, "B"), ("Charlie", 100, "A+")]:
    print(f"  {name:<10} {score:>5}  {grade:>3}")
#   Alice         95    A
#   Bob           87    B
#   Charlie      100   A+

日志、错误消息、CLI 输出——这三类场景覆盖了 90% 的 f-string 实战需求。其中 = 语法做调试、对齐符号做表格、日期格式做日志,这三个组合拳基本能搞定日常工作。

再补一个容易被忽略的技巧——结合 = 语法和 format spec,调试时直接输出格式化后的值:

1
2
3
4
5
# 调试时同时看变量名和格式化结果(= 放在 : 前面)
pi = 3.14159265
count = 1234567
print(f"{pi=:.4f}")     # pi=3.1416
print(f"{count=:,}")    # count=1,234,567

注意语法:=: 前面。这种写法在排查「为什么格式化结果不对」时特别有用——一眼看出是值的问题还是格式的问题。

技巧 10:常见陷阱——别在这些地方踩坑

反斜杠陷阱(3.11 及以下):f-string 表达式部分不能直接用反斜杠。3.12 修复了,但如果你维护老代码:

1
2
3
4
5
6
# 3.11 及以下:报错
# f"{lines.split('\n')}"

# 解决方案:提前把分隔符存到变量
sep = "\n"
f"{lines.split(sep)}"

SQL 注入风险:永远不要用 f-string 拼 SQL:

1
2
3
4
5
# 危险!SQL 注入
query = f"SELECT * FROM users WHERE name = '{user_input}'"

# 安全:参数化查询
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

f-string 是格式化工具,不是安全工具。涉及 SQL、Shell 命令时,永远用参数化接口。

长表达式可读性:f-string 里塞复杂逻辑会变成天书:

1
2
3
4
5
6
# 难读
f"Total: {sum(x['price'] * x['qty'] for x in order['items'] if x['status'] == 'active')}"

# 好读:提前算好
total = sum(x['price'] * x['qty'] for x in order['items'] if x['status'] == 'active')
f"Total: {total}"

f-string 内部只放变量名和简单表达式,复杂逻辑提取到外面。这条规则没有例外。可读性永远比「少一行代码」重要。

版本兼容陷阱——用了 3.12+ 的新语法(同引号嵌套、反斜杠、多行),在 3.11 及以下会直接语法错误,不会像普通代码那样优雅降级。如果你的库要兼容旧版本,务必在 CI 里跑多个 Python 版本的测试。不确定就坚持用不同引号、避免反斜杠,这是最安全的写法。

总结

10 个技巧的优先级:先用 1-4(调试、数字、对齐、日期),日常开发立刻受益;再学 6-7(3.12 新语法、嵌套)解锁进阶能力;9-10(实战+陷阱)随时翻阅。

如果你还在 3.11 或更早,强烈建议升到 3.12+。PEP 701 对 f-string 的改造是质变,用过就回不去。

f-string 不只是「另一种格式化方式」,它是 Python 字符串处理的终极形态。掌握它,你的代码会少写一半,好读一倍。

最后给一个迁移建议:如果你项目里还有大量 %.format(),不要一次性重构。下次改到那行代码时顺手换成 f-string,几个月下来整个项目就干净了。代码迁移最大的敌人从来不是技术难度,而是「改一下整个文件」的心理阻力。

文 / varkm


关注 varkm,一起学习,一起成长