Python multiprocessing
Pool.apply_async
nested function
debugging
Python programming

Pool.apply_async nested function is not executed

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

If multiprocessing.Pool.apply_async() appears to ignore a nested function, the usual cause is pickling, not scheduling. Worker processes need to import and deserialize the callable they are asked to run. Nested functions, lambdas, and locally defined closures are not safely picklable in the standard multiprocessing model, so the task never starts the way you expect.

Why Nested Functions Fail

Pool.apply_async() sends the target function and its arguments to another process. That other process does not share the current stack frame. It needs a callable that can be imported by module name.

This works badly with nested functions:

python
1from multiprocessing import Pool
2
3
4def main():
5    def worker(x):
6        return x * 2
7
8    with Pool() as pool:
9        result = pool.apply_async(worker, (5,))
10        print(result.get())
11
12
13if __name__ == "__main__":
14    main()

On many systems, especially with the spawn start method used on Windows and often on macOS, that nested worker cannot be imported in the child process. The failure may look like "nothing happened" unless you check the result or the error callback.

Correct Fix: Move the Worker to Module Scope

Define the worker at the top level of the module.

python
1from multiprocessing import Pool
2
3
4def worker(x):
5    return x * 2
6
7
8def main():
9    with Pool() as pool:
10        result = pool.apply_async(worker, (5,))
11        print(result.get())
12
13
14if __name__ == "__main__":
15    main()

Now the child process can import worker by name, and the task executes normally.

Always Use the __main__ Guard

Multiprocessing code should almost always be protected by:

python
if __name__ == "__main__":
    main()

Without that guard, module import side effects can recursively spawn more processes or cause confusing startup failures. This is essential on platforms that use spawn.

Surface Errors Instead of Guessing

Another reason people think the nested function was "not executed" is that they never call get() on the AsyncResult, so exceptions stay hidden.

python
1from multiprocessing import Pool
2
3
4def worker(x):
5    return x * 2
6
7
8def handle_error(exc):
9    print(f"worker failed: {exc}")
10
11
12if __name__ == "__main__":
13    with Pool() as pool:
14        result = pool.apply_async(worker, (5,), error_callback=handle_error)
15        print(result.get())

result.get() re-raises the child exception in the parent. That makes debugging much easier than assuming the pool silently ignored the task.

Be Careful with Closures and Bound State

Even if you move the function to module scope, you can still break multiprocessing by passing objects that are not picklable.

Problem pattern:

python
1from multiprocessing import Pool
2
3
4class Job:
5    def __init__(self, factor):
6        self.factor = factor
7
8    def run(self, value):
9        return value * self.factor

Passing bound methods sometimes works, but it is safer to pass simple data and a top-level worker:

python
1from multiprocessing import Pool
2
3
4def worker(args):
5    value, factor = args
6    return value * factor
7
8
9if __name__ == "__main__":
10    with Pool() as pool:
11        result = pool.apply_async(worker, ((5, 3),))
12        print(result.get())

This keeps the cross-process boundary simple and predictable.

When to Use Threads Instead

If the job is mostly I/O-bound and you want to use nested functions or closures freely, a thread pool may be easier because threads share memory and do not pickle the callable.

python
1from multiprocessing.pool import ThreadPool
2
3
4def main():
5    factor = 3
6
7    def worker(x):
8        return x * factor
9
10    with ThreadPool() as pool:
11        result = pool.apply_async(worker, (5,))
12        print(result.get())
13
14
15if __name__ == "__main__":
16    main()

That is not a replacement for CPU-bound multiprocessing, but it is a useful design alternative when the pickling model is the real friction point.

Common Pitfalls

  • Passing a nested function, lambda, or local closure to apply_async.
  • Forgetting the if __name__ == "__main__": guard.
  • Never calling get() on AsyncResult, so exceptions stay hidden.
  • Passing non-picklable objects across process boundaries.
  • Using multiprocessing for I/O-bound work where a thread pool would be simpler.

Summary

  • 'Pool.apply_async() usually cannot execute nested functions reliably because workers need picklable top-level callables.'
  • Move the worker function to module scope.
  • Always protect pool startup with the __main__ guard.
  • Use result.get() or error_callback to surface real failures.
  • If the task is I/O-bound, consider threads instead of fighting the multiprocessing pickling model.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.