Blog

f-strings en Python: la guía completa

La mayoría de quienes programan en Python usa f-strings todos los días y conoce como una quinta parte de lo que hacen.

No es una crítica. Aprendes f"{name}" de un ejemplo, funciona, y nunca hay una razón para ir más lejos, hasta el día en que necesitas un número redondeado a dos decimales dentro de una tabla que quede alineada, y descubres que los dos puntos hacen algo que nunca aprendiste.

Esta es la otra cuatro quintas partes. Qué hace realmente la f, todo el lenguaje de las especificaciones de formato, cómo hacer que tus propias clases funcionen con él, y los tres trabajos para los que una f-string es la herramienta equivocada.

Todo lo de aquí corre en un prompt común de Python. Nada que instalar. Cada salida de abajo se ejecutó en Python 3.12.3 y está pegada de una sesión real.

La f es una instrucción para el compilador

Empecemos por la diferencia que explica todo lo demás. Corre estas dos líneas:

f"{undefined_name}"
"{undefined_name}"

La primera lanza NameError. La segunda es apenas una cadena que casualmente contiene llaves.

Una f-string no es una cadena con una función agregada. Es una cosa distinta que se parece. "{x}" se guarda como texto. f"{x}" no se guarda en ninguna parte: Python la compila a instrucciones que construyen una cadena cuando la línea corre.

Puedes ver las instrucciones:

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

Carga name, dale formato, carga el literal ' has ', carga n, dale formato, construye una cadena con las tres piezas. No hay ninguna llamada a .format() ni ninguna plantilla guardada.

De ahí se siguen tres cosas, y cada una atrapa a la gente más adelante:

  • Un error de tipeo dentro de las llaves es un SyntaxError al importar, no una sorpresa en tiempo de ejecución.

  • Los valores se leen en el momento en que la línea corre. No puedes construir una f-string ahora y llenarla después: no hay nada que llenar.

  • Es rápida, porque no hay plantilla que parsear. Lo medimos al final.

Una f-string no es una cadena. Es un programa chiquito que produce una cadena.

Entre las llaves va cualquier cosa que sea una expresión

Las llaves aceptan una expresión: cualquier cosa que produzca un valor. No aceptan una instrucción: cualquier cosa que haga algo. Esa es toda la regla.

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

Una comprensión de listas entera corrió dentro de una cadena. Aquí no hay un sublenguaje de f-strings: es Python. Si lo puedes escribir en el prompt y te devuelve un valor, puede ir entre las llaves.

f"{x = 5}" no funciona, porque la asignación es una instrucción. Tampoco f"{if x: 1}".

La libertad es real, y es la forma más fácil de escribir una línea que nadie puede leer:

# 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']}"

La misma salida, y la segunda ordena una vez en lugar de dos. La guía que se sostiene: pon una consulta o una llamada corta entre las llaves, y pon la lógica en su propia línea. Si estás contando paréntesis, ya te pasaste.

Llaves, comillas y barras invertidas

Tres reglas causan casi todos los errores de f-strings que la gente realmente encuentra. Las tres fallan en voz alta, al importar, que es el buen resultado.

Las llaves literales se duplican. {{ da {, }} da }:

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

Léelo de a pares. Llave de apertura literal, sustitución real, llave de cierre literal. Sin la duplicación, f"{status: ok}" lee status como variable y todo lo que va después de los dos puntos como especificación de formato, y obtienes:

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

Alterna las comillas. Elige un estilo para la f-string y el otro para adentro de las llaves:

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

Python 3.12 levantó esta restricción, así que ahí f"{user["name"]}" es legal. En 3.11 y anteriores es un SyntaxError. Más sobre esto cerca del final.

Mantén las barras invertidas fuera de las llaves. Antes de 3.12, f"{'\n'.join(names)}" era un error. Saca la barra invertida a una variable:

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

Eso funciona en todas las versiones.

Todo lo que va después de los dos puntos es otro lenguaje

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

El .2f no es Python. No lo puedes escribir en un prompt y obtener nada.

Una llave tiene hasta tres partes, y solo la primera es Python:

{expression!conversion:format_spec}
Parte Ejemplo Qué es
expresión value Python
conversión !r uno de s, r, a
especificación :.2f otro lenguaje

