Skip to content

Files, pathlib & os Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on open() and the with statement, lazy file iteration, text vs binary mode and encoding, pathlib.Path vs os.path, and globbing.

Read the in-depth guidePython Files & pathlib Explained — Reading, Writing, and Modern Path Handling(opens in new tab)
15 of 15

with open(...) uses the file as a context manager, which guarantees the file is closed when the block exits — even if an exception is raised. Without it you must remember to call .close() manually, and a crash mid-block leaks the handle.

with open("data.txt") as f:      # f.close() is automatic
    contents = f.read()
# file is closed here, even on error

f = open("data.txt")             # manual style — fragile
try:
    contents = f.read()
finally:
    f.close()

Leaked handles can exhaust OS file descriptors and leave buffered writes unflushed. Always prefer with for any resource that needs cleanup (files, locks, sockets).

A file object is its own iterator, yielding one line at a time and holding only that line in memory. read() loads the entire file into a single string, and readlines() loads all lines into a list — both can blow up memory on large files.

with open("huge.log") as f:
    for line in f:               # lazy — one line in memory at a time
        process(line)

with open("huge.log") as f:
    data = f.read()              # whole file as one string
    lines = f.readlines()        # whole file as a list of strings

Iterating is the idiomatic, memory-safe way to process big files line by line. Reserve read()/readlines() for small files where you genuinely need the whole content at once.

Text mode ("r", the default) decodes bytes into str using an encoding and normalizes newlines. Binary mode ("rb"/"wb") reads and writes raw bytes with no decoding — required for images, archives, or any non-text data. In text mode you should pass encoding= explicitly, because the default is platform-dependent.

with open("notes.txt", "r", encoding="utf-8") as f:
    text: str = f.read()         # decoded to str

with open("photo.jpg", "rb") as f:
    raw: bytes = f.read()        # raw bytes, no decoding

Relying on the default encoding is a classic cross-platform bug (UTF-8 on Linux/Mac, often a legacy codepage on Windows). Always specify encoding="utf-8" for text, and use binary mode for everything that isn't text.

pathlib.Path is the modern, object-oriented way to handle filesystem paths. A Path is an object with methods and operators, whereas the older os.path module is a collection of string-based functions. Path's / operator joins segments cleanly and works across operating systems.

from pathlib import Path

p = Path("data") / "logs" / "app.log"   # join with /
p.exists()
p.suffix                                 # ".log"
p.stem                                   # "app"
p.read_text(encoding="utf-8")            # one-liner read

import os.path
old = os.path.join("data", "logs", "app.log")
os.path.exists(old)

pathlib is generally preferred for new code: it's more readable and bundles common operations (read_text, mkdir, glob) as methods. Use os.path mainly when working with existing string-based APIs.

Use Path.glob(pattern) for matches in one directory and Path.rglob(pattern) (or glob("**/...")) to recurse into subdirectories. Both return a lazy generator of Path objects, where * matches any characters and ** matches directories recursively.

from pathlib import Path

root = Path("project")
for py in root.glob("*.py"):         # top level only
    print(py.name)

for py in root.rglob("*.py"):        # all subdirectories too
    print(py)

root.mkdir(parents=True, exist_ok=True)  # create dirs safely
[p.name for p in root.iterdir()]         # list directory contents

Other handy Path methods: iterdir() (list a directory), is_file()/is_dir(), mkdir(), unlink() (delete), and with_suffix(). Globbing returns generators, so wrap in list(...) if you need a concrete collection.

r read (default, must exist), w write (truncates/creates), a append (writes at end), x exclusive create (fails if exists), and + adds the other capability (read+write). Add b for binary.

open("f", "r")     # read existing
open("f", "w")     # overwrite/create — WIPES existing content!
open("f", "a")     # append to end, create if missing
open("f", "x")     # create new, FileExistsError if it exists
open("f", "r+")    # read and write, must exist

Rule of thumb: w destroys existing content — use a to append or x to avoid clobbering; combine with b ("rb", "wb") for binary data.

A Path exposes its components as properties: .name (file + ext), .stem (name without ext), .suffix (extension), .parent (containing dir), and .parts (tuple of all segments).

from pathlib import Path
p = Path("/home/user/report.tar.gz")
p.name        # 'report.tar.gz'
p.stem        # 'report.tar'
p.suffix      # '.gz'
p.suffixes    # ['.tar', '.gz']
p.parent      # Path('/home/user')
p.parts       # ('/', 'home', 'user', 'report.tar.gz')

Rule of thumb: use these properties instead of string splitting — .with_suffix()/ .with_name() build modified paths safely across platforms.

Path.resolve() returns an absolute, symlink-resolved path; .absolute() makes it absolute without resolving symlinks; .relative_to(base) computes a relative path. Path.cwd() and Path.home() give common roots.

from pathlib import Path
p = Path("data/file.txt")
p.resolve()                       # /full/abs/path/data/file.txt
Path.cwd()                        # current working directory
Path.home()                       # user home

abs_p = Path("/a/b/c.txt")
abs_p.relative_to("/a")           # Path('b/c.txt')

Rule of thumb: resolve() to normalize to a canonical absolute path; relative_to to express one path relative to a known base (raises if not a subpath).

Path.read_text()/write_text() and read_bytes()/write_bytes() are one-line open-read/write-close helpers — no with block needed for simple whole-file I/O. Pass encoding= for text.

from pathlib import Path
p = Path("notes.txt")
p.write_text("hello", encoding="utf-8")   # opens, writes, closes
content = p.read_text(encoding="utf-8")

Path("img.bin").write_bytes(b"\x00\x01")
data = Path("img.bin").read_bytes()

Rule of thumb: use read_text/write_text for quick whole-file access; switch to with open(...) when you need streaming, appending, or line-by-line processing.

Many os functions have Path method equivalents: os.mkdir→p.mkdir(), os.remove→p.unlink(), os.rename→p.rename(), os.listdir→p.iterdir(), os.path.exists→p.exists(). Directory trees still use shutil.

from pathlib import Path
import shutil

p = Path("out")
p.mkdir(parents=True, exist_ok=True)
(p / "a.txt").touch()             # create empty file
(p / "a.txt").unlink()            # delete a file
shutil.rmtree(p)                  # remove a whole directory tree
shutil.copy("src.txt", "dst.txt") # copy a file

Rule of thumb: prefer Path methods for single files/dirs; use shutil for recursive copy/move/delete of trees (pathlib has no rmtree).

Use the tempfile module: TemporaryDirectory and NamedTemporaryFile are context managers that auto-clean up on exit, and they create files securely (avoiding race conditions of hand-rolled temp names).

import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "work.txt"
    path.write_text("scratch")
# directory and contents removed automatically here

with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=True) as f:
    f.write("a,b\n")

Rule of thumb: never build temp paths by hand — tempfile gives secure, auto-cleaned temporary files/dirs via context managers.

tell() returns the current byte position; seek(offset, whence) moves it. whence is 0=start (default), 1=current, 2=end. Useful for re-reading, skipping headers, or random access in binary files.

with open("data.bin", "rb") as f:
    f.read(4)          # read header
    pos = f.tell()     # current position (4)
    f.seek(0)          # back to start
    f.seek(-10, 2)     # 10 bytes before end

Rule of thumb: use seek/tell for random access (mostly binary mode); in text mode only seek to positions returned by tell() (byte offsets aren't char counts).

* matches any run of characters within one path segment; ** matches across directories (recursive); ? matches a single character; [...] matches a character set. ** needs rglob or glob("**/...").

from pathlib import Path
root = Path("src")
list(root.glob("*.py"))        # .py in src only
list(root.glob("**/*.py"))     # .py at any depth
list(root.glob("test_?.py"))   # test_1.py, test_a.py
list(root.glob("[abc]*.txt"))  # starts with a, b, or c

Rule of thumb: * within a directory, ** for recursion, ?/[...] for single-char and set matches — same patterns the shell uses.

File writes are buffered — data sits in memory until the buffer fills, you call flush(), or the file is closed. A crash before flush/close loses buffered data. with guarantees a close (and flush).

f = open("log.txt", "w")
f.write("important")     # may still be in the buffer, not on disk
f.flush()                # force it to the OS now
import os
os.fsync(f.fileno())     # force OS to write to physical disk
f.close()                # flushes and closes

Rule of thumb: rely on with (or close()) to flush; call flush()/os.fsync() explicitly only when you need durability before the block ends.

Pass an errors= strategy to open/read_text: "strict" (default, raises), "ignore" (drop bad bytes), "replace" (insert �), or "backslashreplace". Useful for messy real-world data with unknown encodings.

open("messy.txt", encoding="utf-8", errors="replace").read()   # bad bytes -> 
open("messy.txt", encoding="utf-8", errors="ignore").read()    # drop bad bytes

from pathlib import Path
Path("messy.txt").read_text(encoding="utf-8", errors="replace")

Rule of thumb: keep errors="strict" to catch encoding problems; use replace/ ignore only when you must tolerate malformed input and can accept data loss.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel