Sam Hooke

These are rough notes from whatever I was working on, interested in or thinking about at the time. They vary greatly in quality and length, but prove useful to me, and hopefully to you too!

mypy and verbose logging

tl;dr:

If you want to add a custom logging level to Python (e.g. verbose), and you want it to play nicely with mypy (e.g. to not report error: "Logger" has no attribute "verbose" every time you call logger.verbose()), there doesn’t seem to be a better way than appending # type: ignore to every call to logger.verbose():

init_verbose_logging()
logger = logging.getLogger(__name__)
logger.verbose("this is logged at verbose level")  # type: ignore

I’d love to find a better way to do this, so please reach out if you know a solution. I’ve done quite a lot of diggint into this, but I’m new to mypy, so perhaps I’ve missed something. Regardless, read on for the story on how I came to this conclusion, and what the horrible alternative solution is.


Python’s standard library logging module comes with five log levels:

  • CRITICAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG

Each has a corresponding log method of the same name, but in lowercase, which is used to write to the log file at that log level. For example:

logger = logging.getLogger(__name__)
logger.debug("This is a DEBUG level message")
logger.critical("This is a CRITICAL level message")

It is fairly typical behaviour to add your own custom log level. For example, a VERBOSE or TRACE level log, which may sit one level lower than DEBUG. Althrough there are many answers on how to do this, they fall into a few categories.

The monkeypatch method

The monkeypatch method involves adding a verbose method to the existing default logger class:

def verbose(self: Any, msg: str, *args: Any, **kwargs: Any) -> None:
    """Function which is monkeypatched into the logging module to add support
    for the "VERBOSE" log level.
    """
    if self.isEnabledFor(VERBOSE):
        self._log(VERBOSE, msg, args, **kwargs)


def init_verbose_logging() -> None:
    """Monkeypatches the logging module to add a `verbose` log method and a
    "VERBOSE" log level.
    """

    # Add custom log level
    logging.addLevelName(VERBOSE, "VERBOSE")

    # Monkeypatch logger to use our log level
    logging.Logger.verbose = verbose

The first problemw with this is the actual monkeypatching itself on the final line. This causes mypy to report error: "Type[Logger]" has no attribute "verbose" because we are trying to set an attribute outside of initialisation. Ideally, we would fix this by modifying the Logger class ourselves (such as when using a mixin with mypy). But unfortunately the Logger class exists in the Python standard library, which is outside of our reach.

We can append # type: ignore to this line, or we could rewrite it using setattr, e.g.:

setattr(logging.Logger, "verbose", verbose)

However, neither of those solutions help mypy understand that our logger class does have a verbose method. So any call to logger.verbose() still results in error: "Logger" has no attribute "verbose".

This means that every time we call logger.verbose(), we would have to append # type: ignore as a comment. For example:

logger.verbose("example")  # type: ignore

In a code base with lots of calls to logger.verbose(), this is not ideal.

The class method

The class method involves using the logging.setLoggerClass to set the default logger class. This avoids the slightly dirty aspect of monkeypatching, but still makes use of the _log private method:

class VerboseLogger(logging.getLoggerClass()):
    def __init__(self, name: str, level: Union[int, Text]=logging.NOTSET) -> None:
        super().__init__(name, level)
        logging.addLevelName(VERBOSE, "VERBOSE")

    def verbose(self, msg: str, *args: Any, **kwargs: Any) -> None:
        if self.isEnabledFor(VERBOSE):
            self._log(VERBOSE, msg, args, **kwargs)


def init_verbose_logging() -> None:
    logging.setLoggerClass(VerboseLogger)

The class method has a couple of disadvantages.

The first disadvantage of the class method is that init_verbose_logging must be called before logging.getLogger is called, since it will not affect any Logger objects that already exist. For example:

logger = logging.getLogger(__name__)
init_verbose_logging()  # Uses class method
logger.verbose("this line will FAIL")

Whereas the monkeypatch method will affect the existing Logger object, and need only be called before logging.verbose is called. For example:

