It is important to understand that in print("Two plus two = " + str(number)) the concatenation operation has nothing to do with print and happens before print is called.
Let's do some timings:
from timeit import Timer
def with_concat():
print("Two plus two = " + str(4))
def no_concat():
print("Two plus two =", 4)
print(min(Timer(with_concat).repeat(100, 100)))
print(min(Timer(no_concat).repeat(100, 100)))
Outputs
0.0006760049999998685
0.0013034899999999627
Counter-intuitively (see my comment to the question, string concatenation may be expensive) the example with concatenation is actually faster (by a factor of 2!) in a reproducible manner. But why?
let's inspect the byte code:
from dis import dis
def with_concat():
print("Two plus two = " + str(4))
def no_concat():
print("Two plus two =", 4)
dis(with_concat)
outputs
0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Two plus two = ')
4 LOAD_GLOBAL 1 (str)
6 LOAD_CONST 2 (4)
8 CALL_FUNCTION 1
10 BINARY_ADD
12 CALL_FUNCTION 1
14 POP_TOP
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
While
dis(no_concat)
outputs
0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Two plus two =')
4 LOAD_CONST 2 (4)
6 CALL_FUNCTION 2
8 POP_TOP
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
Judging by the byte code it also looks like no_concat should be faster (shorter, simpler code).
The delay must come from the C source code (in the case of CPython at least).
Let's look at the relevant lines:
static PyObject *
builtin_print(PyObject *self, PyObject *args, PyObject *kwds)
{
.
.
.
for (i = 0; i < PyTuple_Size(args); i++) {
if (i > 0) {
if (sep == NULL)
err = PyFile_WriteString(" ", file);
else
err = PyFile_WriteObject(sep, file,
Py_PRINT_RAW);
if (err)
return NULL;
}
err = PyFile_WriteObject(PyTuple_GetItem(args, i), file,
Py_PRINT_RAW);
if (err)
return NULL;
}
.
.
.
}
To me it seems like the overhead of using print(*args) is due to the repetitive calls to PyTuple_GetItem(args, i), and that the benefit of using it over print(a + lot + of + concatenated + strings) is there only when the number of concatenated strings is large enough to make the concatenation the bottleneck (ie slower than the repetitive calls to PyTuple_GetItem(args, i)).