What you'll learn
Quick Answer
mmap maps a file's bytes directly into a process's virtual address space, so reading or writing the file becomes a normal memory access (a load or store instruction) instead of a read() or write() syscall. The kernel's page cache backs the mapping, and page faults pull file data in on demand, the same mechanism that backs regular virtual memory. We proved this with a real Python script: writing through a memory map and reading the change back with completely separate, ordinary file I/O.
What mmap actually does
Every normal file read or write goes through a syscall. You call read(fd, buf, n), the kernel copies bytes from its internal page cache into a buffer you allocated in user space, and your program then works on that copy. Same story in reverse for write(): each call involves a context switch into the kernel and a copy of bytes between kernel memory and a buffer your program allocated.
mmap() skips that copy. It asks the kernel to take a range of a file and map it directly into your process's virtual address space, at some address the kernel picks (or one you suggest). After that call returns, you have a pointer — in Python, an object that behaves like a mutable byte array — and every byte of the file is just sitting there in your process's address space. Reading byte 40,000 of a mapped file is a single memory load instruction. Writing to it is a single memory store. There is no syscall per access, no intermediate buffer, and no explicit copy in your code.
This only works because of the virtual memory machinery your CPU and kernel already maintain for every process — the same page tables and page-fault handling that back ordinary heap and stack memory (if you want the page-table and TLB mechanics from the ground up, we've covered that separately in our virtual memory and paging post). mmap doesn't introduce a new memory system; it just tells the existing one "for this range of addresses, the backing store is this file, not anonymous swap space."
Page faults and the page cache
When you call mmap(), the kernel does not read the whole file into memory. It just creates page table entries marked "not present, backed by file X at offset Y" and hands you back a pointer. Nothing has actually been loaded yet. This is identical in spirit to demand paging for regular virtual memory.
The first time your code touches a byte in a given 4 KB page of that mapping, the CPU can't find a valid translation and raises a page fault. The kernel's fault handler looks at the page table entry, sees it's a file-backed mapping, and goes and fetches that page from disk into the page cache (or finds it already sitting in the page cache if some other part of the system read that file recently). It then rewrites the page table entry to point at that physical page, and resumes your instruction as if nothing happened. You experience this as a slightly slower first access to each page and fast access to every subsequent one.
This has a genuinely useful consequence: because the mapping is backed by the page cache, and the page cache is shared system-wide, two unrelated processes that mmap the same file with a shared mapping are, at the hardware level, looking at the same physical RAM pages. That's not an analogy — it's literally true, and it's why mmap is also a legitimate (and fast) inter-process communication mechanism, independent of its use for file I/O.
Proving it: a real mmap demo
Claims about mmap are easy to state and easy to get subtly wrong, so here's an actual script, run for real, that creates a file, maps it, writes through the mapping, and then reads the file back using completely ordinary file I/O — a separate open() call with no connection to the mapping — to prove the write landed in the real file, not just in this process's memory.
import mmap
import os
path = "mmap_testfile.bin"
# Create a file with known initial content
with open(path, "wb") as f:
f.write(b"Hello, world! This is a test file for mmap demonstration purposes.")
size = os.path.getsize(path)
print(f"Initial file size: {size} bytes")
# Open the file and memory-map it
with open(path, "r+b") as f:
mm = mmap.mmap(f.fileno(), 0) # 0 = map the whole file
print(f"First 20 bytes via mmap BEFORE write: {mm[0:20]}")
# Write directly into the mapping - no write() syscall from our code
mm[0:5] = b"HELLO"
print(f"First 20 bytes via mmap AFTER write: {mm[0:20]}")
mm.flush()
mm.close()
# Independent read using plain file I/O - new file handle, no mmap involved
with open(path, "rb") as f:
contents = f.read(20)
print(f"First 20 bytes via plain read(): {contents}")The actual captured output from running this:
Initial file size: 66 bytes
First 20 bytes via mmap BEFORE write: b'Hello, world! This i'
First 20 bytes via mmap AFTER write: b'HELLO, world! This i'
First 20 bytes via plain read(): b'HELLO, world! This i'Look closely at what happened. The code never called write(). It performed a slice assignment, mm[0:5] = b"HELLO", which is a pure in-memory operation from the program's point of view. And yet a totally separate open()/read() call — one that knows nothing about the mapping — sees the change. That's the whole mechanism in one script: the memory write went to a page-cache page the kernel considers to be this file, and mm.flush() (Python's wrapper around msync()) made sure that page's dirty content was pushed back to the actual disk block before we re-read it.
Shared vs. private mappings, and when writes actually land
The demo above used the default shared-style behavior you get from Python's mmap.mmap() on a file opened for read-write: this corresponds to MAP_SHARED at the OS level. Changes you make are visible to any other mapping of the same file (including other processes), and the kernel will eventually write those dirty pages back to disk on its own schedule — you don't have to call flush() for the write to "count" from other readers' perspective, only if you need a guarantee that it has survived to physical disk before your program continues (analogous to fsync() for regular file writes).
The alternative is MAP_PRIVATE: your writes trigger copy-on-write, meaning the kernel duplicates the page just for your process the moment you modify it, your changes become invisible to other mappings of the same file, and nothing is ever written back to the underlying file. This is the mode you want when you're using a file purely as a convenient way to load a large, read-mostly blob (say, a machine learning model's weights) and want to scribble on a private copy without any risk of corrupting the source file on disk.
This distinction matters because it's the difference between "mmap as a fast way to do file I/O" and "mmap as a way to get a large chunk of memory that happens to be initialized from a file." Both are legitimate uses; they're just different tools.
Why this matters: real systems that lean on mmap
LMDB (Lightning Memory-Mapped Database) takes this idea to its logical extreme: its entire storage engine is "open the database file, mmap the whole thing, and treat it as an in-memory B-tree." There is no separate buffer pool or cache management code in LMDB at all — it deliberately offloads that entire job to the kernel's page cache, which is already excellent at it. Reads are just pointer dereferences into the mapping.
SQLite offers an optional mmap mode (PRAGMA mmap_size) for the same reason: for read-heavy workloads on large databases, letting the kernel manage which pages are hot and which get evicted is often faster and simpler than SQLite's own page cache doing the same job with an extra layer of copying on top.
Outside of databases, mmap shows up anywhere a program needs to randomly access parts of a large file without reading the whole thing into a buffer up front: loading large static assets, parsing binary formats where you jump around by offset, and sharing a read-only dataset across multiple worker processes (fork a process after mmapping a big file, and every child shares the same physical pages for it, instead of each copying it into its own memory).
The gotchas that bite people in production
SIGBUS, not a clean exception. If another process truncates the file while you still have it mapped, and you then access a page that's now past the new end of the file, you don't get a Python exception you can catch gracefully — the OS delivers a SIGBUS signal, which by default kills the process. This is a sharp edge mmap has that regular file reads don't: a regular read() past EOF just returns less data or an empty result.
It's not automatically faster. For small files, or for a strictly sequential single pass over a file you're only going to read once, plain buffered read() can be just as fast or faster — the standard library already buffers reads efficiently, and setting up a memory mapping has its own fixed overhead (updating page tables, handling the first-touch page faults). mmap wins when you access a large file repeatedly or randomly, because you avoid paying a syscall-and-copy cost on every single access.
Dirty pages and durability timing are easy to get wrong. A write through a MAP_SHARED mapping is not guaranteed to be on disk the instant the write instruction executes — the kernel can hold it as a dirty page in the cache for a while. If you need a hard guarantee before proceeding (for example, before telling a caller a transaction is durable), you must call msync() / Python's mm.flush() explicitly, the same way you'd call fsync() after a regular write().
