Blog

f-strings em Python: o guia completo

A maioria dos desenvolvedores Python usa f-strings todo dia e conhece cerca de um quinto do que elas fazem.

Isso não é uma crítica. Você aprende f"{name}" de um exemplo, funciona, e nunca aparece um motivo para ir além — até o dia em que você precisa de um número arredondado em duas casas dentro de uma tabela que se alinha, e descobre que os dois-pontos fazem algo que você nunca aprendeu.

Estes são os outros quatro quintos. O que o f realmente faz, a linguagem completa de especificação de formato, como fazer suas próprias classes funcionarem com ela, e os três trabalhos para os quais uma f-string é a ferramenta errada.

Tudo aqui roda num prompt Python comum. Sem instalar nada. Toda saída abaixo foi executada no Python 3.12.3 e colada de uma sessão real.

O f é uma instrução para o compilador

Comece pela diferença que explica todo o resto. Rode estas duas linhas:

f"{undefined_name}"
"{undefined_name}"

A primeira levanta NameError. A segunda é só uma string que por acaso contém chaves.

Uma f-string não é uma string com um recurso pendurado. É outra coisa, que só se parece. "{x}" é guardada como texto. f"{x}" nunca é guardada em lugar nenhum — o Python a compila em instruções que constroem uma string quando a linha roda.

Você pode ver as instruções:

import dis

def greet(name, n):
    return f"{name} has {n}"

dis.dis(greet)
LOAD_FAST                0 (name)
FORMAT_VALUE             0
LOAD_CONST               1 (' has ')
LOAD_FAST                1 (n)
FORMAT_VALUE             0
BUILD_STRING             3
RETURN_VALUE

Carrega name, formata, carrega o literal ' has ', carrega n, formata, constrói uma string a partir das três partes. Não existe chamada a .format() e não existe template guardado em lugar nenhum.

Três coisas decorrem disso, e cada uma pega as pessoas de surpresa mais tarde:

  • Um erro de digitação dentro das chaves é um SyntaxError no momento do import, não uma surpresa em tempo de execução.

  • Os valores são lidos no instante em que a linha roda. Você não pode construir uma f-string agora e preencher depois — não há nada para preencher.

  • Ela é rápida, porque não há template para analisar. Vamos medir isso no fim.

Uma f-string não é uma string. É um pequeno programa que produz uma string.

Qualquer coisa que seja uma expressão vai dentro das chaves

As chaves aceitam uma expressão — qualquer coisa que produza um valor. Elas não aceitam uma instrução — qualquer coisa que faça algo. Essa é a regra inteira.

>>> f"{2 + 2}"
'4'
>>> f"{'Ada'.upper()}"
'ADA'
>>> f"{[i * i for i in range(4)]}"
'[0, 1, 4, 9]'

Uma list comprehension inteira rodou dentro de uma string. Não existe uma sublinguagem de f-string aqui — é Python. Se você consegue digitar no prompt e receber um valor de volta, aquilo cabe nas chaves.

f"{x = 5}" não funciona, porque atribuição é uma instrução. f"{if x: 1}" também não.

A liberdade é real, e é o jeito mais fácil de escrever uma linha que ninguém consegue ler:

# don't
f"Top: {sorted(users, key=lambda u: -u['score'])[0]['name']}"

# do
top = max(users, key=lambda u: u["score"])
f"Top: {top['name']}"

Mesma saída, e a segunda ordena uma vez em vez de duas. A diretriz que se sustenta: coloque uma consulta ou uma chamada curta nas chaves, coloque lógica na própria linha. Se você está contando colchetes, foi longe demais.

Chaves, aspas e barras invertidas

Três regras causam a maior parte dos erros de f-string que as pessoas realmente encontram. As três falham alto, no momento do import, que é o bom desfecho.

Chaves literais são duplicadas. {{{, }}}:

>>> n = 7
>>> f"{{count: {n}}}"
'{count: 7}'

Leia aos pares. Chave literal de abertura, substituição de verdade, chave literal de fechamento. Sem a duplicação, f"{status: ok}"status como variável e tudo depois dos dois-pontos como especificação de formato, e você recebe:

ValueError: Invalid format specifier ' ok' for object of type 'str'

Alterne suas aspas. Escolha um estilo para a f-string e o outro dentro das chaves:

user = {"name": "Ada"}
f"{user['name']}"

O Python 3.12 removeu essa restrição, então f"{user["name"]}" é válido lá. No 3.11 e anteriores é um SyntaxError. Mais sobre isso perto do fim.

Mantenha barras invertidas fora das chaves. Antes do 3.12, f"{'\n'.join(names)}" era um erro. Puxe a barra invertida para uma variável:

newline = "\n"
f"{newline.join(names)}"

Isso funciona em toda versão.

Tudo depois dos dois-pontos é outra linguagem

>>> value = 3.14159
>>> f"{value:.2f}"
'3.14'

O .2f não é Python. Você não consegue digitar isso num prompt e receber nada de volta.

Uma chave tem até três partes, e só a primeira é Python:

{expression!conversion:format_spec}
Parte Exemplo O que é
expressão value Python
conversão !r uma de s, r, a
especificação de formato :.2f outra linguagem

Tudo depois dos dois-pontos é a minilinguagem de especificação de formato, emprestada do .format(), que a emprestou da formatação com %, que a emprestou do C. Essa história é o motivo de parecer ruído de linha. Ela é antiga, e é curta porque foi projetada para ser digitada muito.

Aqui está ela inteira. Toda parte é opcional, e a ordem é fixa:

[[fill]align][sign][z][#][0][width][grouping][.precision][type]

Você não precisa decorar isso. Você precisa saber que a ordem é fixa, para conseguir desmontar uma especificação em vez de adivinhar o que ela faz:

>>> f"{1234.5678:>10,.2f}"
'  1,234.57'
Pedaço Parte Significa
> alinhamento alinhar à direita
10 largura num campo de dez caracteres
, agrupamento vírgula entre os milhares
.2 precisão duas casas decimais
f tipo ponto fixo

Agora cada parte, uma por vez.

Largura, alinhamento e preenchimento

Só a largura já te dá uma coluna:

>>> f"|{'Ada':10}|"
'|Ada       |'
>>> f"|{7:10}|"
'|         7|'

Repare que strings vão para a esquerda por padrão e números para a direita. Isso é proposital — texto se lê da esquerda, números se alinham pelo último dígito — e é um jeito fácil de se surpreender. Diga o alinhamento quando ele importa:

>>> n = 7
>>> f"|{n:<8}|{n:>8}|{n:^8}|"
'|7       |       7|   7    |'

< à esquerda, > à direita, ^ centralizado. Existe um quarto, =, só para números — ele coloca o preenchimento entre o sinal e os dígitos, que é o que um livro-caixa quer:

>>> f"{7:=+9}"
'+       7'

Qualquer caractere único antes do alinhamento vira o preenchimento:

>>> f"{7:*^9}"
'****7****'
>>> f"{'menu':.<20}"
'menu................'

A ordem é sempre preenchimento e depois alinhamento. *^9 é “preencha com asterisco, centralize, largura nove”. ^*9 não existe.

Um mal-entendido que vale esclarecer: largura é um mínimo, nunca um máximo.

>>> f"|{'a very long name':10}|"
'|a very long name|'

A coluna quebra. Se você precisa de um corte rígido, isso é precisão, logo abaixo.

Números que pessoas precisam ler

>>> revenue = 1234567.891
>>> f"{revenue}"
'1234567.891'
>>> f"{revenue:,.2f}"
'1,234,567.89'

Você teve que contar dígitos para ler o primeiro. A , agrupa os milhares e o .2f fixa as casas decimais — recursos separados, utilizáveis sozinhos.

O _ faz o mesmo trabalho com um sublinhado:

>>> f"{1234567:_}"
'1_234_567'

Use , para qualquer coisa que uma pessoa lê, _ quando a saída volta para código Python ou um arquivo de configuração — o Python consegue ler 1_234_567 como número e não consegue ler 1,234,567.

O % multiplica por cem e acrescenta o sinal:

>>> f"{0.4567:.1%}"
'45.7%'

Preste atenção no que você passa. Se algo antes já fez a multiplicação, você vai receber 4567.0%. É uma falha barulhenta, que é o tipo bom.

E aquela que todo mundo reporta como bug:

>>> f"{2.675:.2f}"
'2.67'

Está correto. 2.675 não é exatamente 2,675 em ponto flutuante binário — é ligeiramente menor — então arredonda para baixo. A formatação está sendo honesta sobre o valor que recebeu:

>>> f"{0.1 + 0.2}"
'0.30000000000000004'

Se você precisa que aritmética de dinheiro se comporte como dinheiro se comporta, use decimal.Decimal. As f-strings formatam Decimal com a mesma linguagem de especificação, então nada mais no seu código muda:

>>> from decimal import Decimal
>>> f"{Decimal('0.1') + Decimal('0.2')}"
'0.3'

Sinais e zeros

Três opções de sinal, logo depois do alinhamento:

>>> f"{5:+d} {-5:+d}"     # + : always show a sign
'+5 -5'
>>> f"{5:-d} {-5:-d}"     # - : negatives only (the default)
'5 -5'
>>> f"{5: d} {-5: d}"     # space : a space where the plus would be
' 5 -5'

A terceira é o truque silencioso. Um espaço para positivos mantém a coluna alinhada sem gritar + para quem lê.

Um 0 antes da largura preenche com zeros, e faz isso por dentro do sinal:

>>> f"{7:03d}"
'007'
>>> f"{-7:04d}"
'-007'
>>> f"{-7:0>4}"      # plain fill, for comparison
'00-7'

A última está errada para um número, que é exatamente o motivo de o atalho 0 existir. Preenchimento com zeros é o que você quer para qualquer coisa ordenada como texto:

>>> for i in [1, 9, 10, 99]:
...     print(f"INV-{i:05d}")
INV-00001
INV-00009
INV-00010
INV-00099

Ordene isso como texto e sai em ordem numérica. Sem o preenchimento, não sai.

Tipos de apresentação

Um caractere cada, para inteiros:

>>> f"{255:b} {255:o} {255:x} {255:X}"
'11111111 377 ff FF'

Acrescente # e você recebe o prefixo que o próprio Python escreveria, o que importa quando algo vai ler a saída de volta:

>>> f"{255:#x} {255:#b} {255:#o}"
'0xff 0b11111111 0o377'

Combinado com preenchimento de zeros, é assim que você olha para bytes:

>>> data = bytes([0, 15, 255, 16])
>>> " ".join(f"{b:02x}" for b in data)
'00 0f ff 10'

Para floats, f é ponto fixo, e é científico, e g escolhe entre os dois e remove zeros à direita:

>>> f"{0.000012345:g}"
'1.2345e-05'
>>> f"{1234.5:g}"
'1234.5'

Use g quando a magnitude varia. Use f quando você quer o mesmo número de casas decimais em toda linha, que é o que uma tabela quase sempre quer.

Precisão numa string significa outra coisa — ela trunca:

>>> f"{'a very long name':.6}"
'a very'

Combine com uma largura igual e você tem uma coluna que não transborda:

>>> for name in ["Ada", "a very long name indeed"]:
...     print(f"|{name:<10.10}|")
|Ada       |
|a very lon|

Especificações construídas em tempo de execução

A própria especificação pode conter campos de substituição. O Python resolve esses primeiro, monta a especificação e então a aplica:

>>> width, places = 12, 3
>>> f"{1234.5678:{width}.{places}f}"
'    1234.568'

É assim que você dimensiona uma tabela conforme os dados dela:

>>> rows = [("Ada", 91), ("Grace", 88), ("Alan Turing", 95)]
>>> w = max(len(name) for name, _ in rows)
>>> for name, score in rows:
...     print(f"{name:<{w}}  {score:>3}")
Ada           91
Grace         88
Alan Turing   95

Mude os dados e ela continua cabendo. Sem número mágico na string de formato, sem uma segunda passada para consertar o alinhamento.

O aninhamento só vai um nível fundo. Na prática isso nunca foi um problema.

= — o print de depuração que você para de escrever

>>> user_count = 42
>>> f"{user_count=}"
'user_count=42'

Você digitou o nome uma vez. Isso chegou no Python 3.8 exatamente por esse motivo: todo mundo digitava cada variável de depuração duas vezes, e metade das vezes o rótulo ficava desatualizado depois de um rename.

Funciona com qualquer expressão, e o texto da esquerda é exatamente o que você digitou, espaços incluídos:

>>> items = [1, 2, 3]
>>> f"{len(items)=}"
'len(items)=3'
>>> x = 42
>>> f"{x = }"
'x = 42'

Combina com a especificação:

>>> price = 1234.5678
>>> f"{price=:>12,.2f}"
'price=    1,234.57'

Uma coisa que surpreende as pessoas: o = sozinho usa repr, não str.

>>> name = "Ada"
>>> f"{name=}"
"name='Ada'"

Olhe as aspas. Esse é o padrão certo quando você está depurando, e leva direto ao próximo assunto.

!r — o caractere que conserta suas mensagens de erro

Todo objeto Python consegue produzir duas strings, para dois leitores diferentes. str(obj) é para uma pessoa. repr(obj) é para quem desenvolve — sem ambiguidade, idealmente algo que você poderia colar de volta no Python.

>>> for v in ["Ada", "", " Ada ", None]:
...     print(f"{v!r}")
'Ada'
''
' Ada '
None

Cada um deles é distinguível. Sem o !r, dois deles são impressos como algo que parece nada.

E é por isso que isso importa numa mensagem de erro:

raise ValueError(f"bad status: {status}")     # 'bad status: ' — was it empty? None? a space?
raise ValueError(f"bad status: {status!r}")   # "bad status: ''" — question answered

Faça disso um hábito: se a mensagem é sobre um valor estar errado, use !r. Custa um caractere e elimina a ambiguidade que faz as pessoas reproduzirem a falha só para descobrir qual era o valor.

Existe também o !a, que é o repr com os caracteres não-ASCII escapados:

>>> f"{'café'!a}"
"'caf\\xe9'"

Você vai querer isso mais ou menos uma vez por ano, quando algo lá adiante não aguenta não-ASCII e você precisa ver qual caractere está causando. O !s também existe e é o padrão, então você nunca precisa escrevê-lo.

__format__ — a camada que explica tudo acima

Aqui está a maquinaria por baixo de toda f-string que você já escreveu. Quando o Python formata {value:spec}, ele chama:

type(value).__format__(value, spec)

É isso. A especificação é repassada como uma string comum, e o objeto decide o que fazer com ela. Você pode vê-la chegando:

class Spy:
    def __format__(self, spec):
        return f"spec={spec!r}"

>>> f"{Spy()}"
"spec=''"
>>> f"{Spy():>10,.2f}"
"spec='>10,.2f'"

Não existe um motor central de formatação. int, float, str, Decimal e datetime trazem cada um o seu. É por isso que eles entendem ,.2f.

O que a sua classe herda, object.__format__, aceita uma especificação vazia e devolve str(self). Passe qualquer outra coisa e ele levanta erro:

TypeError: unsupported format string passed to Point.__format__

Consertar isso são três métodos, e o interessante não analisa nada:

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __str__(self):
        return f"({self.x}, {self.y})"

    def __repr__(self):
        return f"Point({self.x!r}, {self.y!r})"

    def __format__(self, spec):
        if not spec:
            return str(self)
        return f"({self.x:{spec}}, {self.y:{spec}})"

Ele repassa a especificação para os números, que já sabem o que fazer com ela. Duas linhas, e Point passa a suportar a linguagem de especificação inteira:

>>> p = Point(3.14159, 2.71828)
>>> f"{p:.2f}"
'(3.14, 2.72)'
>>> f"{p:>8.1f}"
'(     3.1,      2.7)'

Essa delegação é o truque todo. Se o seu objeto embrulha números, entregue a especificação para os números.

Se você só precisa de f"{p}", defina __str__ e __repr__ e pule o __format__ — o herdado cai para str quando a especificação está vazia. Dataclasses escrevem __repr__ por você, mas não __format__, então uma especificação ainda falha lá.

A f-string não formata nada. Ela entrega a especificação ao objeto, e o objeto faz o trabalho.

Datas usam uma especificação completamente diferente, e por um bom motivo

>>> from datetime import datetime
>>> now = datetime(2026, 9, 2, 14, 5, 9)
>>> f"{now:%Y-%m-%d %H:%M}"
'2026-09-02 14:05'
>>> f"{now:%A, %d %B %Y}"
'Wednesday, 02 September 2026'

Isso não se parece nada com a minilinguagem, e a seção anterior explica por quê: datetime.__format__ a ignora por completo e repassa a especificação direto para o strftime. Então todo o vocabulário do strftime funciona dentro de uma f-string.

Os códigos que você realmente vai usar: %Y ano, %m mês, %d dia, %H hora, %M minuto, %S segundo, %B nome do mês, %A dia da semana, %p AM/PM. Repare que %M é minuto e %m é mês — maiúscula e minúscula importam, e errar isso te dá uma data errada com cara de certa.

O ganho prático são nomes de arquivo que ordenam:

>>> f"report-{now:%Y%m%d-%H%M}.csv"
'report-20260902-1405.csv'

timedelta não sobrescreve __format__, então a linguagem de especificação não vale para durações. Extraia os números e formate eles:

>>> from datetime import timedelta
>>> d = timedelta(hours=2, minutes=5)
>>> total = int(d.total_seconds())
>>> f"{total // 3600}h {total % 3600 // 60:02d}m"
'2h 05m'

Esse é o formato geral sempre que um objeto não entende uma especificação.

Strings longas, e o bug que não levanta nada

Literais de string adjacentes se juntam em tempo de compilação, então esse é o jeito barato de quebrar uma linha longa:

message = (
    f"Hello {name}, "
    f"your order {order_id} shipped "
    f"and should arrive by {eta}."
)

Cada pedaço precisa do seu próprio f. Esqueça um e nada quebra:

>>> a = 1
>>> (f"value {a} "
...  "and {a} again")
'value 1 and {a} again'

Leia essa saída. O segundo {a} passou como texto literal. Esse é o bug silencioso mais comum de f-string que existe — sem erro, só uma string errada que pode viajar bem longe antes de alguém notar.

Preste atenção também nos espaços nas junções. Coloque o espaço no fim de cada linha, sempre, e você para de perdê-los.

Para saída multilinha de verdade, use aspas triplas — e textwrap.dedent se o código está indentado dentro de uma função, ou toda linha sai com a indentação junto:

import textwrap

return textwrap.dedent(f"""
    Order {order_id}
    Customer: {name}
    Total:    {total:,.2f}
""")

Três trabalhos para os quais uma f-string está errada

Uma f-string é avaliada onde está escrita, uma vez. Não sobra nada. Qualquer trabalho que precise de “monte o formato agora, preencha depois” não é trabalho de f-string, e existem três desses.

Um template reutilizável

template = "Hello {name}, you scored {score}"
template.format(name="Ada", score=91)

A string continua sendo dado. Você pode colocá-la num arquivo de configuração, ler de um banco, ou deixar um usuário fornecer. Uma f-string não faz nada disso.

O mesmo motivo elimina tradução: ferramentas de extração funcionam puxando os literais de string do seu código, e uma f-string não tem literal para puxar — quando ela existe, já está preenchida.

Log

Esse parece inofensivo. Rode e observe o contador:

import logging

logging.basicConfig(level=logging.WARNING)
log = logging.getLogger("demo")

calls = {"n": 0}

class Record:
    def __str__(self):
        calls["n"] += 1
        return "the expensive text form"

r = Record()

log.debug(f"record: {r}")
print("after f-string debug:", calls["n"])

log.debug("record: %s", r)
print("after %s debug:      ", calls["n"])
after f-string debug: 1
after %s debug:       1

O nível é WARNING, então as duas linhas foram descartadas. A da f-string chamou __str__ mesmo assim. A do %s não chamou — o logging guarda o template e o valor separados, e só faz a substituição se algum handler realmente quiser a mensagem.

A diferença bruta de velocidade é menor do que as pessoas dizem. Duzentas mil chamadas de debug suprimidas, mediana de cinco execuções numa máquina:

f-string  0.052s
%s        0.033s

Cerca de um décimo de microssegundo por chamada. O custo que importa é o outro — se o valor for uma linha de banco ou qualquer coisa com um __repr__ que percorre uma estrutura, você está fazendo esse trabalho toda vez e jogando fora.

A regra que se sustenta: %s em código de biblioteca e em qualquer coisa que loga dentro de um laço, f-strings em código de aplicação onde os valores são baratos. As duas posições se defendem; ser inconsistente não. A maioria dos linters tem uma regra para isso, e ligá-la encerra a discussão. Se você quer os dois, proteja o caso caro:

if log.isEnabledFor(logging.DEBUG):
    log.debug(f"record: {expensive_summary(record)}")

SQL

Monte uma tabela e consulte com uma f-string:

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("create table users(name text, role text)")
con.executemany("insert into users values (?,?)",
                [("Ada", "admin"), ("Grace", "user"), ("Alan", "user")])

name = "Ada"
con.execute(f"select * from users where name = '{name}'").fetchall()
[('Ada', 'admin')]

Funciona. Agora mude uma linha:

name = "x' OR '1'='1"
con.execute(f"select * from users where name = '{name}'").fetchall()
[('Ada', 'admin'), ('Grace', 'user'), ('Alan', 'user')]

Todas as linhas. Imprima a consulta e fica óbvio:

select * from users where name = 'x' OR '1'='1'

O valor carregava uma aspa. Essa aspa fechou a string que a consulta estava montando, e tudo depois dela virou parte da consulta em vez de parte do valor. Você não pediu um OR — o dado forneceu um.

A correção é deixar o driver fazer isso:

>>> con.execute("select * from users where name = ?", (name,)).fetchall()
[]

Nenhuma linha, o que está correto — não existe usuário com esse nome peculiar. O ? é um placeholder, não substituição de string. O banco recebe a consulta e o valor como duas coisas separadas, então o valor nunca é interpretado como SQL e não há nada para escapar.

Drivers diferentes usam estilos diferentes — ? no sqlite3, %s no psycopg e no MySQL, :name para parâmetros nomeados — mas o princípio é idêntico.

Uma f-string perto de SQL não é automaticamente errada. Valores têm que ser parâmetros; identificadores não podem ser, porque nenhum banco deixa você parametrizar nome de tabela ou de coluna. Então, se você precisa de uma coluna dinâmica, a f-string é o mecanismo e a segurança é você quem fornece:

SORTABLE = {"name", "created_at", "score"}

def query(sort_by):
    if sort_by not in SORTABLE:
        raise ValueError(f"cannot sort by {sort_by!r}")
    return f"select * from users order by {sort_by}"

A lista de permissões faz o trabalho. Nunca higienize escapando aspas por conta própria — esse é um jogo que você acaba perdendo.

Isso não é realmente sobre SQL. É o mesmo erro em qualquer lugar onde você constrói uma linguagem a partir do texto não confiável de outra: HTML, comandos de shell, caminhos, expressões regulares. Texto que era para ser dado foi lido como instrução.

Parametrize valores. Use lista de permissões para identificadores. Nunca monte uma consulta formatando entrada de usuário dentro dela.

A que não é uma limitação

As pessoas costumam colocar “entrada do usuário” nessa lista. Ela não pertence a ela:

f"Hello {user_name}"      # fine

user_name é um valor sendo formatado, não código sendo executado. O perigo é o sentido contrário — deixar um usuário fornecer o template. O .format() num template não confiável consegue percorrer atributos e alcançar coisas que você não queria expor. Uma f-string não pode ser usada assim de jeito nenhum, porque não existe template em tempo de execução para entregar. Nesse ponto específico, f-strings são a ferramenta mais segura.

O que o Python 3.12 mudou

d = {"key": "value"}
f"{d["key"]}"

No 3.12 e posteriores isso imprime value. No 3.11 e anteriores é um SyntaxError. Confira com python3 -VV antes de concluir qualquer coisa sobre o seu próprio código.

As restrições existiam porque f-strings não eram analisadas pelo parser de verdade do Python. O compilador arrancava a string, fazia manipulação de texto na parte entre as chaves, e entregava aquilo ao parser separadamente. As regras pareciam arbitrárias porque eram consequências da implementação, não decisões que alguém tomou. A PEP 701 reescreveu as f-strings para usarem o parser normal, e as restrições foram junto.

Agora é válido, só no 3.12 e posteriores:

f"{d["key"]}"                 # reusing the same quote
f"{"\n".join(names)}"         # backslashes in the expression
f"{f"{f"{d["key"]}"}"}"       # nesting as deep as you like, and please don't

total = f"{
    sum(item['price'] for item in order)   # multiple lines, and comments
    :,.2f
}"

As mensagens de erro também melhoraram, porque agora o parser sabe onde está.

Essa é uma decisão de compatibilidade, não de estilo, e ela tem uma pergunta dentro: qual é o Python mais antigo em que o seu código precisa rodar? Se você controla o ambiente e ele é 3.12 ou posterior, use a reutilização de aspas onde ela lê melhor. Se o seu código é uma biblioteca ou roda em algum lugar que você não controla, continue alternando as aspas — funciona em todo lugar, inclusive no 3.12, e não tem nada de pior nisso.

Velocidade, e por que ela é o motivo menos interessante

Meça você mesmo. Isso leva cerca de um segundo:

import timeit

setup = "name='Ada'; n=7"

for label, stmt in [
    ("f-string", "f'{name} has {n}'"),
    ("format",   "'{} has {}'.format(name, n)"),
    ("percent",  "'%s has %s' % (name, n)"),
    ("concat",   "name + ' has ' + str(n)"),
]:
    t = timeit.timeit(stmt, setup=setup, number=1_000_000)
    print(f"{label:9} {t:.3f}s")

Rode uma vez e você recebe alguma coisa. Rode sete vezes e você recebe algo mais honesto. Python 3.12 num notebook comum, sete repetições de um milhão de operações cada:

melhor mediana pior
f-string 0.133s 0.149s 0.230s
.format() 0.199s 0.212s 0.278s
% 0.145s 0.161s 0.176s
concatenação 0.159s 0.173s 0.190s

Duas coisas diferentes são verdadeiras aqui, e vale separar as duas.

O .format() é consistentemente o mais lento, por uma margem que sobrevive ao ruído. Essa parte tem uma explicação de verdade: ele precisa procurar o método e analisar o template em tempo de execução, toda vez que a linha roda, enquanto a f-string foi compilada em instruções que constroem a string direto.

f-strings e % estão basicamente empatados. Nessas sete execuções, a f-string foi a mais rápida quatro vezes e o % foi o mais rápido três. Rode o script uma vez, como quase todo mundo faz, e você sai convicto de uma ordem que muda dependendo de quando você rodou.

Agora olhe a escala assim mesmo. É um milhão de operações, então até a diferença honesta contra o .format() é de cerca de 0,06 microssegundo por string. Para economizar um milissegundo você precisa formatar algo como quinze mil delas.

Ou seja: f-strings são rápidas, e a velocidade quase nunca é o motivo para usar uma. Use porque os valores ficam onde são lidos. Considere a velocidade um bônus com o qual você não precisou se preocupar.

Ela importa em dois lugares reconhecíveis — formatação dentro de um laço apertado, e log suprimido, onde o problema não é velocidade e sim trabalho descartado. Em todo o resto, o gargalo é o banco, a rede, o parse de JSON, ou um algoritmo fazendo mais trabalho do que precisa. Se você acha que formatação está lenta no seu código, use um profiler nesse código em vez de reescrever suas f-strings por palpite.

A versão curta

Se você lembrar de cinco coisas:

  • Uma f-string é compilada, não guardada. Não existe template, e é por isso que ela não pode ser reutilizada e por isso que é rápida.

  • Tudo depois dos dois-pontos é outra linguagem, mais antiga, com ordem fixa de palavras. ,.2f são os cinco caracteres mais úteis dela.

  • f"{x=}" para depurar, !r para qualquer coisa que reporte um valor que deu errado.

  • A especificação é entregue a type(value).__format__. É por isso que datetime usa %Y e por isso que a sua própria classe pode entrar no jogo com duas linhas.

  • Valores em SQL são parâmetros, nunca interpolação. A que morde é sempre a aspa que você não esperava.

O resto é largura e alinhamento, e isso você pode consultar.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.