From 3d673148df47fa9c0b75d10b33ea31bc7607e6cb Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 16 Feb 2026 17:02:32 -0500 Subject: [PATCH 1/3] Add overloads for mutually-exclusive `logging.basicConfig` parameters --- stdlib/@tests/test_cases/check_logging.py | 16 +++++++++++++ stdlib/logging/__init__.pyi | 29 +++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/stdlib/@tests/test_cases/check_logging.py b/stdlib/@tests/test_cases/check_logging.py index fe3d8eb16fd0..838650803bb3 100644 --- a/stdlib/@tests/test_cases/check_logging.py +++ b/stdlib/@tests/test_cases/check_logging.py @@ -28,3 +28,19 @@ def record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord: logging.handlers.QueueListener(queue.Queue()) logging.handlers.QueueListener(queue.SimpleQueue()) logging.handlers.QueueListener(multiprocessing.Queue()) + +# These all raise at runtime. +logging.basicConfig(filename="foo.log", handlers=[]) # type: ignore +logging.basicConfig(filemode="w", handlers=[]) # type: ignore +logging.basicConfig(stream=None, handlers=[]) # type: ignore +logging.basicConfig(filename="foo.log", stream=None) # type: ignore +logging.basicConfig(filename=None, stream=None) # type: ignore +# These are ok. +logging.basicConfig() +logging.basicConfig(handlers=[]) +logging.basicConfig(filename="foo.log", filemode="w") +logging.basicConfig(filename="foo.log", filemode="w", handlers=None) +logging.basicConfig(stream=None) +logging.basicConfig(stream=None, handlers=None) +# dubious but accepted, has same meaning as 'stream=None'. +logging.basicConfig(filename=None) diff --git a/stdlib/logging/__init__.pyi b/stdlib/logging/__init__.pyi index 4e73b844543f..e708b830962e 100644 --- a/stdlib/logging/__init__.pyi +++ b/stdlib/logging/__init__.pyi @@ -576,16 +576,41 @@ if sys.version_info >= (3, 11): def getLevelNamesMapping() -> dict[str, int]: ... def makeLogRecord(dict: Mapping[str, object]) -> LogRecord: ... +@overload # handlers is non-None def basicConfig( *, - filename: StrPath | None = None, + format: str = ..., # default value depends on the value of `style` + datefmt: str | None = None, + style: _FormatStyle = "%", + level: _Level | None = None, + handlers: Iterable[Handler], + force: bool | None = False, + encoding: str | None = None, + errors: str | None = "backslashreplace", +) -> None: ... +@overload # handlers is None, filename is passed (but possibly None) +def basicConfig( + *, + filename: StrPath | None, filemode: str = "a", format: str = ..., # default value depends on the value of `style` datefmt: str | None = None, style: _FormatStyle = "%", level: _Level | None = None, + handlers: None = None, + force: bool | None = False, + encoding: str | None = None, + errors: str | None = "backslashreplace", +) -> None: ... +@overload # handlers is None, filename is not passed +def basicConfig( + *, + format: str = ..., # default value depends on the value of `style` + datefmt: str | None = None, + style: _FormatStyle = "%", + level: _Level | None = None, stream: SupportsWrite[str] | None = None, - handlers: Iterable[Handler] | None = None, + handlers: None = None, force: bool | None = False, encoding: str | None = None, errors: str | None = "backslashreplace", From 1cdd57862ff93801b47d141730b3dac0690cc21a Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 16 Feb 2026 16:59:19 -0500 Subject: [PATCH 2/3] Allow inert `filemode` when `handlers=None` --- stdlib/@tests/test_cases/check_logging.py | 2 ++ stdlib/logging/__init__.pyi | 1 + 2 files changed, 3 insertions(+) diff --git a/stdlib/@tests/test_cases/check_logging.py b/stdlib/@tests/test_cases/check_logging.py index 838650803bb3..4622955a3419 100644 --- a/stdlib/@tests/test_cases/check_logging.py +++ b/stdlib/@tests/test_cases/check_logging.py @@ -42,5 +42,7 @@ def record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord: logging.basicConfig(filename="foo.log", filemode="w", handlers=None) logging.basicConfig(stream=None) logging.basicConfig(stream=None, handlers=None) +# 'filemode' is inert when 'filename' is not passed, but is accepted if 'handlers=None'. +logging.basicConfig(filemode="w", stream=None) # dubious but accepted, has same meaning as 'stream=None'. logging.basicConfig(filename=None) diff --git a/stdlib/logging/__init__.pyi b/stdlib/logging/__init__.pyi index e708b830962e..3b393e700b82 100644 --- a/stdlib/logging/__init__.pyi +++ b/stdlib/logging/__init__.pyi @@ -605,6 +605,7 @@ def basicConfig( @overload # handlers is None, filename is not passed def basicConfig( *, + filemode: str = "a", format: str = ..., # default value depends on the value of `style` datefmt: str | None = None, style: _FormatStyle = "%", From e90fe8b534fefd7f6b1cdc5cd1a818b01ec9cb0e Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Tue, 17 Feb 2026 22:26:12 -0500 Subject: [PATCH 3/3] Disallow passing `filemode`, `encoding`, or `errors` without `filename` --- stdlib/@tests/test_cases/check_logging.py | 10 ++++++++-- stdlib/logging/__init__.pyi | 5 ----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/stdlib/@tests/test_cases/check_logging.py b/stdlib/@tests/test_cases/check_logging.py index 4622955a3419..a7d57cbda132 100644 --- a/stdlib/@tests/test_cases/check_logging.py +++ b/stdlib/@tests/test_cases/check_logging.py @@ -42,7 +42,13 @@ def record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord: logging.basicConfig(filename="foo.log", filemode="w", handlers=None) logging.basicConfig(stream=None) logging.basicConfig(stream=None, handlers=None) -# 'filemode' is inert when 'filename' is not passed, but is accepted if 'handlers=None'. -logging.basicConfig(filemode="w", stream=None) # dubious but accepted, has same meaning as 'stream=None'. logging.basicConfig(filename=None) +# These are technically accepted at runtime, but are forbidden in the stubs to help +# prevent user mistakes. Passing 'filemode' / 'encoding' / 'errors' does nothing +# if 'filename' is not specified. +logging.basicConfig(stream=None, filemode="w") # type: ignore +logging.basicConfig(stream=None, encoding="utf-8") # type: ignore +logging.basicConfig(stream=None, errors="strict") # type: ignore +logging.basicConfig(handlers=[], encoding="utf-8") # type: ignore +logging.basicConfig(handlers=[], errors="strict") # type: ignore diff --git a/stdlib/logging/__init__.pyi b/stdlib/logging/__init__.pyi index 3b393e700b82..89c94816a906 100644 --- a/stdlib/logging/__init__.pyi +++ b/stdlib/logging/__init__.pyi @@ -585,8 +585,6 @@ def basicConfig( level: _Level | None = None, handlers: Iterable[Handler], force: bool | None = False, - encoding: str | None = None, - errors: str | None = "backslashreplace", ) -> None: ... @overload # handlers is None, filename is passed (but possibly None) def basicConfig( @@ -605,7 +603,6 @@ def basicConfig( @overload # handlers is None, filename is not passed def basicConfig( *, - filemode: str = "a", format: str = ..., # default value depends on the value of `style` datefmt: str | None = None, style: _FormatStyle = "%", @@ -613,8 +610,6 @@ def basicConfig( stream: SupportsWrite[str] | None = None, handlers: None = None, force: bool | None = False, - encoding: str | None = None, - errors: str | None = "backslashreplace", ) -> None: ... def shutdown(handlerList: Sequence[Any] = ...) -> None: ... # handlerList is undocumented def setLoggerClass(klass: type[Logger]) -> None: ...