fix: CapturedIO.__init__ type annotations to accept Optional[StringIO] by eachimei · Pull Request #15172 · ipython/ipython
Problem
CapturedIO.__init__ declares its stdout and stderr parameters as StringIO, but capture_output.__enter__ passes None for either parameter when the corresponding capture flag is False:
# capture_output.__enter__ stdout = stderr = outputs = None if self.stdout: stdout = sys.stdout = StringIO() if self.stderr: stderr = sys.stderr = StringIO() return CapturedIO(stdout, stderr, outputs) # stdout/stderr may be None
The runtime behaviour is correct — the stdout and stderr properties already handle None gracefully:
@property def stdout(self) -> str: if not self._stdout: # handles None return '' return self._stdout.getvalue()
But the type annotations don't reflect this, causing false positives in static analysis tools.
Origin
The incorrect annotation was introduced in IPython 9.12.0 via 74904ca ("Extend MonkeyType annotations to terminal, testing, and more utils modules"). MonkeyType infers types from runtime traces, and the tracing run apparently never exercised the capture_output(stdout=False) or capture_output(stderr=False) paths — so it only observed StringIO being passed, never None.
How to reproduce
Run pyright 1.1.396+ on any code that constructs CapturedIO with None arguments, or on IPython's own capture_output.__enter__:
error: Argument of type "StringIO | None" cannot be assigned to parameter "stdout"
of type "StringIO" in function "__init__"
"None" is not assignable to "StringIO" (reportArgumentType)
Note: this is not caught by IPython's own mypy CI because IPython.utils.capture is listed in the mypy overrides in pyproject.toml with ignore_errors = true.
Fix
- Change
stdout: StringIO→stdout: Optional[StringIO](same forstderr) inCapturedIO.__init__. - Add a direct regression test that constructs
CapturedIO(None, None)and asserts the expected empty-string behaviour. Existing tests (test_capture_output_no_stdout,test_capture_output_no_stderr) exercise theNonepath indirectly viacapture_output, but never constructCapturedIOdirectly withNone.
No runtime behaviour change.