Todo lo que va después de los dos puntos es el mini lenguaje de especificación de formato, tomado de .format(), que lo tomó del formateo con %, que lo tomó de C. Esa historia explica por qué parece ruido de línea. Es viejo, y es corto porque se diseñó para escribirse mucho.

Aquí está entero. Cada parte es opcional, y el orden es fijo:

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

No hace falta memorizarlo. Hace falta saber que el orden es fijo, para poder desarmar una especificación en lugar de adivinarla:

>>> f"{1234.5678:>10,.2f}"
'  1,234.57'
Pieza Parte Significa
> alineación alinear a la derecha
10 ancho en un campo de diez caracteres
, agrupación coma entre los miles
.2 precisión dos decimales
f tipo punto fijo

Ahora cada parte por turno.

Ancho, alineación y relleno

El ancho por sí solo te da una columna:

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

Fíjate que las cadenas se alinean a la izquierda por defecto y los números a la derecha. Es deliberado —el texto se lee desde la izquierda, los números se alinean por su último dígito— y es una buena forma de llevarse una sorpresa. Declara la alineación cuando importe:

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

< izquierda, > derecha, ^ centrado. Hay un cuarto, =, solo para números: pone el relleno entre el signo y los dígitos, que es lo que quiere un libro contable:

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

Cualquier carácter suelto antes de la alineación se vuelve el relleno:

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

El orden es siempre relleno y después alineación. *^9 es «rellena con asterisco, centra, ancho nueve». ^*9 no existe.

Un malentendido que conviene aclarar: el ancho es un mínimo, nunca un máximo.

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

La columna se rompe. Si necesitas un corte duro, eso es la precisión, más abajo.

Números que una persona tiene que leer

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

Tuviste que contar dígitos para leer el primero. La , agrupa los miles y .2f fija los decimales: son funciones separadas, y se pueden usar solas.

_ hace el mismo trabajo con un guion bajo:

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

Usa , para cualquier cosa que lea una persona, y _ cuando la salida vuelva a código Python o a un archivo de configuración: Python puede leer 1_234_567 como número y no puede leer 1,234,567.

% multiplica por cien y agrega el signo:

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

Cuida lo que le pasas. Si algo más arriba ya hizo la multiplicación, obtendrás 4567.0%. Es una falla ruidosa, que es de las buenas.

Y la que todo el mundo reporta como error:

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

Está correcto. 2.675 no es exactamente 2.675 en punto flotante binario —es apenas un poquito menor— así que redondea hacia abajo. El formateo está siendo honesto sobre el valor que recibió:

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

Si necesitas que la aritmética con dinero se comporte como se comporta el dinero, usa decimal.Decimal. Las f-strings lo formatean con el mismo lenguaje de especificación, así que nada más en tu código cambia:

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

Signos y ceros

Tres opciones de signo, justo después de la alineación:

>>> 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'

La tercera es el truco silencioso. Un espacio para los positivos mantiene la columna alineada sin gritarle un + a quien lee.

Un 0 antes del ancho rellena con ceros, y lo hace por dentro del signo:

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

Esa última está mal para un número, que es exactamente por qué existe el atajo del 0. El relleno con ceros es lo que quieres para cualquier cosa que se ordene como texto:

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

Ordena eso como cadenas y sale en orden numérico. Sin el relleno, no.

Tipos de presentación

Un carácter cada uno, para enteros:

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

Agrega # y obtienes el prefijo que Python mismo escribiría, cosa que importa cuando algo va a volver a leer la salida:

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

Combinado con relleno de ceros, así es como se miran los bytes:

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

Para los flotantes, f es punto fijo, e es científico, y g elige entre los dos y quita los ceros sobrantes:

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

Usa g cuando la magnitud varíe. Usa f cuando quieras la misma cantidad de decimales en cada fila, que es lo que casi siempre quiere una tabla.

La precisión sobre una cadena significa otra cosa: recorta.

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

Combínala con un ancho que le haga juego y tienes una columna que no se puede desbordar:

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

Especificaciones construidas en tiempo de ejecución

La especificación puede contener a su vez campos de reemplazo. Python los resuelve primero, arma la especificación, y después la aplica:

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

Que es como se dimensiona una tabla según sus datos:

>>> 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

Cambia los datos y sigue entrando. Sin números mágicos en la cadena de formato, y sin una segunda pasada para arreglar la alineación.

El anidamiento solo baja un nivel. En la práctica eso nunca ha sido un problema.

= — el print de depuración que dejas de escribir

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

Escribiste el nombre una vez. Esto llegó en Python 3.8 exactamente por eso: todo el mundo estaba escribiendo dos veces cada variable que depuraba, y la mitad de las veces la etiqueta quedaba desfasada después de un renombre.

Funciona con cualquier expresión, y el texto de la izquierda es exactamente lo que escribiste, espacios incluidos:

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

Se combina con la especificación:

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

Una cosa que sorprende: el = a secas usa repr, no str.

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

Mira las comillas. Ese es el valor por defecto correcto cuando estás depurando, y lleva directo a lo siguiente.

!r — el carácter que arregla tus mensajes de error

Todo objeto de Python puede producir dos cadenas, para dos lectores distintos. str(obj) es para una persona. repr(obj) es para quien programa: sin ambigüedad, e idealmente algo que podrías pegar de vuelta en Python.

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

Cada uno de esos se distingue del resto. Sin !r, dos de ellos se imprimen como algo que parece no ser nada.

Por eso esto importa en un mensaje de error:

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

Hazlo costumbre: si el mensaje es sobre un valor que está mal, usa !r. Cuesta un carácter y elimina la ambigüedad que hace que la gente vuelva a correr una falla solo para averiguar cuál era el valor.

También existe !a, que es repr con lo no ASCII escapado:

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

Lo vas a querer como una vez al año, cuando algo más abajo no puede con caracteres no ASCII y necesitas ver cuál está causando el problema. !s también existe y es el valor por defecto, así que nunca necesitas escribirlo.

__format__ — la capa que explica todo lo de arriba

Aquí está la maquinaria debajo de cada f-string que hayas escrito. Cuando Python formatea {value:spec}, llama a:

type(value).__format__(value, spec)

Eso es todo. La especificación se pasa tal cual como una cadena, y el objeto decide qué hacer con ella. Puedes verla llegar:

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

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

No hay un motor central de formato. int, float, str, Decimal y datetime traen cada uno el suyo. Por eso entienden ,.2f.

El que hereda tu clase, object.__format__, acepta una especificación vacía y devuelve str(self). Dale cualquier otra cosa y lanza:

TypeError: unsupported format string passed to Point.__format__

Arreglarlo son tres métodos, y el interesante no parsea 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}})"

Le pasa la especificación hacia abajo a los números, que ya saben qué hacer con ella. Dos líneas, y Point soporta todo el lenguaje de especificación:

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

Esa delegación es todo el truco. Si tu objeto envuelve números, pásale la especificación a los números.

Si solo vas a necesitar f"{p}", define __str__ y __repr__ y sáltate __format__: el heredado cae de vuelta a str con una especificación vacía. Las dataclasses escriben __repr__ por ti pero no __format__, así que ahí una especificación sigue fallando.

La f-string no formatea nada. Le pasa la especificación al objeto, y el objeto hace el trabajo.

Las fechas usan una especificación completamente distinta, por una buena razón

>>> 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'

Eso no se parece en nada al mini lenguaje, y la sección anterior explica por qué: datetime.__format__ lo ignora por completo y le pasa la especificación directo a strftime. Así que todo el vocabulario de strftime funciona dentro de una f-string.

Los códigos que de verdad vas a usar: %Y año, %m mes, %d día, %H hora, %M minuto, %S segundo, %B nombre del mes, %A día de la semana, %p AM/PM. Ojo que %M es minuto y %m es mes: las mayúsculas importan, y equivocarse te da una fecha equivocada con aspecto plausible.

La ganancia práctica son nombres de archivo que se ordenan solos:

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

timedelta no sobrescribe __format__, así que el lenguaje de especificación no aplica a las duraciones. Saca los números y formatea esos:

>>> 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'

Esa es la forma general siempre que un objeto no entienda una especificación.

Cadenas largas, y el error que no lanza nada

Los literales de texto adyacentes se unen al compilar, así que esta es la forma barata de cortar una línea larga:

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

Cada pieza necesita su propia f. Olvida una y no falla nada:

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

Lee esa salida. El segundo {a} salió como texto literal. Este es el error silencioso más común que hay con f-strings: sin error, solo una cadena equivocada que puede viajar muy lejos antes de que alguien lo note.

Cuida también los espacios en las uniones. Pon el espacio al final de cada línea, siempre igual, y dejarás de perderlos.

Para salida de varias líneas de verdad, usa comillas triples, y textwrap.dedent si el código está indentado dentro de una función, o cada línea sale con la indentación pegada:

import textwrap

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

Tres trabajos para los que una f-string está mal

Una f-string se evalúa donde está escrita, una vez. No queda nada. Cualquier trabajo que necesite «arma la forma ahora y llénala después» no es trabajo de una f-string, y hay tres de esos.

Una plantilla reutilizable

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

La cadena sigue siendo un dato. La puedes poner en un archivo de configuración, leerla de una base de datos, o dejar que la provea una persona. Una f-string no puede hacer nada de eso.

La misma razón descarta la traducción: las herramientas de extracción funcionan sacando cadenas literales de tu código, y una f-string no tiene literal que sacar; para cuando existe, ya está llena.

Logging

Este parece inofensivo. Córrelo y mira el 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

El nivel es WARNING, así que las dos líneas se descartaron. La de la f-string llamó a __str__ igual. La de %s no: logging guarda la plantilla y el valor por separado, y solo sustituye si algún handler quiere el mensaje de verdad.

La diferencia bruta de velocidad es menor de lo que se dice. Doscientas mil llamadas de debug suprimidas, mediana de cinco corridas en una máquina:

f-string  0.052s
%s        0.033s

Como una décima de microsegundo por llamada. El costo que importa es el otro: si el valor es una fila de base de datos o cualquier cosa con un __repr__ que recorra una estructura, estás haciendo ese trabajo cada vez y tirándolo.

La regla que se sostiene: %s en código de biblioteca y en cualquier cosa que registre dentro de un bucle, f-strings en código de aplicación donde los valores son baratos. Las dos son defendibles; ser inconsistente no lo es. Casi todos los linters tienen una regla para esto y activarla zanja la discusión. Si quieres las dos, protege el caso caro:

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

SQL

Arma una tabla y consúltala con una 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. Ahora cambia una línea:

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

Todas las filas. Imprime la consulta y es evidente:

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

El valor traía una comilla. Esa comilla cerró la cadena que la consulta estaba construyendo, y todo lo que venía después pasó a ser parte de la consulta en vez de parte del valor. Tú no pediste un OR: lo aportaron los datos.

La solución es dejar que lo haga el driver:

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

Ninguna fila, que es lo correcto: no hay ningún usuario con ese nombre tan peculiar. El ? es un marcador de posición, no una sustitución de texto. La base de datos recibe la consulta y el valor como dos cosas separadas, así que el valor nunca se parsea como SQL y no hay nada que escapar.

Los distintos drivers usan estilos distintos —? para sqlite3, %s para psycopg y MySQL, :nombre para parámetros con nombre— pero el principio es idéntico.

Una f-string cerca de SQL no está automáticamente mal. Los valores tienen que ser parámetros; los identificadores no pueden serlo, porque ninguna base de datos te deja parametrizar el nombre de una tabla o de una columna. Así que si necesitas una columna dinámica, una f-string es el mecanismo y la seguridad la tienes que poner tú:

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}"

La lista de permitidos hace el trabajo. Nunca sanees escapando comillas tú mismo: ese es un juego que terminas perdiendo.

Esto en realidad no es sobre SQL. Es el mismo error dondequiera que construyas un lenguaje a partir del texto no confiable de otro: HTML, comandos de shell, rutas, expresiones regulares. Texto pensado como dato se leyó como instrucciones.

Parametriza los valores. Usa listas de permitidos para los identificadores. Nunca construyas una consulta metiendo entrada del usuario dentro de ella.

La que no es una limitación

La gente suele agregar «entrada del usuario» a esta lista. No pertenece ahí:

f"Hello {user_name}"      # fine

user_name es un valor que se está formateando, no código que se está ejecutando. El peligro va en la otra dirección: dejar que una persona provea la plantilla. .format() sobre una plantilla no confiable puede recorrer atributos y llegar a cosas que no querías exponer. Las f-strings no se pueden usar así en absoluto, porque no hay ninguna plantilla en tiempo de ejecución que entregar. En este punto concreto, las f-strings son la herramienta más segura.

Qué cambió Python 3.12

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

En 3.12 y posteriores eso imprime value. En 3.11 y anteriores es un SyntaxError. Comprueba con python3 -VV antes de sacar conclusiones sobre tu propio código.

Las restricciones existían porque las f-strings no las parseaba el parser real de Python. El compilador sacaba la cadena, hacía manipulación de texto sobre la parte entre las llaves, y le pasaba eso al parser por separado. Las reglas se sentían arbitrarias porque eran consecuencias de la implementación, no decisiones que alguien tomó. La PEP 701 reescribió las f-strings para usar el parser normal, y las restricciones se fueron con eso.

Ahora legal, solo en 3.12 y 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
}"

Los mensajes de error también mejoraron, porque ahora el parser sabe dónde está.

Esta es una decisión de compatibilidad, no de estilo, y contiene una sola pregunta: ¿cuál es el Python más viejo en el que tiene que correr tu código? Si controlas el entorno y es 3.12 o posterior, usa la reutilización de comillas donde se lea mejor. Si tu código es una biblioteca o corre en lugares que no controlas, sigue alternando comillas: funciona en todas partes, 3.12 incluido, y no tiene nada de peor.

Velocidad, y por qué es la razón menos interesante

Mídela tú mismo. Esto tarda como un 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")

Córrelo una vez y obtendrás algo. Córrelo siete veces y obtendrás algo más honesto. Python 3.12 en una laptop común, siete repeticiones de un millón de operaciones cada una:

mejor mediana peor
f-string 0.133s 0.149s 0.230s
.format() 0.199s 0.212s 0.278s
% 0.145s 0.161s 0.176s
concatenación 0.159s 0.173s 0.190s

Aquí hay dos cosas ciertas y distintas, y vale la pena separarlas.

.format() es de manera confiable el más lento, por un margen que sobrevive al ruido. Esa parte tiene una explicación real detrás: tiene que buscar el método y parsear la plantilla en tiempo de ejecución, cada vez que la línea corre, mientras que la f-string se compiló a instrucciones que construyen la cadena directamente.

Las f-strings y % están básicamente empatadas. En esas siete corridas la f-string fue la más rápida cuatro veces y % lo fue tres. Corre el script una sola vez, como hace casi todo el mundo, y te vas convencido de un orden que cambia según cuándo lo corriste.

Ahora mira la escala de todos modos. Eso es un millón de operaciones, así que hasta la diferencia honesta contra .format() es de unos 0,06 microsegundos por cadena. Para ahorrar un milisegundo necesitas formatear unas quince mil.

Entonces: las f-strings son rápidas, y la velocidad casi nunca es la razón para usar una. Úsalas porque los valores están donde se leen. Toma la velocidad como un extra que no tuviste que pensar.

Sí importa en dos lugares reconocibles: formatear dentro de un bucle apretado, y el logging suprimido, donde el problema no es la velocidad sino el trabajo descartado. En todo lo demás el cuello de botella es la base de datos, la red, el parseo de JSON, o un algoritmo haciendo más trabajo del necesario. Si crees que el formateo va lento en tu código, perfila ese código en lugar de reescribir tus f-strings por corazonada.

La versión corta

Si recuerdas cinco cosas:

  • Una f-string se compila, no se guarda. No hay plantilla, y por eso no se puede reutilizar y por eso es rápida.

  • Todo lo que va después de los dos puntos es otro lenguaje, más viejo y con un orden fijo de palabras. ,.2f son los cinco caracteres más útiles que tiene.

  • f"{x=}" para depurar, !r para cualquier cosa que reporte un valor que salió mal.

  • La especificación se le entrega a type(value).__format__. Por eso datetime usa %Y y por eso tu propia clase puede sumarse con dos líneas.

  • Los valores en SQL son parámetros, nunca interpolación. La que te muerde es siempre la comilla que no esperabas.

El resto es ancho y alineación, y eso se puede buscar.

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.