An in-memory file system is a tree problem in disguise. Directories are nodes with children, files are leaves with content, and every operation starts by walking a path like /a/b/c from the root. Get one reusable traversal helper right and the rest is short.
Requirements#
Support the basic Unix-style operations:
- Create a file or directory at a path.
- Read and write a file's content.
- List a directory's contents.
- Delete a file or directory at a path.
- Paths are hierarchical, like
/a/b/c.
Actor: the user (or calling program).
Clarifications
- One kind of file (text content); a directory can hold many entries.
- Failure cases: invalid path, missing parent, duplicate name, writing to a directory, creating or deleting the root.
Out of scope: UI, concurrency, permissions, persistence.
Core entities#
Two classes are enough:
| Entity | Holds |
|---|---|
Node |
name, is_file, children: Dict[str, Node] (directories), content: str (files) |
FileSystem |
root node |
One Node type with an is_file flag keeps the code short. A directory uses children; a file uses content.
Relationships#
Everything hangs off the root as a tree:
FileSystem→ rootNode(composition).Node→ childNodes (composition: deleting a directory deletes its subtree).
For example, after the demo below creates /a/b/f.txt, the tree looks like this:
/(dir)a(dir)b(dir)f.txt(file, content"hello")
Key flows#
Every operation builds on one traversal helper.
Traverse(path): split on /, drop empty parts (so / → [] → root), then follow children one component at a time; return None if any step is missing.
Create(path): traverse to the parent → reject if missing, if the parent is a file, or if the name exists → add the new node.
Delete(path): traverse to the parent → remove the child by name (the whole subtree goes with it).
Code#
A single Node class plus the FileSystem that walks it:
from typing import Dict, List, Optional
class Node:
def __init__(self, name: str, is_file: bool):
self.name = name
self.is_file = is_file
self.children: Dict[str, "Node"] = {} # used by directories
self.content: str = "" # used by files
class FileSystem:
def __init__(self):
self.root = Node("/", is_file=False)
def _components(self, path: str) -> List[str]:
return [p for p in path.split("/") if p] # "/a//b/" -> ["a", "b"]
def _traverse(self, parts: List[str]) -> Optional[Node]:
node = self.root
for part in parts:
if node.is_file or part not in node.children:
return None
node = node.children[part]
return node
def create(self, path: str, is_file: bool) -> bool:
parts = self._components(path)
if not parts:
return False # can't create the root
parent = self._traverse(parts[:-1])
name = parts[-1]
# Missing parent, parent is a file, or duplicate name
if parent is None or parent.is_file or name in parent.children:
return False
parent.children[name] = Node(name, is_file)
return True
def read(self, path: str) -> Optional[str]:
node = self._traverse(self._components(path))
if node is None or not node.is_file:
return None
return node.content
def write(self, path: str, content: str) -> bool:
node = self._traverse(self._components(path))
if node is None or not node.is_file:
return False
node.content = content
return True
def ls(self, path: str) -> Optional[List[str]]:
node = self._traverse(self._components(path))
if node is None:
return None
if node.is_file:
return [node.name] # ls on a file lists the file itself
return sorted(node.children)
def delete(self, path: str) -> bool:
parts = self._components(path)
if not parts:
return False # can't delete the root
parent = self._traverse(parts[:-1])
if (parent is None or parent.is_file
or parts[-1] not in parent.children):
return False
del parent.children[parts[-1]] # removes the whole subtree
return True
if __name__ == "__main__":
fs = FileSystem()
print(fs.create("/a", is_file=False)) # True
print(fs.create("/a/b", is_file=False)) # True
print(fs.create("/a/b/f.txt", is_file=True)) # True
print(fs.create("/a/b/f.txt", True)) # False: duplicate
print(fs.create("/x/y", False)) # False: parent missing
print(fs.write("/a/b/f.txt", "hello")) # True
print(fs.read("/a/b/f.txt")) # hello
print(fs.ls("/a/b")) # ['f.txt']
print(fs.delete("/a/b/f.txt")) # True
print(fs.ls("/a/b")) # []
Extensions and trade-offs#
Common follow-ups and how the design absorbs them:
- Separate
FileandDirectoryclasses: a commonNodebase with two subclasses (the Composite pattern) removes theis_filechecks and lets each type carry only its own fields. More classes, but cleaner invariants. mkdir -p: create missing intermediate directories while traversing instead of failing.- Move/rename: detach the node from one parent's
childrenand attach it to another; check you're not moving a directory into its own subtree. - Size and metadata: add size, timestamps and permissions to
Node; directory size is the sum over its subtree. - Search: DFS from a node to find files by name or extension.
- Concurrency: a single lock is simplest; finer-grained locking (per directory) is possible but tricky because operations span parent and child.
Key takeaways#
- Model the file system as a tree; directories hold a
name → Nodemap. - Write one traversal helper and reuse it in every operation.
- Operations that change structure (create, delete) work on the parent of the target.
- Guard the root and the "parent is a file" case explicitly.
- The Composite pattern is the natural refactor to separate files and directories.