Skip to content

fix: CapturedIO.__init__ type annotations to accept Optional[StringIO]#15172

Merged
Carreau merged 1 commit into
ipython:mainfrom
eachimei:fix/captured-io-optional-types
Apr 10, 2026
Merged

fix: CapturedIO.__init__ type annotations to accept Optional[StringIO]#15172
Carreau merged 1 commit into
ipython:mainfrom
eachimei:fix/captured-io-optional-types

Conversation

@eachimei

@eachimei eachimei commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

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: StringIOstdout: Optional[StringIO] (same for stderr) in CapturedIO.__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 the None path indirectly via capture_output, but never construct CapturedIO directly with None.

No runtime behaviour change.

Fixes #15181

@Carreau Carreau merged commit 8635f71 into ipython:main Apr 10, 2026
20 checks passed
@Carreau

Carreau commented Apr 10, 2026

Copy link
Copy Markdown
Member

thanks !

@Carreau Carreau added this to the 9.13 milestone Apr 22, 2026
Carreau added a commit that referenced this pull request Apr 23, 2026
- Fill out whatsnew/version9.rst with all 6 milestone PRs: terminal
  image rendering via Kitty protocol (#15184), Python 3.11 support
  restoration (#15175), theme-aware color fix (#15156), CapturedIO
  type annotation fix (#15172), docs and contributing improvements
- Mark release.py as a full release (_version_extra = "")

https://claude.ai/code/session_016yXG8tqxaMuYxyw2bqZBEP
pull Bot pushed a commit to Stars1233/ipython that referenced this pull request Apr 23, 2026
- Fill out whatsnew/version9.rst with all 6 milestone PRs: terminal
  image rendering via Kitty protocol (ipython#15184), Python 3.11 support
  restoration (ipython#15175), theme-aware color fix (ipython#15156), CapturedIO
  type annotation fix (ipython#15172), docs and contributing improvements
- Mark release.py as a full release (_version_extra = "")

https://claude.ai/code/session_016yXG8tqxaMuYxyw2bqZBEP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: CapturedIO.__init__ type annotations don't accept None (Optional[StringIO])

2 participants