I am actively trying to get race condition and cause problem in calculation for demonstration purposes but i can't achieve such problem simply. My tought process was to create a counter variable, reach it from diffrent threads and async functions (i did not tried mp since it pauses process) and increase it by one. Running 3 instances of for loops in range x and increasing the counter by 1 in each loop i expected to get value lower then 3x due to race condition. I did not use locks, but i still get 3x value each run. Does it due to GIL update? i have tried with python versions 3.10 / 3.11 / 3.13. What should i do to get race condition simple structure my code to get race condition import threading import asyncio def multithreading_race_condition(): counter2 = 0 def increment(): nonlocal counter2 for _ in range(10000): counter2 = counter2 + 1 threads = [threading.Thread(target=increment) for _ in range(3)] for t in threads: t.start() for t in threads: t.join() print(f"Multithreading Final Counter: {counter2}") async def asyncio_race_condition(): counter3 = 0 async def increment(): nonlocal counter3 for _ in range(10000): counter3 = counter3 + 1 tasks = [asyncio.create_task(increment()) for _ in range(3)] await asyncio.gather(*tasks) print(f"Asyncio Final Counter: {counter3}") def main(): print("\nMultithreading Example:") multithreading_race_condition() print("\nAsyncio Example:") asyncio.run(asyncio_race_condition()) if __name__ == "__main__": main()  my output is Multithreading Example: Multithreading Final Counter: 30000 Asyncio Example: Asyncio Final Counter: 30000 
The time between fetching the value of counter, incrementing it and reassigning it back is very short. counter = counter + 1  In order to force a race condition for demonstration purposes, you should extend that window perhaps with sleep tmp = counter time.sleep(random.random()) counter = tmp + 1  I would also increase the number of concurrent tasks to give more chance for an issue to pop up. This should dramatically illustrate things. import threading import asyncio import time import random def multithreading_race_condition(): counter = 0 iteration_count = 10 task_count = 10 def increment(): nonlocal counter for _ in range(iteration_count): tmp = counter time.sleep(random.random()) counter = tmp + 1 threads = [threading.Thread(target=increment) for _ in range(task_count)] for t in threads: t.start() for t in threads: t.join() print(f"Multithreading Final Counter was {counter} expected {iteration_count * task_count}") async def asyncio_race_condition(): counter = 0 iteration_count = 10 task_count = 10 async def increment(): nonlocal counter for _ in range(iteration_count): tmp = counter await asyncio.sleep(random.random()) counter = tmp + 1 tasks = [asyncio.create_task(increment()) for _ in range(task_count)] await asyncio.gather(*tasks) print(f"Asyncio Final Counter was {counter} expected {iteration_count * task_count}") def main(): print("\nMultithreading Example:") multithreading_race_condition() print("\nAsyncio Example:") asyncio.run(asyncio_race_condition()) if __name__ == "__main__": main()  That should likely give you something like: Multithreading Example: Multithreading Final Counter was 10 expected 100 Asyncio Example: Asyncio Final Counter was 10 expected 100  Dramatically illustrating a race condition. Here is a second way to demonstrate a race using code a little more like what you have. Note that if counter is loaded first you more likely will find a race and if loaded last you are likely not to find one. You can use dis to see how that might be true: import dis test = """ counter = counter + 1 + (time.sleep(random.random()) or 0) """ print(dis.dis(test)) test2 = """ counter = 1 + (time.sleep(random.random()) or 0) + counter """ print(dis.dis(test2))  So, here are two examples that difffer only in the order of operations on either side of a long opperation. The first is very likely to demonstrate a race condition while the second is unlikely (though not impossible) to do so. import threading import time import random def multithreading_race_condition(): counter = 0 def increment(): nonlocal counter for _ in range(10): counter = counter + 1 + (time.sleep(random.random()) or 0) #counter = 1 + (time.sleep(random.random()) or 0) + counter threads = [threading.Thread(target=increment) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print(f"Multithreading Final Counter: {counter}") def multithreading_no_race_condition(): counter = 0 def increment(): nonlocal counter for _ in range(10): #counter = counter + 1 + (time.sleep(random.random()) or 0) counter = 1 + (time.sleep(random.random()) or 0) + counter threads = [threading.Thread(target=increment) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print(f"Multithreading Final Counter: {counter}") def main(): print("\nMultithreading Example with race:") multithreading_race_condition() print("\nMultithreading Example with (probably) no race:") multithreading_no_race_condition() if __name__ == "__main__": main()  I am guessing that will give you: Multithreading Example with race: Multithreading Final Counter: 10 Multithreading Example with (probably) no race: Multithreading Final Counter: 100 