Skip to content

ThreadingMock.call_count is not thread safe #142651

Description

@godlygeek

Bug report

Bug description:

Running this test program with (plain old, non-free-threaded) Python 3.14 fails:

import threading
import unittest.mock

m = unittest.mock.ThreadingMock()

LOOPS = 10_000
THREADS = 10


def test_function():
    for _ in range(LOOPS):
        m()


threads = [threading.Thread(target=test_function) for _ in range(THREADS)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

assert m.call_count == LOOPS * THREADS, f"Expected {LOOPS * THREADS}, got {m.call_count}"

Results are something like:

$ python3.14 python_mock_call_count_atomicity.py
Traceback (most recent call last):
  File "python_mock_call_count_atomicity.py", line 21, in <module>
    assert m.call_count == LOOPS * THREADS, f"Expected {LOOPS * THREADS}, got {m.call_count}"
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Expected 100000, got 95507

This is essentially the same bug as #122957, but that was closed with just a fix to the test suite. This is a bug report that the version of MagicMock for multithreading tests is not thread-safe and so the obvious way of writing those multithreading tests may not work.

CPython versions tested on:

3.14

Operating systems tested on:

Linux

Linked PRs

Activity

  1. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Dec 12, 2025
  2. chaope commented on Dec 13, 2025

    @chaope
    Contributor

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

    call_args_list
    This is a list of all the calls made to the mock object in sequence (so the length of the list is the number of times it has been called).

    Probably we can just use the length of this list as call_count.

    I'll open a PR with this solution.

  3. cjw296 commented on Dec 13, 2025

    @cjw296
    Contributor

    Does Mock document itself to be thread-safe? I'm not averse to bug fixes like this, but unless it's a clearly documented expectation that Mock is thread safe, I fear there may be many more cases like this lurking...

  4. added a commit that references this issue on Dec 15, 2025
  5. added 2 commits that reference this issue on Dec 15, 2025
  6. added 2 commits that reference this issue on Dec 15, 2025
  7. kumaraditya303 commented on Dec 15, 2025

    @kumaraditya303
    Contributor

    Does Mock document itself to be thread-safe? I'm not averse to bug fixes like this, but unless it's a clearly documented expectation that Mock is thread safe, I fear there may be many more cases like this lurking...

    I created #142752 for followup on this

  8. cjw296 commented on Dec 15, 2025

    @cjw296
    Contributor

    @godlygeek / @chaope - if either of you need this sooner than the next Python release, lemme know and I'll crank the backport machinery to get the backport release on pypi up to date.

  9. godlygeek commented on Dec 15, 2025

    @godlygeek
    ContributorAuthor

    I don't need it backported, but thank you. I've worked around this in the test suite where I hit it instead of using call_count, but I wanted to file the bug report in the hopes of it getting fixed going forward so that people don't keep bumping into it.

    So, thanks for the fix!

  10. added a commit that references this issue on Dec 16, 2025
  11. colesbury commented on Dec 17, 2025

    @colesbury
    Contributor

    iOS Test failure in unrelated PR:

       FAIL: test_call_count_thread_safe (test.test_unittest.testmock.testthreadingmock.TestThreadingMock.test_call_count_thread_safe)
       ----------------------------------------------------------------------
       Traceback (most recent call last):
         File "/Users/runner/Library/Developer/CoreSimulator/Devices/AD3CB764-0EC8-4BE8-B517-274B0ADC1CE1/data/Containers/Bundle/Application/1DB972BD-087A-4A50-86C3-C413564FE5EC/iOSTestbed.app/python/lib/python3.15/test/test_unittest/testmock/testthreadingmock.py", line 219, in test_call_count_thread_safe
           self.assertEqual(m.call_count, LOOPS * THREADS)
           ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       AssertionError: 998 != 1000
       
       ----------------------------------------------------------------------
    
  12. 2 remaining items

  13. kumaraditya303 commented on Dec 18, 2025

    @kumaraditya303
    Contributor

    The problem with the fix in the PR is that the combination of these two lines are not atomic:

    I am able to reproduce the issue, adding an artifical delay makes it easier to reproduce. Here is the modified code:

    diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
    index 34fd49bf56f..f272d3b259b 100644
    --- a/Lib/unittest/mock.py
    +++ b/Lib/unittest/mock.py
    @@ -1187,7 +1187,10 @@ def _increment_mock_call(self, /, *args, **kwargs):
             _call = _Call((args, kwargs), two=True)
             self.call_args = _call
             self.call_args_list.append(_call)
    -        self.call_count = len(self.call_args_list)
    +        l = len(self.call_args_list)
    +        import time
    +        time.sleep(0.01) # simulate context switch
    +        self.call_count = l
     
             # initial stuff for method_calls:
             do_method_calls = self._mock_parent is not None
    
    cpython on  main [$!] via C v17.0.0-clang via 🐍 v3.10.19 took 11s 
    ❯ ./python.exe -m test test_unittest.testmock.testthreadingmock -F -j 40
    Using random seed: 2077880501
    0:00:00 load avg: 10.85 Run tests in parallel using 40 worker processes
    0:00:03 load avg: 11.26 [  1/1] test_unittest.testmock.testthreadingmock failed (1 failure)
    test test_unittest.testmock.testthreadingmock failed -- Traceback (most recent call last):
      File "/Users/kumaraditya/work/cpython/Lib/test/test_unittest/testmock/testthreadingmock.py", line 219, in test_call_count_thread_safe
        self.assertEqual(m.call_count, LOOPS * THREADS)
        ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    AssertionError: 999 != 1000
    

    I created #142922 to use a lock around the stores so that all three are set atomically.
    With that change the test passes consistently.

  14. ebolyen commented on Feb 9, 2026

    @ebolyen

    Hello, the recently release backports #142743 and #142744 have caused a slight regression in our tests.

    Arguably we shouldn't be setting call_count directly, but I thought it was worth pointing out as others may also be doing the wrong thing here. One could imagine a setter for call_count to raise an appropriate error or warning as desired.

    This PR mostly outlines the issue and change we will need to make to adapt to Python 3.13.12 and 3.14.3

  15. added a commit that references this issue on Feb 19, 2026
  16. added a commit that references this issue on Mar 10, 2026
  17. added 2 commits that reference this issue on Mar 10, 2026
  18. added 2 commits that reference this issue on Mar 10, 2026
  19. added a commit that references this issue on Apr 25, 2026
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

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions