Photo By Fancycrave introduced a new string formatting mechanism known as or more commonly as . F-strings provide a concise and convenient way to embed python expressions inside string literals for formatting: PEP 498 Literal String Interpolation F-strings (because of the leading f character preceding the string literal) We can execute functions inside f-strings: F-strings are fast! Much faster than and — the two most commonly used string formatting mechanisms: %-formatting str.format() Why are f-strings so fast and how do they actually work? provides a clue: PEP 498 F-strings provide a way to embed expressions inside string literals, using a minimal syntax. It should be noted that an f-string is really an expression evaluated at run time, not a constant value. In Python source code, an f-string is a literal string, prefixed with ‘f’, which contains expressions inside braces. The expressions are replaced with their values. The key point here is that What this essentially means is that expressions inside f-strings are evaluated just like any other python expressions within the scope they appear in. The CPython compiler does the heavy lifting during the parsing stage to separate an f-string into string literals and expressions to generate the appropriate (AST): an f-string is really an expression evaluated at run time, not a constant value . Abstract Syntax Tree We use the the module to look at the abstract syntax tree associated with a simple expression within and outside of an f-string. We can see that the expression within the f-string gets parsed into a plain old binary operation just as it does outside the f-string. ast a + b a + b f{a + b} We can even see at the bytecode level that f-string expressions get evaluated just like any other python expressions: The function simply sums the local variables and and returns the results. The function does the same but the addition happens within an f-string. Besides the instruction in the disassembled bytecode of the (this instruction is there because after all, an f-string needs to stringify the results of the enclosed expression), the bytecode instructions to evaluate within and outside an f-string are the same. add_two a b add_two_fstring FORMAT_VALUE add_two_fstring function a + b Processing f-strings simply breaks down into evaluating the expression (just like any other python expression) enclosed within the curly braces and then combining it with the string literal portion of the the f-string to return the value of the final string. There is no additional runtime processing required . This makes f-strings pretty fast and efficient. Why is much slower than f-strings? The answer becomes clear once we look at the disassembled byte code for a function using str.format(): str.format() From the disassembled bytecode, two bytecode instructions immediately jump out: and . When we use , the function first needs to be looked up in the global scope. This is done via the bytecode instruction. Global variable lookup is not really a cheap operation and involves a number of steps(take a look at one of my earlier on how attribute lookup works if you are curious). Once the function is located, the binary add operation ( is invoked to sum the variables and Finally the function is executed via the bytecode instruction and the stringified results are returned. Function invocation in python is not cheap and has considerable overhead. When using the extra time spent in and is what contributes to str.format() being much slower than f-strings. LOAD_ATTR CALL_FUNCTION str.format() format LOAD_ATTR post format BINARY_ADD) a b. format CALL_FUNCTION str.format(), LOAD_ATTR CALL_FUNCTION What about formatting? We saw that this is faster than str.format() but still slower than f-strings. Again, lets look at the disassembled byte code for a function using %-string formatting for clues: %-string Right off the bat, we don’t see the and bytecode instructions — so %-string formatting avoids the overhead of global attribute lookup and python function invocation. This explains why it is faster than str.format(). But why %-string formatting is still slower than f-strings? One potential place where %-string formatting might be spending extra time is in the bytecode instruction**.** I haven’t done thorough profiling of the bytecode instruction but looking at the CPython source code, we can get a sense of why there might just be a tiny bit of overhead involved with invoking LOAD_ATTR CALL_FUNCTION BINARY_MODULO BINARY_MODULO BINARY_MODULO: From the python C source code snippet above, we see that the operation is overloaded. Each time it is invoked, it needs to check the type of its operands (line 7 -13 in the above code snippet) to determine whether the operands are string objects or not. If they are, then the modulo operator performs string formatting operations. Else it computes the usual modulo(returns remainder from the division of the first argument from the second). Although small, this type checking does come with an overhead which f-strings avoid. BINARY_MODULO Hopefully, this post has helped shed some light on why f-strings stand out from the crowd when it comes to string formatting. F-strings are fast, simple to use, practical and lead to much cleaner code. Use them!