logger = logging.getLogger(__name__)
init_verbose_logging()  # Uses monkeypatch method
logger.verbose("this line will SUCCEED")

However, we can easily work around that by modifying our codebase to ensure that init_verbose_logging is called first thing.

The second disadvantage is more problematic. The use of logging.getLoggerClass() is not supported by mypy. It will report error: Unsupported dynamic base class "logging.getLoggerClass". We can ignore this specific warning by applying the # type: ignore comment to the class, e.g.:

class VerboseLogger(logging.getLoggerClass()):  # type: ignore
	# etc...

Alternatively, it may be safe to replace logging.getLoggerClass() with logging.Logger, assuming that no other code in your codebase also tries to replace the logger class.

At this point we encounter the exact same problem we had with the monkeypatch method, in that every call to logger.verbose() must have # type: ignore afterwards, which is not ideal.

Solution

So what is the solution?

I have found one solution, but I will say up front, it is not great.

For your own Python code, you can include the type hint annotations that mypy needs inline. However, if you are using third-party code which does not have type annotations, you would be in a pickle if it were not for stubs. Stubs allow the type hinting to be defined in separate .pyi files. These can be included inside the Python module that requires the type hinting, or in a separate Python module with the -stubs suffix. This latter point is especially important, because it means that without modifying the third-party library itself, you can define the type hinting for it.

For example, if there exists a foobar library without type hinting, a foobar-stubs package can be created which has the type hinting defined for the public interface.

Since the Python standard library does not have type hinting throughout, the typeshed package (which is included inside mypy) was developed to provide these stubs. This lets you use all of the Python standard library with mypy out-of-the-box.

If we look inside the typeshed definition for the Python standard library logging module, we can see in __init__.pyi that only the five log levels mentioned at the start of this post are defined:

    elif sys.version_info >= (3,):
        def debug(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def info(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def warning(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                    **kwargs: Any) -> None: ...
        def warn(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def error(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def critical(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                     **kwargs: Any) -> None: ...

Going one step further, there is also the concept of partial stubs. As the name implies, these are stubs that only provide a type hinting definition for part of the public interface of the Python module.

Note that the type checker resolution order will check for partial stubs before it falls back to typeshed. This means it is possible to override the typeshed type hinting!

Perhaps you can see where this is going?

We can write a partial stub to define the verbose method, which will fix the error: "Logger" has no attribute "verbose". This should be as easy finding a way to override the __init__.pyi file to include:

        def verbose(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...

Where this solution falls short is that partial stubs operate on a per-file basis, and according to the BDFL, there is no intention to support partial stubs at the function level.

So the “solution”, which I am not keen to present, is to create a Python package called logging-stubs. I followed the mypy example partial stubs package, and created this:

logging-stubs
	setup.py
	logging-stubs
		__init__.pyi
		py.typed

The file contents are as follows:

setup.py
import setuptools

setuptools.setup(
    name='logging-stubs',
    author='yourself',
    version='0.1',
    zip_safe=False,
    package_data={'logging-stubs': ['__init__.pyi', 'py.typed']},
    packages=['logging-stubs'],
)

In accordance with PEP-0561, this indicates the package is a partial stub.

py.typed
partial

The following __init__.pyi file is a copy of the latest logging/__init__.pyi file from typeshed, but with the verbose method added into the Logger class (in two places).

__init__.pyi
# Stubs for logging (Python 3.7)

from typing import (
    Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, IO,
    Tuple, Text, Union, overload,
)
from string import Template
from time import struct_time
from types import TracebackType, FrameType
import sys
import threading

_SysExcInfoType = Union[Tuple[type, BaseException, Optional[TracebackType]],
                        Tuple[None, None, None]]
if sys.version_info >= (3, 5):
    _ExcInfoType = Union[None, bool, _SysExcInfoType, BaseException]
else:
    _ExcInfoType = Union[None, bool, _SysExcInfoType]
_ArgsType = Union[Tuple[Any, ...], Mapping[str, Any]]
_FilterType = Union[Filter, Callable[[LogRecord], int]]
_Level = Union[int, Text]
if sys.version_info >= (3, 6):
    from os import PathLike
    _Path = Union[str, PathLike[str]]
else:
    _Path = str

raiseExceptions: bool
logThreads: bool
logMultiprocessing: bool
logProcesses: bool

def currentframe() -> FrameType: ...

if sys.version_info >= (3,):
    _levelToName: Dict[int, str]
    _nameToLevel: Dict[str, int]
else:
    _levelNames: Dict[Union[int, str], Union[str, int]]  # Union[int:str, str:int]

class Filterer(object):
    filters: List[Filter]
    def __init__(self) -> None: ...
    def addFilter(self, filter: Filter) -> None: ...
    def removeFilter(self, filter: Filter) -> None: ...
    def filter(self, record: LogRecord) -> bool: ...

class Logger(Filterer):
    name: str
    level: int
    parent: Union[Logger, PlaceHolder]
    propagate: bool
    handlers: List[Handler]
    disabled: int
    def __init__(self, name: str, level: _Level = ...) -> None: ...
    def setLevel(self, level: _Level) -> None: ...
    def isEnabledFor(self, level: int) -> bool: ...
    def getEffectiveLevel(self) -> int: ...
    def getChild(self, suffix: str) -> Logger: ...
    if sys.version_info >= (3, 8):
        def verbose(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def debug(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def info(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def warning(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                    **kwargs: Any) -> None: ...
        def warn(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def error(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def exception(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                      stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                      **kwargs: Any) -> None: ...
        def critical(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                     **kwargs: Any) -> None: ...
        def log(self, level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                **kwargs: Any) -> None: ...
        def _log(
            self,
            level: int,
            msg: Any,
            args: _ArgsType,
            exc_info: Optional[_ExcInfoType] = ...,
            extra: Optional[Dict[str, Any]] = ...,
            stack_info: bool = ...,
            stacklevel: int = ...,
        ) -> None: ...  # undocumented
    elif sys.version_info >= (3,):
        def verbose(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def debug(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def info(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def warning(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                    **kwargs: Any) -> None: ...
        def warn(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def error(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def critical(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                     **kwargs: Any) -> None: ...
        fatal = critical
        def log(self, level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                **kwargs: Any) -> None: ...
        def exception(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                      stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                      **kwargs: Any) -> None: ...
        def _log(
            self,
            level: int,
            msg: Any,
            args: _ArgsType,
            exc_info: Optional[_ExcInfoType] = ...,
            extra: Optional[Dict[str, Any]] = ...,
            stack_info: bool = ...,
        ) -> None: ...  # undocumented
    else:
        def debug(self,
                  msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def info(self,
                 msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def warning(self,
                    msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        warn = warning
        def error(self,
                  msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def critical(self,
                     msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        fatal = critical
        def log(self,
                level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def exception(self,
                      msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                      extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def _log(
            self,
            level: int,
            msg: Any,
            args: _ArgsType,
            exc_info: Optional[_ExcInfoType] = ...,
            extra: Optional[Dict[str, Any]] = ...,
        ) -> None: ...  # undocumented
    def addFilter(self, filt: _FilterType) -> None: ...
    def removeFilter(self, filt: _FilterType) -> None: ...
    def filter(self, record: LogRecord) -> bool: ...
    def addHandler(self, hdlr: Handler) -> None: ...
    def removeHandler(self, hdlr: Handler) -> None: ...
    if sys.version_info >= (3, 8):
        def findCaller(self, stack_info: bool = ..., stacklevel: int = ...) -> Tuple[str, int, str, Optional[str]]: ...
    elif sys.version_info >= (3,):
        def findCaller(self, stack_info: bool = ...) -> Tuple[str, int, str, Optional[str]]: ...
    else:
        def findCaller(self) -> Tuple[str, int, str]: ...
    def handle(self, record: LogRecord) -> None: ...
    if sys.version_info >= (3,):
        def makeRecord(self, name: str, level: int, fn: str, lno: int, msg: Any,
                       args: _ArgsType,
                       exc_info: Optional[_SysExcInfoType],
                       func: Optional[str] = ...,
                       extra: Optional[Mapping[str, Any]] = ...,
                       sinfo: Optional[str] = ...) -> LogRecord: ...
    else:
        def makeRecord(self,
                       name: str, level: int, fn: str, lno: int, msg: Any,
                       args: _ArgsType,
                       exc_info: Optional[_SysExcInfoType],
                       func: Optional[str] = ...,
                       extra: Optional[Mapping[str, Any]] = ...) -> LogRecord: ...
    if sys.version_info >= (3,):
        def hasHandlers(self) -> bool: ...


CRITICAL: int
FATAL: int
ERROR: int
WARNING: int
WARN: int
INFO: int
DEBUG: int
NOTSET: int


class Handler(Filterer):
    level: int  # undocumented
    formatter: Optional[Formatter]  # undocumented
    lock: Optional[threading.Lock]  # undocumented
    name: Optional[str]  # undocumented
    def __init__(self, level: _Level = ...) -> None: ...
    def createLock(self) -> None: ...
    def acquire(self) -> None: ...
    def release(self) -> None: ...
    def setLevel(self, level: _Level) -> None: ...
    def setFormatter(self, fmt: Formatter) -> None: ...
    def addFilter(self, filt: _FilterType) -> None: ...
    def removeFilter(self, filt: _FilterType) -> None: ...
    def filter(self, record: LogRecord) -> bool: ...
    def flush(self) -> None: ...
    def close(self) -> None: ...
    def handle(self, record: LogRecord) -> None: ...
    def handleError(self, record: LogRecord) -> None: ...
    def format(self, record: LogRecord) -> str: ...
    def emit(self, record: LogRecord) -> None: ...


class Formatter:
    converter: Callable[[Optional[float]], struct_time]
    _fmt: Optional[str]
    datefmt: Optional[str]
    if sys.version_info >= (3,):
        _style: PercentStyle
        default_time_format: str
        default_msec_format: str

    if sys.version_info >= (3, 8):
        def __init__(self, fmt: Optional[str] = ...,
                     datefmt: Optional[str] = ...,
                     style: str = ..., validate: bool = ...) -> None: ...
    elif sys.version_info >= (3,):
        def __init__(self, fmt: Optional[str] = ...,
                     datefmt: Optional[str] = ...,
                     style: str = ...) -> None: ...
    else:
        def __init__(self,
                     fmt: Optional[str] = ...,
                     datefmt: Optional[str] = ...) -> None: ...

    def format(self, record: LogRecord) -> str: ...
    def formatTime(self, record: LogRecord, datefmt: Optional[str] = ...) -> str: ...
    def formatException(self, exc_info: _SysExcInfoType) -> str: ...
    if sys.version_info >= (3,):
        def formatMessage(self, record: LogRecord) -> str: ...  # undocumented
        def formatStack(self, stack_info: str) -> str: ...


class Filter:
    def __init__(self, name: str = ...) -> None: ...
    def filter(self, record: LogRecord) -> int: ...


class LogRecord:
    args: _ArgsType
    asctime: str
    created: int
    exc_info: Optional[_SysExcInfoType]
    exc_text: Optional[str]
    filename: str
    funcName: str
    levelname: str
    levelno: int
    lineno: int
    module: str
    msecs: int
    message: str
    msg: str
    name: str
    pathname: str
    process: int
    processName: str
    relativeCreated: int
    if sys.version_info >= (3,):
        stack_info: Optional[str]
    thread: int
    threadName: str
    if sys.version_info >= (3,):
        def __init__(self, name: str, level: int, pathname: str, lineno: int,
                     msg: Any, args: _ArgsType,
                     exc_info: Optional[_SysExcInfoType],
                     func: Optional[str] = ...,
                     sinfo: Optional[str] = ...) -> None: ...
    else:
        def __init__(self,
                     name: str, level: int, pathname: str, lineno: int,
                     msg: Any, args: _ArgsType,
                     exc_info: Optional[_SysExcInfoType],
                     func: Optional[str] = ...) -> None: ...
    def getMessage(self) -> str: ...


class LoggerAdapter:
    logger: Logger
    extra: Mapping[str, Any]
    def __init__(self, logger: Logger, extra: Mapping[str, Any]) -> None: ...
    def process(self, msg: Any, kwargs: MutableMapping[str, Any]) -> Tuple[Any, MutableMapping[str, Any]]: ...
    if sys.version_info >= (3, 8):
        def debug(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def info(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def warning(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                    **kwargs: Any) -> None: ...
        def warn(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def error(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def exception(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                      stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                      **kwargs: Any) -> None: ...
        def critical(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                     **kwargs: Any) -> None: ...
        def log(self, level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                **kwargs: Any) -> None: ...
    elif sys.version_info >= (3,):
        def debug(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def info(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def warning(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                    **kwargs: Any) -> None: ...
        def warn(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
        def error(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
        def exception(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                      stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                      **kwargs: Any) -> None: ...
        def critical(self, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                     **kwargs: Any) -> None: ...
        def log(self, level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                **kwargs: Any) -> None: ...
    else:
        def debug(self,
                  msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def info(self,
                 msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def warning(self,
                    msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                    extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def error(self,
                  msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def exception(self,
                      msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                      extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def critical(self,
                     msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                     extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
        def log(self,
                level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    def isEnabledFor(self, lvl: int) -> bool: ...
    if sys.version_info >= (3,):
        def getEffectiveLevel(self) -> int: ...
        def setLevel(self, lvl: Union[int, str]) -> None: ...
        def hasHandlers(self) -> bool: ...
    if sys.version_info >= (3, 6):
        def _log(
            self,
            level: int,
            msg: Any,
            args: _ArgsType,
            exc_info: Optional[_ExcInfoType] = ...,
            extra: Optional[Dict[str, Any]] = ...,
            stack_info: bool = ...,
        ) -> None: ...  # undocumented

if sys.version_info >= (3,):
    def getLogger(name: Optional[str] = ...) -> Logger: ...
else:
    @overload
    def getLogger() -> Logger: ...
    @overload
    def getLogger(name: Union[Text, str]) -> Logger: ...
def getLoggerClass() -> type: ...
if sys.version_info >= (3,):
    def getLogRecordFactory() -> Callable[..., LogRecord]: ...

if sys.version_info >= (3, 8):
    def debug(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
              stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
              **kwargs: Any) -> None: ...
    def info(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
             stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
             **kwargs: Any) -> None: ...
    def warning(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                **kwargs: Any) -> None: ...
    def warn(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
             stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
             **kwargs: Any) -> None: ...
    def error(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
              stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
              **kwargs: Any) -> None: ...
    def critical(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
    def exception(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
    def log(level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
            stack_info: bool = ..., stacklevel: int = ..., extra: Optional[Dict[str, Any]] = ...,
            **kwargs: Any) -> None: ...
elif sys.version_info >= (3,):
    def debug(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
              stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
              **kwargs: Any) -> None: ...
    def info(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
             stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
             **kwargs: Any) -> None: ...
    def warning(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                **kwargs: Any) -> None: ...
    def warn(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
             stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
             **kwargs: Any) -> None: ...
    def error(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
              stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
              **kwargs: Any) -> None: ...
    def critical(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                 **kwargs: Any) -> None: ...
    def exception(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
                  **kwargs: Any) -> None: ...
    def log(level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
            stack_info: bool = ..., extra: Optional[Dict[str, Any]] = ...,
            **kwargs: Any) -> None: ...
else:
    def debug(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
              extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    def info(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
             extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    def warning(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    warn = warning
    def error(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
              extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    def critical(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                 extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    def exception(msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
                  extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
    def log(level: int, msg: Any, *args: Any, exc_info: _ExcInfoType = ...,
            extra: Optional[Dict[str, Any]] = ..., **kwargs: Any) -> None: ...
fatal = critical

def disable(lvl: int) -> None: ...
def addLevelName(lvl: int, levelName: str) -> None: ...
def getLevelName(lvl: Union[int, str]) -> Any: ...

def makeLogRecord(attrdict: Mapping[str, Any]) -> LogRecord: ...

if sys.version_info >= (3, 8):
    def basicConfig(*, filename: Optional[_Path] = ..., filemode: str = ...,
                    format: str = ..., datefmt: Optional[str] = ..., style: str = ...,
                    level: Optional[_Level] = ..., stream: Optional[IO[str]] = ...,
                    handlers: Optional[Iterable[Handler]] = ..., force: bool = ...) -> None: ...
elif sys.version_info >= (3,):
    def basicConfig(*, filename: Optional[_Path] = ..., filemode: str = ...,
                    format: str = ..., datefmt: Optional[str] = ..., style: str = ...,
                    level: Optional[_Level] = ..., stream: Optional[IO[str]] = ...,
                    handlers: Optional[Iterable[Handler]] = ...) -> None: ...
else:
    @overload
    def basicConfig() -> None: ...
    @overload
    def basicConfig(*, filename: Optional[str] = ..., filemode: str = ...,
                    format: str = ..., datefmt: Optional[str] = ...,
                    level: Optional[_Level] = ..., stream: IO[str] = ...) -> None: ...
def shutdown() -> None: ...

def setLoggerClass(klass: type) -> None: ...

def captureWarnings(capture: bool) -> None: ...

if sys.version_info >= (3,):
    def setLogRecordFactory(factory: Callable[..., LogRecord]) -> None: ...


if sys.version_info >= (3,):
    lastResort: Optional[StreamHandler]


class StreamHandler(Handler):
    stream: IO[str]  # undocumented
    if sys.version_info >= (3, 2):
        terminator: str
    def __init__(self, stream: Optional[IO[str]] = ...) -> None: ...
    if sys.version_info >= (3, 7):
        def setStream(self, stream: IO[str]) -> Optional[IO[str]]: ...


class FileHandler(StreamHandler):
    baseFilename: str  # undocumented
    mode: str  # undocumented
    encoding: Optional[str]  # undocumented
    delay: bool  # undocumented
    def __init__(self, filename: _Path, mode: str = ...,
                 encoding: Optional[str] = ..., delay: bool = ...) -> None: ...
    def _open(self) -> IO[Any]: ...


class NullHandler(Handler): ...


class PlaceHolder:
    def __init__(self, alogger: Logger) -> None: ...
    def append(self, alogger: Logger) -> None: ...


# Below aren't in module docs but still visible

class RootLogger(Logger): ...

root: RootLogger


if sys.version_info >= (3,):
    class PercentStyle(object):
        default_format: str
        asctime_format: str
        asctime_search: str
        _fmt: str

        def __init__(self, fmt: str) -> None: ...
        def usesTime(self) -> bool: ...
        def format(self, record: Any) -> str: ...

    class StrFormatStyle(PercentStyle):
        ...

    class StringTemplateStyle(PercentStyle):
        _tpl: Template

    _STYLES: Dict[str, Tuple[PercentStyle, str]]


BASIC_FORMAT: str

Inside this logging-stubs package, run python setup.py bdist_wheel to build the Python wheel, then add it as a test dependency to your code.

Needless to say (again), this is not a good solution. It is very brittle, because we would miss out on any updates the typeshed project make to the standard library logging support. Looking at the history for this file, we can see it has been fairly updated over the years, even over the past few months.

To come full circle, I regret to conclude that the best solution I could find for adding a verbose log level that plays nicely with mypy is to sprinkle a little # type: ignore after every logging.verbose() call.

P.S. While I ran out of time to write about it, I did explore the idea of finding a way to suppress all the errors caused by logger.verbose() by modifying the mypy configuration, but was unsuccessful. Perhaps though this is worth exploring further?

<< Previous: Researching generating playing cards from an SVG template using Inkscape