fix: align py::print with Python semantics - #6120
Conversation
|
TL;DR: No zero args test, I'm also running a simplification review, I'll push that and post it next. 🤖 AI text below 🤖 Verified: kwargs keys reaching Review of PR #6120 —
|
Normalize sep/end once and route all output through a single write helper, reuse bool_ and set_error, and iterate args with the fast tuple iterator. Share the Catch2 shutdown-probe harness between the interpreter and subinterpreter tests, and use one marker exception in the Python tests. No change in behavior. Assisted-by: ClaudeCode:claude-fable-5
|
TL;DR: 31 lines shorter pybind11.h, 43 lines shorter overall, 4KB smaller, uses our 🤖 AI text below 🤖 All 14 print tests pass. The cleanup is complete — here's the summary. Applied
Tests:
Skipped (with reasons)
Verification
|
|
Quick update (I can only work on it more tomorrow in my timezone): In the meantime I had this idea:
codex: |
|
Did a quick check for performance and potential downsides: (and PyEval_GetFrameBuiltins is the same, and we need that one for free-threading). 🤖 AI text below 🤖 Both questions answered — here are the results. PerformanceBenchmarked on CPython 3.14 (macOS,
So the custom code is not buying meaningful speed. The naive delegation ( Behavioral differences (the actual downsides)I diffed the edge cases directly; most things match exactly — stream protocol call sequence (
Bottom lineThere's no performance argument for keeping the emulation — with a Benchmark and diff scripts are in the scratchpad ( |
| // Importing sys can fail while an interpreter is shutting down. | ||
| return; | ||
| } | ||
| PyObject *sys_dict = PyModule_GetDict(sys.ptr()); |
There was a problem hiding this comment.
Why use PyObject instead of handles or py::object here?
| }); | ||
|
|
||
| m.def("print_args", | ||
| [](const py::args &args, const py::kwargs &kwargs) { py::print(*args, **kwargs); }); |
There was a problem hiding this comment.
Hold on can this it just be bound as py::print? Or does the overloading system not like that?
There was a problem hiding this comment.
I pointed codex to this question. I then added this to the PR #6121 Description:
The lambda is intentional: py::print is a variadic function template rather than a single function that m.def can bind directly, and a specialization taking py::args and py::kwargs would pass those containers as ordinary arguments instead of unpacking them.
|
Converted to Draft, in favor of PR #6121. (I will close this PR after 6121 is merged.) |
Description
This is a comprehensive cleanup of
py::print, prompted by the issue reported in #6012.Original problem
Windows GUI applications built with
/SUBSYSTEM:WINDOWSmay havesys.stdoutset toNone.py::printselected that object as its default stream and then attempted to access itswriteattribute, so printing could fail in this otherwise valid interpreter state.A narrowly placed
Nonecheck addresses that immediate failure, but the surrounding implementation had several related differences from Python'sprintbehavior. In particular, an explicitfile=Nonewas not treated like an omittedfile, stream selection happened after argument formatting, unknown keyword arguments were silently ignored, and output was assembled and sent through a cachedwritemethod instead of following Python's incremental file protocol.Resulting behavior
This change makes
py::printfollow Python's relevant semantics more closely:file, or passingfile=None, resolves the currentsys.stdout.sys.stdoutisNone, printing is a no-op. This decision is made before converting values withstr, matching Python and avoiding side effects during shutdown or in GUI applications without a console stream.sys.stdoutis missing during normal interpreter operation,py::printraisesRuntimeError("lost sys.stdout")instead of silently discarding output.sep=Noneandend=Noneselect their defaults, while invalidsepandendvalues are rejected before any output is written.sep,end,file, andflushare accepted as keyword arguments. Unknown keywords, including names containing embedded NULs or unpaired surrogates, produce a safe and informative error.flushis truth-tested after successful output. This deliberately preservespy::print's historical portable ordering rather than interpreter-specific argument-parsing details; truth-testing and flushing errors propagate normally.PyFile_WriteObjectandPyFile_WriteStringAPIs. This preserves consequential incremental stream behavior, including repeatedwritelookup, partial output when a later conversion or write fails, and correct zero-argument behavior.The implementation deliberately does not delegate to
builtins.print: doing so would makepy::printsensitive to monkey-patching (and possible recursion) and would complicate its shutdown behavior.Interpreter lifetime and free-threading
The default-stream lookup distinguishes ordinary execution from interpreter teardown. During normal execution a missing
sys.stdoutremains an error; during finalization, printing safely becomes a no-op when the interpreter'ssysstate is no longer available.CPython's built-in
printobtains a strong reference directly from the current interpreter'ssysdictionary through the private_PySys_GetRequiredAttr(), which is unavailable to extension modules. On conventional GIL builds, pybind11 can safely promote the borrowed result ofPySys_GetObject()while retaining the canonical lookup. On free-threaded builds, concurrent replacement makes that unsafe, so pybind11 obtains a strong reference through the importedsysmodule instead.Because the free-threaded fallback necessarily consults
sys.modules, the regression that replacessys.modules["sys"]is skipped on those builds; this records the narrow limitation without complicating production code.Regression coverage exercises shutdown callbacks in both the main interpreter and subinterpreters. A separate
sys.stdout is Noneregression verifies that values are not converted withstronce no output is possible.Test coverage
The expanded tests exercise
py::print's portable contract across normal output and failure paths, including:None, missing, falsey, and custom streams;sepandenddefaults and validation, plusfileandflushhandling;__str__,write,flushtruth testing, andflushitself;No ABI-visible data structures or layouts are changed.
Validation completed locally:
pre-commit run --all-filespassed.This supersedes the implementation proposed in #6012 while preserving its intended behavior when
sys.stdout is None.Suggested changelog entry:
py::printto more closely follow Python's stream and keyword semantics, propagate stream errors, and safely handlesys.stdout is None, including during interpreter shutdown.📚 Documentation preview 📚: https://pybind11--6120.org.readthedocs.build/
📚 Documentation preview 📚: https://pybind11--6120.org.readthedocs.build/