Skip to content

Patched async function not registering calls until coroutine is awaited #137594

Description

@GabrielSalla

Bug report

Bug description:

When using autospec=True the patched async function only has calls registered after the coroutine is awaited. This behavior started happening after I upgraded from 3.12 to 3.13.6.

Code to reproduce

File tests/other_module.py

async def a(value):
    return value * 2

File tests/test_1.py

from unittest.mock import patch

import pytest

import other_module

pytestmark = pytest.mark.asyncio(loop_scope="session")

def b(value):
    return other_module.a(value)


async def test_1():
    with patch.object(other_module, "a", side_effect=other_module.a, autospec=True) as a_mock:
        result = b(10)

        assert not a_mock.called  # This should be True because 'a' was called, but it's False

        await result
        assert a_mock.called  # Now it's True

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Aug 9, 2025
  2. corona10 commented on Aug 10, 2025

    @corona10
    Member
  3. kumaraditya303 commented on Aug 16, 2025

    @kumaraditya303
    Contributor

    This behavior started happening after I upgraded from 3.12 to 3.13.6.

    Can you do git bisect to find which commit caused the change in behavior?

  4. tirkarthi commented on Sep 6, 2025

    @tirkarthi
    Member

    This seems to be a regression with 9bf8d82 where async def is used instead of def to help inspect.iscoroutinefunction . create_autospec should check for signature as per docs despite the function being async as per the docs. Since the change was in Python 3.13 and not backported maybe set the signature as sync and mark it using markcoroutinefunction introduced in Python 3.12 but this might be an issue with mock backport since inspect will not have markcoroutinefunction.

    https://docs.python.org/3/library/inspect.html#inspect.markcoroutinefunction

    https://docs.python.org/3/library/unittest.mock.html#unittest.mock.create_autospec

    unittest.mock.create_autospec(spec, spec_set=False, instance=False, **kwargs)
    Create a mock object using another object as a spec. Attributes on the mock will use the corresponding attribute on the spec object as their spec.
    Functions or methods being mocked will have their arguments checked to ensure that they are called with the correct signature.

    # gh137594_autospec
    
    from unittest.mock import create_autospec
    
    async def a(b, c):
        pass
    
    spec = create_autospec(a)
    spec()

    Before 9bf8d82

    python gh137594_autospec.py
    Traceback (most recent call last):
      File "/home/karthikeyan/stuff/python/gh137594/gh137594_autospec.py", line 7, in <module>
        spec()
        ~~~~^^
      File "<string>", line 2, in a
      File "/home/karthikeyan/stuff/python/cpython/Lib/unittest/mock.py", line 192, in checksig
        sig.bind(*args, **kwargs)
        ~~~~~~~~^^^^^^^^^^^^^^^^^
      File "/home/karthikeyan/stuff/python/cpython/Lib/inspect.py", line 3233, in bind
        return self._bind(args, kwargs)
               ~~~~~~~~~~^^^^^^^^^^^^^^
      File "/home/karthikeyan/stuff/python/cpython/Lib/inspect.py", line 3146, in _bind
        raise TypeError(msg) from None
    TypeError: missing a required argument: 'b'
    

    main as of c919d02

    python gh137594_autospec.py
    /home/karthikeyan/stuff/python/gh137594/gh137594_autospec.py:7: RuntimeWarning: coroutine 'a' was never awaited
      spec()
    RuntimeWarning: Enable tracemalloc to get the object allocation traceback
    
    

    A simpler reproducer for the issue without pytest dependency

    import asyncio
    from unittest.mock import patch
    import other_module
    
    
    async def a(i):
        pass
    
    async def a1(i):
        pass
    
    def b(value):
        return a(value)
    
    
    async def test_1():
        with patch(f"{__name__}.a", side_effect=a1, autospec=True) as a_mock:
            result = b(10)
    
            assert a_mock.called  # This should be True because 'a' was called, but it's False
    
            await result
            assert a_mock.called  # Now it's True
    
    
    asyncio.run(test_1())

    cc: @graingert @cjw296

  5. cjw296 commented on Sep 15, 2025

    @cjw296
    Contributor

    @tirkarthi - nice work on the investigation. Would your reproducer be something we can add as a unit test to prevent this happening again? If so, then the fix becomes getting that test to pass while still keeping the test(s) introduced in #94962 passing.

    I'm not sure I understand the fix you're proposing, but if you could put it and the unit test in a new PR that would be a great place to start. The backport is a secondary concern...

  6. tirkarthi commented on Oct 9, 2025

    @tirkarthi
    Member

    If so, then the fix becomes getting that test to pass while still keeping the test(s) introduced in #94962 passing.

    My patch will need a change to the tests since #94962 explicitly has a test so that a coroutine function can be called with invalid signature and the validation is done only when it's awaited unlike the normal flow and in previous versions of python where the signature validation is during the call to coroutine function itself instead of during await. With autospec I would expect the validation to be done during the call itself.

    Relevant test : https://github.com/python/cpython/pull/94962/files#diff-701e692796f7bc8001aec6f354f4770ad9fdf05116ca6934fc1dbf7705f80fdbR251-R252

    Python 3.11 vs main difference

    python3.11 -m asyncio    
    asyncio REPL 3.11.11 (main, Dec  4 2024, 08:55:08) [GCC 9.4.0] on linux
    Use "await" directly instead of "asyncio.run()".
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import asyncio
    >>> async def foo(a, b): pass
    ... 
    >>> foo()
    Traceback (most recent call last):
      File "/usr/lib/python3.11/concurrent/futures/_base.py", line 456, in result
        return self.__get_result()
               ^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
        raise self._exception
      File "/usr/lib/python3.11/asyncio/__main__.py", line 34, in callback
        coro = func()
               ^^^^^^
      File "<console>", line 1, in <module>
    TypeError: foo() missing 2 required positional arguments: 'a' and 'b'
    >>> from unittest.mock import create_autospec
    >>> mock = create_autospec(foo)
    >>> mock()
    Traceback (most recent call last):
      File "/usr/lib/python3.11/concurrent/futures/_base.py", line 456, in result
        return self.__get_result()
               ^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result
        raise self._exception
      File "/usr/lib/python3.11/asyncio/__main__.py", line 34, in callback
        coro = func()
               ^^^^^^
      File "<console>", line 1, in <module>
      File "<string>", line 2, in foo
      File "/usr/lib/python3.11/unittest/mock.py", line 192, in checksig
        sig.bind(*args, **kwargs)
      File "/usr/lib/python3.11/inspect.py", line 3195, in bind
        return self._bind(args, kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.11/inspect.py", line 3110, in _bind
        raise TypeError(msg) from None
    TypeError: missing a required argument: 'a'
    

    main branch where invalid signature is not validated during call time.

    ./python -m asyncio                           
    asyncio REPL 3.15.0a0 (heads/main:d2deb8fdef1, Oct  9 2025, 18:30:25) [GCC 9.4.0] on linux
    Use "await" directly instead of "asyncio.run()".
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import asyncio
    >>> async def foo(a, b): pass
    ... 
    >>> foo()
    Traceback (most recent call last):
      File "/home/karthikeyan/stuff/python/cpython/Lib/concurrent/futures/_base.py", line 450, in result
        return self.__get_result()
               ~~~~~~~~~~~~~~~~~^^
      File "/home/karthikeyan/stuff/python/cpython/Lib/concurrent/futures/_base.py", line 395, in __get_result
        raise self._exception
      File "/home/karthikeyan/stuff/python/cpython/Lib/asyncio/__main__.py", line 44, in callback
        coro = func()
      File "<python-input-1>", line 1, in <module>
        foo()
        ~~~^^
    TypeError: foo() missing 2 required positional arguments: 'a' and 'b'
    >>> from unittest.mock import create_autospec
    >>> mock = create_autospec(foo)
    
    # mock() is allowed and signature is validated only during await
    >>> m = mock()
    >>> await m
    Traceback (most recent call last):
      File "/home/karthikeyan/stuff/python/cpython/Lib/concurrent/futures/_base.py", line 450, in result
        return self.__get_result()
               ~~~~~~~~~~~~~~~~~^^
      File "/home/karthikeyan/stuff/python/cpython/Lib/concurrent/futures/_base.py", line 395, in __get_result
        raise self._exception
      File "<python-input-6>", line 1, in <module>
        await m
      File "<string>", line 2, in foo
      File "/home/karthikeyan/stuff/python/cpython/Lib/unittest/mock.py", line 218, in checksig
        sig.bind(*args, **kwargs)
        ~~~~~~~~^^^^^^^^^^^^^^^^^
      File "/home/karthikeyan/stuff/python/cpython/Lib/inspect.py", line 3233, in bind
        return self._bind(args, kwargs)
               ~~~~~~~~~~^^^^^^^^^^^^^^
      File "/home/karthikeyan/stuff/python/cpython/Lib/inspect.py", line 3146, in _bind
        raise TypeError(msg) from None
    TypeError: missing a required argument: 'a'
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions