Skip to content

Speed up print() by looking up file.write once per call #160725

Open

Description

@cmaloney

Feature or enhancement

print() currently looks up file.write, creating a new bound method, for every argument, separator, and line ending. Currently print('a', 1, 2.5) does six lookups. The default sep and end also go through PyFile_WriteString(), which converts " " and "\n" from C strings with strlen() and the UTF-8 decoder each time, only to return CPython's cached one-character strings. Looking write up once per call and passing the cached strings directly makes print() 1.20x faster (geometric mean), and write() is still called exactly as before.

Measurements

Benchmark main patched Time Instructions
print('hello world'), /dev/null 291 ns 234 ns 1.25x faster −18%
print('hello world'), StringIO 253 ns 193 ns 1.31x faster −14%
print('a', 1, 2.5), /dev/null 996 ns 687 ns 1.45x faster −31%
print(*range(10)), /dev/null 2.86 µs 1.73 µs 1.66x faster −39%
print(), /dev/null n.s. −3%
print('x' * 2**20), /dev/null n.s.
print(list(range(1000))), /dev/null n.s. −0.1%
print('hello world', flush=True), disk file 1.55 µs 1.38 µs 1.12x faster

Time: pyperf, -p 10, release build without PGO, Linux x86_64. n.s. = not significant. Instructions: per print() call, measured with valgrind (cachegrind) and PYTHONHASHSEED=0, with a noise floor of about ±1%.

Behavior change

write is looked up once per call. If an argument's __str__ rebinds file.write, the new method is used from the next print() call on. A file without a write method still raises AttributeError before any argument is converted.

Benchmark

Benchmark script
import pyperf

SETUP = """
import io, os, tempfile
devnull = open(os.devnull, 'w')
sio = io.StringIO()
disk = tempfile.TemporaryFile('w')
ten_ints = tuple(range(10))
big = 'x' * 2**20
big_list = list(range(1000))
"""

CASES = {
    "one_str[devnull]": "print('hello world', file=devnull)",
    "one_str[sio]": "print('hello world', file=sio)",
    "three_args[devnull]": "print('a', 1, 2.5, file=devnull)",
    "ten_ints[devnull]": "print(*ten_ints, file=devnull)",
    "empty[devnull]": "print(file=devnull)",
    "str_1mib[devnull]": "print(big, file=devnull)",
    "list_1000[devnull]": "print(big_list, file=devnull)",
    "flush[disk]": "print('hello world', file=disk, flush=True)",
}

runner = pyperf.Runner()
for name, stmt in CASES.items():
    runner.timeit(name, stmt, SETUP)
./python-main bench_print.py -p 10 -o main.json
./python-patched bench_print.py -p 10 -o patched.json
python -m pyperf compare_to main.json patched.json --table

Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

No response

Linked PRs

Activity

  1. self-assigned this
    on Oct 26, 2026
  2. added 3 commits that reference this issue on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

interpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetopic-IOtype-featureA feature request or enhancement

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions