os.fork

One call, two processes: after fork() the parent and an exact copy of it both continue from the same line, told apart only by the return value. POSIX only - fork, forkpty and register_at_fork do not exist on Windows (nor on WASI, Android or iOS).

os functionPython 3 (all versions); register_at_fork 3.7+
Common call
pid = os.fork()
Returns
0 in the child, child pid in the parent
Replaces
subprocess / multiprocessing are the portable alternatives
Watch out
end the child with os._exit(), never by falling through
os.fork() / os.forkpty() / os.register_at_fork(*, beforebefore — register_at_fork(): called in the parent just before forking. Before-hooks run in reverse registration order.type: callable · default: None=None, after_in_parentafter_in_parent — register_at_fork(): called in the parent after the fork, in registration order.type: callable · default: None=None, after_in_childafter_in_child — register_at_fork(): called in the child after the fork, in registration order - the place to reset locks, random seeds or connections.type: callable · default: None=None)
→ int | tuple[int, int] | None

Parameters

NameTypeRequiredDescription
beforecallableno (None)register_at_fork(): called in the parent just before forking. Before-hooks run in reverse registration order.
after_in_parentcallableno (None)register_at_fork(): called in the parent after the fork, in registration order.
after_in_childcallableno (None)register_at_fork(): called in the child after the fork, in registration order - the place to reset locks, random seeds or connections.

Return value

int | tuple[int, int] | None — fork(): 0 in the child, the child pid in the parent. forkpty(): (pid, fd) where fd is the master end of the pseudo-terminal. register_at_fork(): None.

Common patterns

Classic fork, wait, exit
The child does its work and leaves with os._exit; the parent reaps it with waitpid.
import os
pid = os.fork()
if pid == 0:
    try:
        print('child working', flush=True)
    finally:
        os._exit(0)
else:
    _, status = os.waitpid(pid, 0)
    print('child exit code', os.waitstatus_to_exitcode(status))
Reset state in every forked child
register_at_fork hooks run for os.fork and for multiprocessing with the fork start method.
import os, random
os.register_at_fork(after_in_child=random.seed)
Run a child inside a pseudo-terminal
forkpty gives the parent the master fd; the pty module wraps this more portably.
import os
pid, fd = os.forkpty()
if pid == 0:
    os.execlp('ls', 'ls', '--color=auto')
else:
    output = os.read(fd, 1024)
    os.waitpid(pid, 0)
Portable: let multiprocessing pick
An explicit spawn context works the same on Windows, macOS and Linux.
import multiprocessing as mp

def work(n):
    return n * n

if __name__ == '__main__':
    with mp.get_context('spawn').Pool(2) as pool:
        print(pool.map(work, range(4)))

Examples

1. The portable start method
import multiprocessing as mp 'spawn' in mp.get_all_start_methods()
Returns
True
2. Choose it explicitly
import multiprocessing as mp mp.get_context('spawn').get_start_method()
Returns
'spawn'
3. Run code in a child process on any OS
import subprocess, sys r = subprocess.run([sys.executable, '-c', 'print("hello from the child")'], capture_output=True, text=True) r.stdout
Returns
'hello from the child\n'
4. The child's exit code
import subprocess, sys subprocess.run([sys.executable, '-c', 'raise SystemExit(5)']).returncode
Returns
5
5. Reading the fork() return value
def role(pid): return 'child' if pid == 0 else 'parent' [role(0), role(4242)]
Returns
['child', 'parent']
6. A pipe: how forked processes talk
import os r, w = os.pipe() os.write(w, b'hello') os.close(w) data = os.read(r, 100) os.close(r) data
Returns
b'hello'

Pitfalls

1. Leaving a child with sys.exit
sys.exit only raises SystemExit. Any try/except or finally in code the child inherited from the parent can catch it, and the child keeps running parent code. os._exit ends the process at once. Shown here in a child Python so it runs on every OS.
sys.exit
import subprocess, sys
code = 'import sys\ntry:\n    sys.exit(0)\nexcept SystemExit:\n    print("child kept running")'
subprocess.run([sys.executable, '-c', code], capture_output=True, text=True).stdout
'child kept running\n'
os._exit
import subprocess, sys
code = 'import os\ntry:\n    os._exit(0)\nexcept SystemExit:\n    print("child kept running")'
subprocess.run([sys.executable, '-c', code], capture_output=True, text=True).stdout
''
2. Losing the child output at os._exit
os._exit does not flush stdio buffers. When stdout is a pipe or file it is block-buffered, so whatever the child printed without flushing is gone.
print, then _exit
import subprocess, sys
code = 'import os; print("result", end=""); os._exit(0)'
subprocess.run([sys.executable, '-c', code], capture_output=True, text=True).stdout
''
flush first
import subprocess, sys
code = 'import os; print("result", end="", flush=True); os._exit(0)'
subprocess.run([sys.executable, '-c', code], capture_output=True, text=True).stdout
'result'

When to use

Use it
  • Unix daemons and servers that pre-fork workers
  • fork followed directly by an exec* call (what subprocess does under the hood on many systems)
  • register_at_fork: libraries that must reset locks, seeds or sockets in forked children
Reach for something else
  • Code that must run on Windows → subprocess or multiprocessing
  • Processes that already run threads → fork copies only the calling thread (3.12+ warns, see notes)
  • macOS programs using system frameworks (including urllib.request) - the docs call fork unsafe there

Notes

CPython impl
posix.fork (Modules/posixmodule.c) calls PyOS_BeforeFork, fork(), then PyOS_AfterFork_Parent / PyOS_AfterFork_Child - which run the register_at_fork hooks. Calling fork() in a subinterpreter raises RuntimeError (3.8+).
Availability
fork: POSIX; forkpty and register_at_fork: Unix; none of them exist on Windows, WASI, Android or iOS.
Threads
Since 3.12 fork() and forkpty() raise a DeprecationWarning when Python can detect that the process has more than one thread: only the calling thread survives in the child, and locks held by the others stay locked forever.
Hook order
before-hooks run in reverse registration order, after_in_parent and after_in_child hooks in registration order. There is no way to unregister. Calling register_at_fork() with no argument raises TypeError.
multiprocessing
On Linux with Python 3.12 the default start method is 'fork'; on Windows it is 'spawn' (fork is not available there). get_context('spawn') behaves the same everywhere.

FAQ

No. fork is POSIX only, and forkpty and register_at_fork are Unix only (none exist on Windows, WASI, Android or iOS), so on Windows os.fork raises AttributeError. Examples here never fork the page process; they show the portable alternatives - subprocess and multiprocessing with the spawn start method - and the child-process rules in a separate Python child.